mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
167
Commits
v1.3.9
...
v2.7.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fa9f6cb8 | ||
|
|
b98ca86886 | ||
|
|
d917c8ffe6 | ||
|
|
5d44737199 | ||
|
|
6e8693a6b0 | ||
|
|
6d9e7404a1 | ||
|
|
70ceb389b5 | ||
|
|
832e364a2f | ||
|
|
51f9c5c7d8 | ||
|
|
15141c8528 | ||
|
|
0cdac10fd4 | ||
|
|
aaab548bfd | ||
|
|
46611b5606 | ||
|
|
fdbdb46577 | ||
|
|
e7fd81d0eb | ||
|
|
ce50bae6a0 | ||
|
|
8331cf3866 | ||
|
|
670262e53b | ||
|
|
c584f942cf | ||
|
|
e83b6bb583 | ||
|
|
cc5de668f0 | ||
|
|
653c7b8c4b | ||
|
|
4e38bcd4cc | ||
|
|
fcbeda4888 | ||
|
|
4101fddc5b | ||
|
|
86df7bea22 | ||
|
|
4db8c0c286 | ||
|
|
1d03335903 | ||
|
|
b1b541014f | ||
|
|
b40a7e435f | ||
|
|
8305fb4c5f | ||
|
|
6c678be511 | ||
|
|
2823663f09 | ||
|
|
033c2264f8 | ||
|
|
8eb6ce19f7 | ||
|
|
1cfc4e6f65 | ||
|
|
454d76e305 | ||
|
|
f3609fc3a5 | ||
|
|
16da51d095 | ||
|
|
51fb4c7a29 | ||
|
|
0b27cb55dc | ||
|
|
9e836eef69 | ||
|
|
07be4b8258 | ||
|
|
217087c6d4 | ||
|
|
2315a18c86 | ||
|
|
f59e9d2dc5 | ||
|
|
1da220085c | ||
|
|
962b2a01f1 | ||
|
|
eeda14f424 | ||
|
|
6269e20085 | ||
|
|
3d05296eb2 | ||
|
|
e90ab95cd0 | ||
|
|
23ec2f025e | ||
|
|
624898dd5e | ||
|
|
c6d6604399 | ||
|
|
7473704954 | ||
|
|
7a6b216bf6 | ||
|
|
1ef09124af | ||
|
|
6340f73d61 | ||
|
|
b9f4992a33 | ||
|
|
07a1fee013 | ||
|
|
63f37e51ad | ||
|
|
01ea08a5cb | ||
|
|
bddb20c5bb | ||
|
|
f9a97297da | ||
|
|
5fc2b56635 | ||
|
|
172953ccb3 | ||
|
|
d55300f53d | ||
|
|
c3d4500e3a | ||
|
|
cf72e28727 | ||
|
|
b488a1be16 | ||
|
|
805d0cb4b6 | ||
|
|
fa045c2504 | ||
|
|
23fd373a9b | ||
|
|
ab619cb073 | ||
|
|
2d96822014 | ||
|
|
887adc87de | ||
|
|
4219bd02c5 | ||
|
|
48544464ae | ||
|
|
9b4f2477e4 | ||
|
|
26e8840484 | ||
|
|
b6ef6d3ce9 | ||
|
|
cdfcd24049 | ||
|
|
95a8e0b125 | ||
|
|
84b7509f33 | ||
|
|
a53baec9dc | ||
|
|
4b4bc86cf7 | ||
|
|
4d3860089e | ||
|
|
758f55f13c | ||
|
|
1e191c3adc | ||
|
|
1ba2cf7c9d | ||
|
|
9ed1cc00d5 | ||
|
|
3be1173197 | ||
|
|
f6e69c62b1 | ||
|
|
6d9679a7fc | ||
|
|
6ea260a81f | ||
|
|
26da600761 | ||
|
|
33172a7aee | ||
|
|
7eb9f6ac95 | ||
|
|
fc06c285d5 | ||
|
|
ca2e4fec79 | ||
|
|
87ca90f6cc | ||
|
|
7dfea78c87 | ||
|
|
d7808009dc | ||
|
|
62afb7a130 | ||
|
|
87bef9cb9c | ||
|
|
3860cb21c6 | ||
|
|
bf4ad7886c | ||
|
|
1d51c207f4 | ||
|
|
2eb3f70e0c | ||
|
|
eb4d883194 | ||
|
|
3f5c9ff9de | ||
|
|
47039796de | ||
|
|
fd054abcca | ||
|
|
2861bfbd47 | ||
|
|
6ea3127f9c | ||
|
|
f7acb5e115 | ||
|
|
b1ad1f7699 | ||
|
|
ab713df3c0 | ||
|
|
e0d44f2f1c | ||
|
|
61463bc05f | ||
|
|
a219fc3527 | ||
|
|
1cd4028ce8 | ||
|
|
de9a30eb93 | ||
|
|
2210561af3 | ||
|
|
c5ddc474c4 | ||
|
|
688748da36 | ||
|
|
753d311593 | ||
|
|
5bae3d9931 | ||
|
|
a3495a8218 | ||
|
|
5fa68f77a2 | ||
|
|
dbba2e9791 | ||
|
|
6207f26a68 | ||
|
|
7fcd39822e | ||
|
|
d20f8a604f | ||
|
|
a742646127 | ||
|
|
f5078d5ef2 | ||
|
|
3784084abc | ||
|
|
2f463b7735 | ||
|
|
7be5625d66 | ||
|
|
aef14a9cd2 | ||
|
|
67648a04d3 | ||
|
|
58345e3194 | ||
|
|
4cb2ad9a5a | ||
|
|
27395d8e22 | ||
|
|
230c5ea180 | ||
|
|
4d4370ddce | ||
|
|
7e913e7847 | ||
|
|
ec7871e4e9 | ||
|
|
cc5a45a14f | ||
|
|
5fff3457ba | ||
|
|
6f5d66c1d9 | ||
|
|
d94795bf4f | ||
|
|
009b71c42a | ||
|
|
a10716d74e | ||
|
|
b44bf5f401 | ||
|
|
e0af1838e2 | ||
|
|
7d5502a34f | ||
|
|
78ccddf87c | ||
|
|
df1bc48e84 | ||
|
|
3b48d61571 | ||
|
|
92d5e2a40d | ||
|
|
234857c414 | ||
|
|
b1bbe785ea | ||
|
|
487fb49bff | ||
|
|
b14b7fa926 | ||
|
|
4cc1ab143d |
+2
-1
@@ -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"
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Android CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
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 with Gradle
|
|
||||||
run: ./gradlew assembleDebug
|
|
||||||
- name: Run tests
|
|
||||||
run: ./gradlew testGplayDebugUnitTest testFossDebugUnitTest
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Code tests & eval
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-all:
|
||||||
|
name: Assemble all variants
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ Foss,Gplay ]
|
||||||
|
variant: [ Debug ]
|
||||||
|
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 (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||||
|
run: ./gradlew assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
name: Run all tests
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ Foss,Gplay ]
|
||||||
|
variant: [ Release ]
|
||||||
|
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: Run tests (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||||
|
run: ./gradlew test${{ matrix.flavor }}${{ matrix.variant }}
|
||||||
@@ -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 }}
|
||||||
+12
-2
@@ -1,4 +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
|
/fastlane/report.xml
|
||||||
|
|||||||
Generated
+167
@@ -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>
|
||||||
Generated
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
+37
-37
@@ -8,20 +8,20 @@ GEM
|
|||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.554.0)
|
aws-partitions (1.610.0)
|
||||||
aws-sdk-core (3.126.0)
|
aws-sdk-core (3.131.3)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.54.0)
|
aws-sdk-kms (1.58.0)
|
||||||
aws-sdk-core (~> 3, >= 3.126.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.112.0)
|
aws-sdk-s3 (1.114.0)
|
||||||
aws-sdk-core (~> 3, >= 3.126.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.5.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
@@ -36,8 +36,8 @@ GEM
|
|||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.91.0)
|
excon (0.92.4)
|
||||||
faraday (1.9.3)
|
faraday (1.10.0)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -56,8 +56,8 @@ GEM
|
|||||||
faraday-em_synchrony (1.0.0)
|
faraday-em_synchrony (1.0.0)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.0.3)
|
faraday-multipart (1.0.4)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (~> 2)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
@@ -66,7 +66,7 @@ GEM
|
|||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.6)
|
fastimage (2.2.6)
|
||||||
fastlane (2.204.3)
|
fastlane (2.208.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
@@ -106,9 +106,9 @@ GEM
|
|||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.3.0)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.16.0)
|
google-apis-androidpublisher_v3 (0.25.0)
|
||||||
google-apis-core (>= 0.4, < 2.a)
|
google-apis-core (>= 0.7, < 2.a)
|
||||||
google-apis-core (0.4.2)
|
google-apis-core (0.7.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -117,19 +117,19 @@ GEM
|
|||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.10.0)
|
google-apis-iamcredentials_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.4, < 2.a)
|
google-apis-core (>= 0.7, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.7.0)
|
google-apis-playcustomapp_v1 (0.10.0)
|
||||||
google-apis-core (>= 0.4, < 2.a)
|
google-apis-core (>= 0.7, < 2.a)
|
||||||
google-apis-storage_v1 (0.11.0)
|
google-apis-storage_v1 (0.18.0)
|
||||||
google-apis-core (>= 0.4, < 2.a)
|
google-apis-core (>= 0.7, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.5.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.2.0)
|
google-cloud-errors (1.2.0)
|
||||||
google-cloud-storage (1.36.1)
|
google-cloud-storage (1.37.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
@@ -137,20 +137,20 @@ GEM
|
|||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.1.0)
|
googleauth (1.2.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.4)
|
http-cookie (1.0.5)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
jmespath (1.5.0)
|
jmespath (1.6.1)
|
||||||
json (2.6.1)
|
json (2.6.2)
|
||||||
jwt (2.3.0)
|
jwt (2.4.1)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
@@ -161,9 +161,9 @@ GEM
|
|||||||
optparse (0.1.1)
|
optparse (0.1.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.6.0)
|
plist (3.6.0)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.7)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
representable (3.1.1)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
@@ -173,9 +173,9 @@ GEM
|
|||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
security (0.1.3)
|
security (0.1.3)
|
||||||
signet (0.16.0)
|
signet (0.17.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simctl (1.6.8)
|
simctl (1.6.8)
|
||||||
@@ -192,11 +192,11 @@ GEM
|
|||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (1.8.0)
|
unicode-display_width (1.8.0)
|
||||||
webrick (1.7.0)
|
webrick (1.7.0)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.21.0)
|
xcodeproj (1.22.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
|||||||
+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:
|
||||||
|
|
||||||
@@ -14,14 +15,18 @@ A companion app that adds support for AirPod specific features to Android:
|
|||||||
* Ear detection with automatic play/pause.
|
* Ear detection with automatic play/pause.
|
||||||
* Automatically connect phone & AirPods.
|
* Automatically connect phone & AirPods.
|
||||||
* Show popup when case is opened.
|
* Show popup when case is opened.
|
||||||
|
* Support for Wear OS
|
||||||
|
* Widgets
|
||||||
|
|
||||||
CAPod is ad-free. Some additional features require an in-app purchase.
|
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 Pro
|
* AirPods 3. Generation
|
||||||
|
* AirPods Pro 1. Generation
|
||||||
|
* AirPods Pro 2. Generation
|
||||||
* AirPods Max
|
* AirPods Max
|
||||||
* Power Beats Pro
|
* Power Beats Pro
|
||||||
* Power Beats 3
|
* Power Beats 3
|
||||||
@@ -29,27 +34,38 @@ Currently supported models:
|
|||||||
* Beats Studio 3
|
* Beats Studio 3
|
||||||
* Beats X
|
* Beats X
|
||||||
* Beats Flex
|
* Beats Flex
|
||||||
|
* Beats Fit Pro
|
||||||
|
* Fake AirPods 1. Generation 🎭
|
||||||
|
* Fake AirPods 2. Generation 🎭
|
||||||
|
* Fake AirPods 3. Generation 🎭
|
||||||
|
* Fake AirPods Pro1 🎭
|
||||||
|
* Fake AirPods Pro2 🎭
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
| Source | Status |
|
||||||
* [GitHub](https://github.com/d4rken/capod/releases/latest)
|
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||||
|
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||||
|
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||||
|
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||||
|
|
||||||
|
|
||||||
## 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)
|
||||||
|
* [Sponsor development](https://github.com/sponsors/d4rken) on GitHub
|
||||||
* 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)
|
||||||
|
|
||||||
## 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">
|
||||||
|
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
|
||||||
## Thanks to
|
## Thanks to
|
||||||
|
|
||||||
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
|
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
Vendored
+21
@@ -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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.darken.capod.common">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<receiver
|
||||||
|
android:name=".bluetooth.BleScanResultReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
</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"
|
||||||
|
}
|
||||||
+2
-2
@@ -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,5 @@
|
|||||||
|
package eu.darken.capod.common
|
||||||
|
|
||||||
|
object PrivacyPolicy {
|
||||||
|
const val URL = "https://github.com/d4rken-org/capod/blob/main/PRIVACY_POLICY.md"
|
||||||
|
}
|
||||||
+10
-4
@@ -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,33 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
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.logTag
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BleScanResultForwarder @Inject constructor() {
|
||||||
|
|
||||||
|
private val forwarder = MutableSharedFlow<Collection<ScanResult>>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 128,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val results: Flow<Collection<ScanResult>> = forwarder
|
||||||
|
|
||||||
|
fun forward(scanResults: Collection<ScanResult>) {
|
||||||
|
log(TAG, VERBOSE) { "forward($scanResults)" }
|
||||||
|
val success = forwarder.tryEmit(scanResults)
|
||||||
|
if (!success) log(TAG, WARN) { "Failed to forward (overflow?) $scanResults" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
|
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.logTag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class BleScanResultReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||||
|
@Inject lateinit var scanResultForwarder: BleScanResultForwarder
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
log(TAG, VERBOSE) { "onReceive($context, $intent)" }
|
||||||
|
if (intent.action != ACTION) {
|
||||||
|
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (intent.extras == null) {
|
||||||
|
log(TAG) { "Extras are null!" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0)
|
||||||
|
log(TAG, VERBOSE) { "errorCode=$errorCode" }
|
||||||
|
if (errorCode != 0) {
|
||||||
|
log(TAG, WARN) { "ScanCallback error code: $errorCode" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1)
|
||||||
|
log(TAG, VERBOSE) { "callbackType=$callbackType" }
|
||||||
|
|
||||||
|
val scanResults = intent.getParcelableArrayListExtra<ScanResult>(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT)
|
||||||
|
log(TAG, VERBOSE) { "scanResults=$scanResults" }
|
||||||
|
|
||||||
|
if (scanResults == null) {
|
||||||
|
log(TAG) { "Scan results were empty!" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val pending = goAsync()
|
||||||
|
appScope.launch {
|
||||||
|
scanResultForwarder.forward(scanResults)
|
||||||
|
pending.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder", "Receiver")
|
||||||
|
const val ACTION = "eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
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 android.content.Intent
|
||||||
|
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 eu.darken.capod.common.notifications.PendingIntentCompat
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
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,
|
||||||
|
private val scanResultForwarder: BleScanResultForwarder,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission") fun scan(
|
||||||
|
filters: Set<ScanFilter>,
|
||||||
|
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
||||||
|
disableOffloadFiltering: Boolean = true,
|
||||||
|
disableOffloadBatching: Boolean = true,
|
||||||
|
disableDirectScanCallback: Boolean = true,
|
||||||
|
): Flow<Collection<BleScanResult>> = callbackFlow {
|
||||||
|
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
||||||
|
|
||||||
|
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
||||||
|
|
||||||
|
val useOffloadedFiltering = adapter.isOffloadedFilteringSupported.also {
|
||||||
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
||||||
|
} && !disableOffloadFiltering
|
||||||
|
if (disableOffloadFiltering) log(TAG, WARN) { "Offloaded filtering is disabled!" }
|
||||||
|
|
||||||
|
val useOffloadedBatching = adapter.isOffloadedScanBatchingSupported.also {
|
||||||
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
||||||
|
} && !disableOffloadBatching
|
||||||
|
if (disableOffloadBatching) log(TAG, WARN) { "Offloaded scan-batching is disabled!" }
|
||||||
|
|
||||||
|
if (disableDirectScanCallback) log(TAG, WARN) { "Direct scan callback is disabled!" }
|
||||||
|
|
||||||
|
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
|
||||||
|
|
||||||
|
val filterResults: (Collection<ScanResult>) -> Collection<BleScanResult> = { results ->
|
||||||
|
results
|
||||||
|
.filter { result ->
|
||||||
|
val passed = when {
|
||||||
|
useOffloadedFiltering -> true
|
||||||
|
filters.isEmpty() -> true
|
||||||
|
else -> filters.any { it.matches(result) }
|
||||||
|
}
|
||||||
|
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
|
||||||
|
passed
|
||||||
|
}
|
||||||
|
.map { BleScanResult.fromScanResult(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
trySend(filterResults(setOf(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
|
log(TAG, VERBOSE) {
|
||||||
|
val delay = System.currentTimeMillis() - lastScanAt
|
||||||
|
lastScanAt = System.currentTimeMillis()
|
||||||
|
"onBatchScanResults(delay=${delay}ms, results=$results)"
|
||||||
|
}
|
||||||
|
|
||||||
|
trySend(filterResults(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
log(TAG, WARN) { "onScanFailed(errorCode=$errorCode)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val forwarderConsumer = if (disableDirectScanCallback) {
|
||||||
|
scanResultForwarder.results
|
||||||
|
.onEach { results -> trySend(filterResults(results)) }
|
||||||
|
.launchIn(this)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val flushJob = if (!disableDirectScanCallback) {
|
||||||
|
launch {
|
||||||
|
log(TAG) { "Flush job launched" }
|
||||||
|
while (isActive) {
|
||||||
|
log(TAG, VERBOSE) { "Flushing scan results." }
|
||||||
|
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
|
||||||
|
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> break
|
||||||
|
ScannerMode.BALANCED -> delay(2000)
|
||||||
|
ScannerMode.LOW_LATENCY -> delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val filterList = when {
|
||||||
|
useOffloadedFiltering -> filters.toList()
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val scanSettings = ScanSettings.Builder().apply {
|
||||||
|
setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
ScannerMode.BALANCED -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_BALANCED)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
ScannerMode.LOW_LATENCY -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val delay = if (useOffloadedBatching) {
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> 2000L
|
||||||
|
ScannerMode.BALANCED -> 1000L
|
||||||
|
ScannerMode.LOW_LATENCY -> 500L
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0L // Anything > 0 enables batching
|
||||||
|
}
|
||||||
|
setReportDelay(delay)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
if (disableDirectScanCallback) {
|
||||||
|
val callbackIntent = createStartIntent()
|
||||||
|
log(TAG) { "Intent callback: startScan(filters=$filters, settings=$scanSettings, callbackIntent=$callbackIntent)" }
|
||||||
|
scanner.startScan(filterList, scanSettings, callbackIntent)
|
||||||
|
} else {
|
||||||
|
log(TAG) { "Direct callback: startScan(filters=$filters, settings=$scanSettings, callback=$callback)" }
|
||||||
|
scanner.startScan(filterList, scanSettings, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
forwarderConsumer?.cancel()
|
||||||
|
flushJob?.cancel()
|
||||||
|
if (disableDirectScanCallback) {
|
||||||
|
scanner.stopScan(createStopIntent())
|
||||||
|
} else {
|
||||||
|
scanner.stopScan(callback)
|
||||||
|
}
|
||||||
|
log(TAG) { "BleScanner stopped" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { fakeBleData.maybeAddfakeData(it) }
|
||||||
|
|
||||||
|
private val receiverIntent by lazy {
|
||||||
|
Intent(context, BleScanResultReceiver::class.java).apply {
|
||||||
|
action = BleScanResultReceiver.ACTION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStartIntent(): PendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
CALLBACK_INTENT_REQUESTCODE,
|
||||||
|
receiverIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createStopIntent(): PendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
270,
|
||||||
|
receiverIntent,
|
||||||
|
PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CALLBACK_INTENT_REQUESTCODE = 270
|
||||||
|
private val TAG = logTag("Bluetooth", "BleScanner")
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -20,7 +20,6 @@ import kotlinx.coroutines.channels.awaitClose
|
|||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -31,12 +30,11 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val adapter: BluetoothAdapter
|
val adapter: BluetoothAdapter?
|
||||||
get() = manager.adapter
|
get() = manager.adapter
|
||||||
|
|
||||||
val scanner: BluetoothLeScanner
|
val scanner: BluetoothLeScanner?
|
||||||
get() = adapter.bluetoothLeScanner
|
get() = adapter?.bluetoothLeScanner
|
||||||
?: throw IllegalStateException("Bluetooth is disabled or permissiong missing")
|
|
||||||
|
|
||||||
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
|
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
|
||||||
send(manager.adapter?.isEnabled ?: false)
|
send(manager.adapter?.isEnabled ?: false)
|
||||||
@@ -159,7 +157,9 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
|
fun bondedDevices(): Flow<Set<BluetoothDevice>> = flow {
|
||||||
|
emit(adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable"))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||||
try {
|
try {
|
||||||
+9
-1
@@ -3,6 +3,7 @@ package eu.darken.capod.common.bluetooth
|
|||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class FakeBleData @Inject constructor(
|
|||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
|
fun maybeAddfakeData(originals: Collection<BleScanResult>): Collection<BleScanResult> {
|
||||||
if (!debugSettings.showFakeData.value) return originals
|
if (!debugSettings.showFakeData.value) return originals
|
||||||
return originals + getFakeData()
|
return originals + getFakeData()
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ class FakeBleData @Inject constructor(
|
|||||||
val fakeDevices = mutableListOf<BleScanResult>()
|
val fakeDevices = mutableListOf<BleScanResult>()
|
||||||
// AirPods Gen1
|
// AirPods Gen1
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "78:73:AF:B4:85:22",
|
address = "78:73:AF:B4:85:22",
|
||||||
rssi = Random.nextInt(100) * -1,
|
rssi = Random.nextInt(100) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
@@ -32,6 +34,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// AirPods Gen2
|
// AirPods Gen2
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "78:73:FF:B4:85:5E",
|
address = "78:73:FF:B4:85:5E",
|
||||||
rssi = Random.nextInt(100) * -1,
|
rssi = Random.nextInt(100) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
@@ -44,6 +47,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// AirPods Gen3
|
// AirPods Gen3
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "4E:9E:D1:49:D2:6D",
|
address = "4E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||||
@@ -55,6 +59,7 @@ class FakeBleData @Inject constructor(
|
|||||||
}
|
}
|
||||||
// AirPods Max
|
// AirPods Max
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "7E:E5:C7:65:D2:B5",
|
address = "7E:E5:C7:65:D2:B5",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||||
@@ -66,6 +71,7 @@ class FakeBleData @Inject constructor(
|
|||||||
}
|
}
|
||||||
// BeatsFlex
|
// BeatsFlex
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "5E:9E:D1:49:D2:6D",
|
address = "5E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
@@ -78,6 +84,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// Tws i99999
|
// Tws i99999
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "5E:9E:D1:29:D2:6D",
|
address = "5E:9E:D1:29:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
@@ -90,6 +97,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// Unknown Device
|
// Unknown Device
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "6E:9E:D1:49:D2:6D",
|
address = "6E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||||
+2
-2
@@ -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(
|
||||||
+2
-2
@@ -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
|
||||||
+6
-2
@@ -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)
|
||||||
+3
-4
@@ -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
-1
@@ -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
|
||||||
+9
-3
@@ -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 -> {
|
||||||
+9
-1
@@ -7,6 +7,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.error.hasCause
|
import eu.darken.capod.common.error.hasCause
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
|||||||
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 {
|
||||||
@@ -71,4 +72,11 @@ fun <T> Flow<T>.setupCommonEventHandlers(tag: String, identifier: () -> String)
|
|||||||
log(tag, ERROR) { "${identifier()} failed: ${it.asLog()}" }
|
log(tag, ERROR) { "${identifier()} failed: ${it.asLog()}" }
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.throttleLatest(delayMillis: Long): Flow<T> = this
|
||||||
|
.conflate()
|
||||||
|
.transform {
|
||||||
|
emit(it)
|
||||||
|
delay(delayMillis)
|
||||||
}
|
}
|
||||||
+5
@@ -9,4 +9,9 @@ object PendingIntentCompat {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
val FLAG_MUTABLE: Int = if (hasApiLevel(31)) {
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
-2
@@ -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(
|
||||||
@@ -71,7 +71,13 @@ enum class Permission(
|
|||||||
isGranted = {
|
isGranted = {
|
||||||
android.provider.Settings.canDrawOverlays(it)
|
android.provider.Settings.canDrawOverlays(it)
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
POST_NOTIFICATIONS(
|
||||||
|
minApiLevel = Build.VERSION_CODES.S,
|
||||||
|
labelRes = R.string.permission_post_notifications_label,
|
||||||
|
descriptionRes = R.string.permission_post_notifications_description,
|
||||||
|
permissionId = "android.permission.POST_NOTIFICATIONS",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Permission.isRequired(context: Context): Boolean = when {
|
fun Permission.isRequired(context: Context): Boolean = when {
|
||||||
+1
-1
@@ -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
-1
@@ -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)
|
||||||
}
|
}
|
||||||
+2
@@ -9,6 +9,8 @@ interface UpgradeRepo {
|
|||||||
|
|
||||||
fun launchBillingFlow(activity: Activity)
|
fun launchBillingFlow(activity: Activity)
|
||||||
|
|
||||||
|
fun getSponsorUrl(): String? = null
|
||||||
|
|
||||||
interface Info {
|
interface Info {
|
||||||
val type: Type
|
val type: Type
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user