mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
788038beb7 | ||
|
|
18ac05ed80 | ||
|
|
02f68821d2 | ||
|
|
62886f00f1 | ||
|
|
f2eb45d00b | ||
|
|
2fca9097c2 | ||
|
|
051677e7dd | ||
|
|
fbcd595b45 | ||
|
|
aa38f58f62 | ||
|
|
a3b01a02ee | ||
|
|
8544983984 | ||
|
|
07a11799b1 | ||
|
|
65a7b1933a | ||
|
|
bbfb735124 | ||
|
|
7b474fb0e1 | ||
|
|
c6524d7cee | ||
|
|
148cdf5109 | ||
|
|
60a3039415 | ||
|
|
6f7b6e30c9 | ||
|
|
08c095eae4 | ||
|
|
9087be6282 | ||
|
|
d46fe35eab | ||
|
|
1afb77ee33 | ||
|
|
a9215fc11c | ||
|
|
d80a110280 | ||
|
|
fdfc7f44fb | ||
|
|
26fbfc08f7 | ||
|
|
89049ee3ab | ||
|
|
68f81f900b | ||
|
|
513bd891af | ||
|
|
37cd753ab8 | ||
|
|
34bf2311b4 | ||
|
|
a86093f617 | ||
|
|
71f66e6004 | ||
|
|
2d1ab85e01 | ||
|
|
f029eb9102 | ||
|
|
471837ab88 | ||
|
|
1e80d742cc | ||
|
|
16b6273938 | ||
|
|
3407370563 | ||
|
|
3cda73a36c | ||
|
|
b604c423ad | ||
|
|
0c8b121c2c | ||
|
|
da11961611 | ||
|
|
48b9952fc0 | ||
|
|
99fa99eba5 | ||
|
|
06e67e04ac | ||
|
|
4a2f7c20a2 | ||
|
|
82e8769407 | ||
|
|
ad38702df1 | ||
|
|
3f98c62a8f | ||
|
|
ec7447992f | ||
|
|
99958e13eb | ||
|
|
b9fe5d4a14 | ||
|
|
1443fd5714 | ||
|
|
798083b235 | ||
|
|
18a7311089 | ||
|
|
9f49d62f12 | ||
|
|
0c3c4e7ea1 | ||
|
|
7cdf653842 | ||
|
|
32d99eada8 | ||
|
|
9e4adb16c6 | ||
|
|
4b5638e4f0 | ||
|
|
d8fb9cdd30 | ||
|
|
3920489dcd | ||
|
|
09d013588d | ||
|
|
d8bd03c65d | ||
|
|
8b2c6b9498 | ||
|
|
0f63d3a0d4 | ||
|
|
672eb333b0 | ||
|
|
de609b4d05 | ||
|
|
7e6e9eee36 | ||
|
|
a321bb733c | ||
|
|
490369a3e1 | ||
|
|
a2cce044ef | ||
|
|
8884bb88c7 | ||
|
|
313b0e9131 | ||
|
|
47048cf7e7 | ||
|
|
ef1c4615aa | ||
|
|
c9587b2bf0 | ||
|
|
669fa2fd22 | ||
|
|
fb8ec72825 | ||
|
|
d1687f574f | ||
|
|
72f47bec1f | ||
|
|
20e8aecf83 | ||
|
|
ee41474670 | ||
|
|
ac29e28eda | ||
|
|
58b96cbdee | ||
|
|
0c1fc15251 | ||
|
|
7b8e0cc952 | ||
|
|
c8aa591feb |
@@ -48,7 +48,6 @@ jobs:
|
||||
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')"
|
||||
@@ -58,7 +57,6 @@ jobs:
|
||||
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')
|
||||
@@ -138,6 +136,14 @@ jobs:
|
||||
ruby-version: 2.7.6
|
||||
bundler-cache: true
|
||||
|
||||
# - name: Assemble WearOS beta and upload to Google Play
|
||||
# if: contains(steps.tagger.outputs.tag, '-beta')
|
||||
# run: bundle exec fastlane beta_wearos
|
||||
# env:
|
||||
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Assemble beta and upload to Google Play
|
||||
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||
run: bundle exec fastlane beta
|
||||
@@ -145,7 +151,14 @@ jobs:
|
||||
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
|
||||
# - name: Assemble WearOS production and upload to Google Play
|
||||
# if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||
# run: bundle exec fastlane production_wearos
|
||||
# env:
|
||||
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Assemble production and upload to Google Play
|
||||
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||
@@ -153,5 +166,4 @@ jobs:
|
||||
env:
|
||||
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
+42
-42
@@ -1,27 +1,27 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
CFPropertyList (3.0.6)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.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-partitions (1.784.0)
|
||||
aws-sdk-core (3.177.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (1.70.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-s3 (1.128.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@@ -30,14 +30,14 @@ GEM
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.4)
|
||||
faraday (1.10.0)
|
||||
excon (0.100.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -65,8 +65,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.208.0)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.213.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -90,7 +90,7 @@ GEM
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
@@ -106,9 +106,9 @@ GEM
|
||||
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)
|
||||
google-apis-androidpublisher_v3 (0.45.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -117,27 +117,27 @@ GEM
|
||||
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-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 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)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.2.0)
|
||||
googleauth (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -148,20 +148,20 @@ GEM
|
||||
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)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.3.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)
|
||||
plist (3.7.0)
|
||||
public_suffix (5.0.1)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -178,7 +178,7 @@ GEM
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
@@ -194,7 +194,7 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
|
||||
+28
-8
@@ -20,23 +20,43 @@ On Android 12+ the newer and more fine grained `BLUETOOTH_SCAN` permission is us
|
||||
|
||||
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.
|
||||
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.
|
||||
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 error reports
|
||||
|
||||
Anonymous device information may be collected in the event of an app crash or error.
|
||||
*This was removed in v2.11.0+*
|
||||
|
||||
To do this the app uses the service "Bugsnag":
|
||||
If an error occurs, an automated report may be sent to help me fix the issue.
|
||||
|
||||
This is optional and you can opt out of this in the settings.
|
||||
|
||||
Error reports are collected using "Bugsnag":
|
||||
https://www.bugsnag.com/
|
||||
|
||||
Bugsnag's privacy policy can be found here:
|
||||
https://docs.bugsnag.com/legal/privacy-policy/
|
||||
|
||||
Crash reports may contain device and app related information, e.g. your phone model, Android version and app version.
|
||||
Error reports contain device and app information related to the error that occured.
|
||||
Additional information about the error context may also be included, e.g. what this app did shortly before the error.
|
||||
|
||||
You can disable automatic reports in the app's settings.
|
||||
Additional details about the type of data that is collected by the error tracking SDK can be found here:
|
||||
https://docs.bugsnag.com/platforms/android/playstore-privacy/
|
||||
|
||||
Error reports are pseudonymous. Unless you tell me your install-ID, I don't know that an error report came from you.
|
||||
|
||||
Error reports are automatically deleted after 90 days.
|
||||
|
||||
## Debug logs
|
||||
|
||||
The app has a debug log feature that can be used to assist troubleshooting efforts. This feature creates a log file that contains verbose output of what the app is doing.
|
||||
|
||||
It is manually triggered by the user through an option in the app settings. The recorded log file can be shared through compatible apps (e.g. your email app) using the system's share dialog. As this log file may contain sensitive information (e.g. details about files or installed applications) it should only be shared with trusted parties.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
[](https://github.com/d4rken-org/capod/releases/latest)
|
||||
[](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
|
||||
[](https://crowdin.com/project/capod)
|
||||
[](https://github.com/d4rken-org/capod/releases/latest)
|
||||
[](https://github.com/d4rken-org/capod/edit/main/README.md#download)
|
||||
[](https://github.com/d4rken-org/capod/edit/main/README.md#download)
|
||||
|
||||
A companion app that adds support for AirPod specific features to Android:
|
||||
|
||||
@@ -27,9 +28,11 @@ Currently supported models:
|
||||
* AirPods 3. Generation
|
||||
* AirPods Pro 1. Generation
|
||||
* AirPods Pro 2. Generation
|
||||
* AirPods Pro 2. Generation (USB-C)
|
||||
* AirPods Max
|
||||
* Power Beats Pro
|
||||
* Power Beats 3
|
||||
* Power Beats 4
|
||||
* Beats Solo 3
|
||||
* Beats Studio 3
|
||||
* Beats X
|
||||
@@ -75,6 +78,8 @@ Currently supported models:
|
||||
Travis & Rye, Erik & Sipes, Brandon & Teplov, Sam. (2019). Handoff All Your Privacy – A Review of Apple’s Bluetooth
|
||||
Low Energy Continuity Protocol. Proceedings on Privacy Enhancing Technologies. 2019. 34-53. 10.2478/popets-2019-0057.
|
||||
* Celosia, Guillaume. (2020). Privacy Challenges in Wireless Communications of the Internet of Things.
|
||||
* The [MagicPods](https://github.com/steam3d/MagicPods-Windows) project. If you are looking for "CAPod for Windows",
|
||||
check it out.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ android {
|
||||
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
|
||||
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
||||
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
||||
|
||||
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
|
||||
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
|
||||
) ?: "bugsnag_apikey_placeholder"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -100,7 +96,4 @@ dependencies {
|
||||
|
||||
addTesting()
|
||||
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest>
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${bugsnagApiKey}" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,58 +0,0 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.bugsnag.android.Event
|
||||
import com.bugsnag.android.OnErrorCallback
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BugsnagErrorHandler @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val bugsnagLogger: BugsnagLogger,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : OnErrorCallback {
|
||||
|
||||
override fun onError(event: Event): Boolean {
|
||||
bugsnagLogger.injectLog(event)
|
||||
|
||||
TAB_APP.also { tab ->
|
||||
event.addMetadata(tab, "gitSha", BuildConfigWrap.GIT_SHA)
|
||||
event.addMetadata(tab, "buildTime", BuildConfigWrap.BUILDTIME)
|
||||
|
||||
context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) }
|
||||
}
|
||||
|
||||
return debugSettings.isAutoReportingEnabled.value && !eu.darken.capod.common.BuildConfigWrap.DEBUG
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAB_APP = "app"
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
fun Context.tryFormattedSignature(): String? = try {
|
||||
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures?.let { sigs ->
|
||||
val sb = StringBuilder("[")
|
||||
for (i in sigs.indices) {
|
||||
sb.append(sigs[i].hashCode())
|
||||
if (i + 1 != sigs.size) sb.append(", ")
|
||||
}
|
||||
sb.append("]")
|
||||
sb.toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log(WARN) { e.asLog() }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import com.bugsnag.android.Event
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import java.lang.String.format
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BugsnagLogger @Inject constructor() : Logging.Logger {
|
||||
|
||||
// Adding one to the initial size accounts for the add before remove.
|
||||
private val buffer: Deque<String> = ArrayDeque(BUFFER_SIZE + 1)
|
||||
|
||||
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
|
||||
val line = "${System.currentTimeMillis()} ${priority.toLabel()}/$tag: $message"
|
||||
synchronized(buffer) {
|
||||
buffer.addLast(line)
|
||||
if (buffer.size > BUFFER_SIZE) {
|
||||
buffer.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun injectLog(event: Event) {
|
||||
synchronized(buffer) {
|
||||
var i = 100
|
||||
buffer.forEach { event.addMetadata("Log", format(Locale.ROOT, "%03d", i++), it) }
|
||||
event.addMetadata("Log", format(Locale.ROOT, "%03d", i), event.originalError?.asLog())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BUFFER_SIZE = 200
|
||||
|
||||
private fun Logging.Priority.toLabel(): String = when (this) {
|
||||
Logging.Priority.VERBOSE -> "V"
|
||||
Logging.Priority.DEBUG -> "D"
|
||||
Logging.Priority.INFO -> "I"
|
||||
Logging.Priority.WARN -> "W"
|
||||
Logging.Priority.ERROR -> "E"
|
||||
Logging.Priority.ASSERT -> "WTF"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,15 @@ package eu.darken.capod.debug.autoreport
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.bugsnag.android.Bugsnag
|
||||
import com.bugsnag.android.Configuration
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.InstallId
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
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 javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@@ -23,9 +18,6 @@ class GplayAutoReporting @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val debugSettings: DebugSettings,
|
||||
private val installId: InstallId,
|
||||
private val bugsnagLogger: Provider<BugsnagLogger>,
|
||||
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
|
||||
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
|
||||
) : AutomaticBugReporter {
|
||||
|
||||
override fun setup(application: Application) {
|
||||
@@ -34,35 +26,13 @@ class GplayAutoReporting @Inject constructor(
|
||||
|
||||
if (!isEnabled) return
|
||||
|
||||
ReLinker
|
||||
.log { message -> log(TAG) { "ReLinker: $message" } }
|
||||
.loadLibrary(application, "bugsnag-plugin-android-anr")
|
||||
// Currently no 3rd party bug tracking
|
||||
|
||||
try {
|
||||
val bugsnagConfig = Configuration.load(context).apply {
|
||||
if (debugSettings.isAutoReportingEnabled.value) {
|
||||
Logging.install(bugsnagLogger.get())
|
||||
setUser(installId.id, null, null)
|
||||
autoTrackSessions = true
|
||||
addOnError(bugsnagErrorHandler.get())
|
||||
addMetadata("App", "buildFlavor", BuildConfigWrap.FLAVOR)
|
||||
log(TAG) { "Bugsnag setup done!" }
|
||||
} else {
|
||||
autoTrackSessions = false
|
||||
addOnError(nopBugsnagErrorHandler.get())
|
||||
log(TAG) { "Installing Bugsnag NOP error handler due to user opt-out!" }
|
||||
}
|
||||
}
|
||||
|
||||
Bugsnag.start(context, bugsnagConfig)
|
||||
Bugs.reporter = this
|
||||
} catch (e: IllegalStateException) {
|
||||
log(TAG) { "Bugsnag API Key not configured." }
|
||||
}
|
||||
Bugs.reporter = this
|
||||
}
|
||||
|
||||
override fun notify(throwable: Throwable) {
|
||||
Bugsnag.notify(throwable)
|
||||
log(TAG, WARN) { "notify($throwable)" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import com.bugsnag.android.Event
|
||||
import com.bugsnag.android.OnErrorCallback
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NOPBugsnagErrorHandler @Inject constructor() : OnErrorCallback {
|
||||
|
||||
override fun onError(event: Event): Boolean {
|
||||
log(WARN) { "Error, but skipping bugsnag due to user opt-out: ${event.originalError?.asLog()}" }
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import java.time.Instant
|
||||
|
||||
data class BluetoothDevice2(
|
||||
internal val internal: BluetoothDevice,
|
||||
val seenFirstAt: Instant,
|
||||
) {
|
||||
val address: String
|
||||
get() = internal.address
|
||||
|
||||
val name: String?
|
||||
get() = internal.name
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import android.bluetooth.*
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothHeadset
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.le.BluetoothLeScanner
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -12,14 +16,24 @@ 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.Logging.Priority.ERROR
|
||||
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 eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -74,7 +88,7 @@ class BluetoothManager2 @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
|
||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile)" }
|
||||
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
|
||||
}
|
||||
|
||||
@@ -128,6 +142,7 @@ class BluetoothManager2 @Inject constructor(
|
||||
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))
|
||||
@@ -145,23 +160,54 @@ class BluetoothManager2 @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val seenDevicesLock = Mutex()
|
||||
private val seenDevicesCache = mutableMapOf<String, Instant>()
|
||||
|
||||
fun connectedDevices(
|
||||
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
|
||||
): Flow<List<BluetoothDevice>> = isBluetoothEnabled
|
||||
): Flow<List<BluetoothDevice2>> = isBluetoothEnabled
|
||||
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
|
||||
.map { devices ->
|
||||
devices.filter { device ->
|
||||
featureFilter.any { feature ->
|
||||
device.hasFeature(feature)
|
||||
}
|
||||
val currentAddresses = devices.map { it.address }
|
||||
|
||||
seenDevicesLock.withLock {
|
||||
val cleanedCache = seenDevicesCache.filterKeys { currentAddresses.contains(it) }
|
||||
seenDevicesCache.clear()
|
||||
seenDevicesCache.putAll(cleanedCache)
|
||||
}
|
||||
|
||||
devices
|
||||
.filter { device -> featureFilter.any { feature -> device.hasFeature(feature) } }
|
||||
.map { device ->
|
||||
BluetoothDevice2(
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: Instant.now().also {
|
||||
seenDevicesCache[device.address] = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun bondedDevices(): Flow<Set<BluetoothDevice>> = flow {
|
||||
emit(adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable"))
|
||||
fun bondedDevices(): Flow<Set<BluetoothDevice2>> = flow {
|
||||
val rawDevices = adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
||||
val wrappedDevices = rawDevices.map { device ->
|
||||
|
||||
BluetoothDevice2(
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: Instant.now().also {
|
||||
seenDevicesCache[device.address] = it
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}.toSet()
|
||||
emit(wrappedDevices)
|
||||
}
|
||||
|
||||
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||
suspend fun nudgeConnection(device: BluetoothDevice2): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||
try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
|
||||
@@ -169,7 +215,7 @@ class BluetoothManager2 @Inject constructor(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
|
||||
connectMethod.invoke(bluetoothProfile.proxy, device)
|
||||
connectMethod.invoke(bluetoothProfile.proxy, device.internal)
|
||||
|
||||
log(TAG) { "Nudged connection to $device" }
|
||||
true
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
||||
class MaterialSwitchPreference(context: Context, attrs: AttributeSet?) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
init {
|
||||
// Use material switch
|
||||
widgetLayoutResource = eu.darken.capod.common.R.layout.preference_material_switch
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package eu.darken.capod.common.uix
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -15,6 +16,8 @@ abstract class Activity2 : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
log(tag, VERBOSE) { "onCreate(savedInstanceState=$savedInstanceState)" }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows( window, false )
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -40,6 +42,17 @@ abstract class Fragment2(@LayoutRes val layoutRes: Int?) : Fragment(layoutRes ?:
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
log(tag, VERBOSE) { "onViewCreated(view=$view, savedInstanceState=$savedInstanceState)" }
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
|
||||
val systemWindowInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(
|
||||
v.paddingLeft,
|
||||
systemWindowInsets.top,
|
||||
v.paddingRight,
|
||||
0
|
||||
)
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.darken.capod.common.error.ErrorEventSource
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.navigation.NavEventSource
|
||||
import eu.darken.capod.common.navigation.navVia
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -31,4 +32,7 @@ abstract class ViewModel3(
|
||||
.setupCommonEventHandlers(TAG) { "launchInViewModel()" }
|
||||
.launchIn(vmScope)
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navVia(navEvents)
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ class GeneralSettings @Inject constructor(
|
||||
val isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false)
|
||||
val useIndirectScanResultCallback = preferences.createFlowPreference("core.compat.indirectcallback.enabled", false)
|
||||
|
||||
val isOnboardingDone = preferences.createFlowPreference("core.onboarding.done", false)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||
monitorMode,
|
||||
scannerMode,
|
||||
|
||||
@@ -10,7 +10,7 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -91,6 +91,10 @@ interface PodDevice {
|
||||
"AirPods Pro 2",
|
||||
R.drawable.devic_airpods_pro2_both
|
||||
),
|
||||
@Json(name = "airpods.pro2.usbc") AIRPODS_PRO2_USBC(
|
||||
"AirPods Pro 2 USB-C",
|
||||
R.drawable.devic_airpods_pro2_both
|
||||
),
|
||||
@Json(name = "airpods.max") AIRPODS_MAX(
|
||||
"AirPods Max",
|
||||
R.drawable.devic_headphones_generic
|
||||
@@ -110,6 +114,9 @@ interface PodDevice {
|
||||
@Json(name = "beats.powerbeats.3") POWERBEATS_3(
|
||||
"Power Beats 3"
|
||||
),
|
||||
@Json(name = "beats.powerbeats.4") POWERBEATS_4(
|
||||
"Power Beats 4"
|
||||
),
|
||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||
"Power Beats Pro"
|
||||
),
|
||||
|
||||
@@ -19,6 +19,7 @@ abstract class AppleFactoryModule {
|
||||
|
||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
|
||||
@@ -26,6 +27,7 @@ abstract class AppleFactoryModule {
|
||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeats4(factory: PowerBeats4.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
data class AirPodsPro2Usbc(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
) : DualApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2_USBC
|
||||
|
||||
@get:DrawableRes
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.devic_airpods_pro2_both
|
||||
|
||||
@get:DrawableRes
|
||||
override val caseIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_case
|
||||
|
||||
@get:DrawableRes
|
||||
override val leftPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_left
|
||||
|
||||
@get:DrawableRes
|
||||
override val rightPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsPro2Usbc(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x2420.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro2", "USBC")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
data class PowerBeats4(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_4
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = PowerBeats4(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0D20.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "4")
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,11 @@ class ReactionSettings @Inject constructor(
|
||||
false
|
||||
)
|
||||
|
||||
val showPopUpOnConnection = preferences.createFlowPreference(
|
||||
"reaction.popup.connected",
|
||||
false
|
||||
)
|
||||
|
||||
val onePodMode = preferences.createFlowPreference(
|
||||
"reaction.onepod.enabled",
|
||||
false
|
||||
@@ -58,6 +63,7 @@ class ReactionSettings @Inject constructor(
|
||||
autoConnect,
|
||||
autoConnectCondition,
|
||||
showPopUpOnCaseOpen,
|
||||
showPopUpOnConnection,
|
||||
onePodMode,
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
package eu.darken.capod.reaction.core.popup
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
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 eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.reaction.core.ReactionSettings
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -19,11 +30,13 @@ import javax.inject.Singleton
|
||||
class PopUpReaction @Inject constructor(
|
||||
private val podMonitor: PodMonitor,
|
||||
private val reactionSettings: ReactionSettings,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
) {
|
||||
|
||||
private val coolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||
private val caseCoolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||
|
||||
fun monitor(): Flow<Event> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||
private fun monitorCase(): Flow<Event> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
podMonitor.mainDevice.distinctUntilChangedBy { it?.rawDataHex }
|
||||
@@ -32,7 +45,7 @@ class PopUpReaction @Inject constructor(
|
||||
}
|
||||
}
|
||||
.withPrevious()
|
||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||
.setupCommonEventHandlers(TAG) { "popUpCase" }
|
||||
.mapNotNull { (previous, current) ->
|
||||
if (previous !is DualApplePods? || current !is DualApplePods) {
|
||||
return@mapNotNull null
|
||||
@@ -54,35 +67,37 @@ class PopUpReaction @Inject constructor(
|
||||
}
|
||||
log(TAG) { "Case lid status changed for monitored device." }
|
||||
|
||||
tryPopWindow(current)
|
||||
throttleCasePopUps(current)
|
||||
}
|
||||
|
||||
private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
|
||||
private fun throttleCasePopUps(current: DualApplePods): Event? = when {
|
||||
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
|
||||
val now = Instant.now()
|
||||
val lastShown = coolDowns[current.identifier] ?: Instant.MIN
|
||||
val lastShown = caseCoolDowns[current.identifier] ?: Instant.MIN
|
||||
val sinceLastPop = Duration.between(lastShown, now)
|
||||
log(TAG) { "Time since last popup: $sinceLastPop" }
|
||||
log(TAG) { "Time since last case popup: $sinceLastPop" }
|
||||
|
||||
if (sinceLastPop >= Duration.ofSeconds(10)) {
|
||||
coolDowns[current.identifier] = Instant.now()
|
||||
caseCoolDowns[current.identifier] = Instant.now()
|
||||
Event.PopupShow(device = current)
|
||||
} else {
|
||||
log(TAG, INFO) { "Popup is still on cooldown: $sinceLastPop" }
|
||||
log(TAG, INFO) { "Case popup is still on cooldown: $sinceLastPop" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||
when (current.caseLidState) {
|
||||
DualApplePods.LidState.CLOSED -> {
|
||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||
coolDowns.remove(current.identifier)
|
||||
caseCoolDowns.remove(current.identifier)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||
coolDowns[current.identifier] = Instant.now()
|
||||
caseCoolDowns[current.identifier] = Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +105,78 @@ class PopUpReaction @Inject constructor(
|
||||
|
||||
Event.PopupHide()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val connectionCoolDowns = mutableMapOf<String, Instant>()
|
||||
|
||||
private fun monitorConnection(): Flow<Event> = reactionSettings.showPopUpOnConnection.flow
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (!isEnabled) return@flatMapLatest emptyFlow()
|
||||
|
||||
combine(
|
||||
generalSettings.mainDeviceAddress.flow,
|
||||
bluetoothManager.connectedDevices().distinctUntilChanged(),
|
||||
podMonitor.mainDevice.distinctUntilChangedBy { it?.rawDataHex },
|
||||
) { targetAddress, devices, broadcast ->
|
||||
log(TAG) { "$targetAddress $broadcast $devices " }
|
||||
val direct = devices.singleOrNull { it.address == targetAddress }.also {
|
||||
log(TAG, VERBOSE) { "Connected main device is $it" }
|
||||
}
|
||||
if (direct == null) {
|
||||
connectionCoolDowns.remove(targetAddress).also {
|
||||
if (it != null) log(TAG) { "Cleared connection cooldown for $targetAddress due to disconect" }
|
||||
}
|
||||
}
|
||||
if (direct != null && broadcast != null) direct to broadcast else null
|
||||
}
|
||||
}
|
||||
.withPrevious()
|
||||
.mapNotNull { (previouss, currents) ->
|
||||
val previousConnected = previouss?.first
|
||||
log(TAG, VERBOSE) { "previousConnected: $previousConnected" }
|
||||
val previousBroadcasted = previouss?.second
|
||||
log(TAG, VERBOSE) { "previousBroadcasted: $previousBroadcasted" }
|
||||
val currentConnected = currents?.first
|
||||
log(TAG, VERBOSE) { "currentConnected: $currentConnected" }
|
||||
val currentBroadcasted = currents?.second
|
||||
log(TAG, VERBOSE) { "currentBroadcasted: $currentBroadcasted" }
|
||||
|
||||
if (previousConnected != null && previousBroadcasted != null && currentConnected == null) {
|
||||
return@mapNotNull Event.PopupHide()
|
||||
}
|
||||
|
||||
if (currentConnected == null || currentBroadcasted == null) {
|
||||
// We need an active connection
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val ageOfBroadcastedDevice = Duration.between(Instant.now(), currentBroadcasted.seenFirstAt)
|
||||
val ageOfConnectedDevice = Duration.between(Instant.now(), currentConnected.seenFirstAt)
|
||||
if (ageOfBroadcastedDevice > (ageOfConnectedDevice + Duration.ofSeconds(30))) {
|
||||
// This is likely a false positive, some random nearby device
|
||||
// We expect the first broadcasts to not be much older than the first connection
|
||||
log(TAG, VERBOSE) { "Current broadcasted main device is probably a false-positive" }
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
val lastShown = connectionCoolDowns[currentConnected.address]
|
||||
val sinceLastPop = lastShown?.let { Duration.between(it, now) }
|
||||
log(TAG) { "Time since last connection popup: ${sinceLastPop?.seconds}s" }
|
||||
|
||||
if (lastShown == null) {
|
||||
connectionCoolDowns[currentConnected.address] = Instant.now()
|
||||
Event.PopupShow(device = currentBroadcasted)
|
||||
} else {
|
||||
log(TAG) { "Connection popup is still on cooldown: $sinceLastPop" }
|
||||
null
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "popUpConnection" }
|
||||
|
||||
fun monitor(): Flow<Event> = merge(monitorCase(), monitorConnection())
|
||||
|
||||
sealed class Event {
|
||||
data class PopupShow(
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/switchWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
@@ -58,4 +58,6 @@
|
||||
<string name="pods_case_unknown_state">حالة غير معروفة</string>
|
||||
<string name="last_seen_x">آخر ظهور: %s</string>
|
||||
<string name="first_seen_x">أول ظهور:%s</string>
|
||||
<string name="permission_post_notifications_label"></string>
|
||||
<string name="permission_post_notifications_description">"اسمح لـلتطبيق بإظهار الإشعارات المتعلقة بسماعات الأذن الخاصة بك، على سبيل المثال. حالتهم الحالية خلال الاتصال."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Невядомы стан</string>
|
||||
<string name="last_seen_x">Апошні раз быў: %s</string>
|
||||
<string name="first_seen_x">Упершыню быў: %s</string>
|
||||
<string name="permission_post_notifications_label">Адлюстраванне апавяшчэнняў</string>
|
||||
<string name="permission_post_notifications_description">"Дазволіць CAPod адлюстроўваць вам апавяшчэнні аб вашых AirPods: напрыклад, іх бягучы стан пры падключэнні."</string>
|
||||
<string name="permission_post_notifications_label">Паказваць апавяшчэнні</string>
|
||||
<string name="permission_post_notifications_description">"Дазволіць CAPod адлюстроўваць апавяшчэнні аб вашых AirPods: напрыклад, іх бягучы стан пры падключэнні."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Estat desconegut</string>
|
||||
<string name="last_seen_x">Últim cop vist: %s</string>
|
||||
<string name="first_seen_x">Primera vegada vist: %s</string>
|
||||
<string name="permission_post_notifications_label">Publica notificacions</string>
|
||||
<string name="permission_post_notifications_description">"Permet que el CAPod us mostri notificacions sobre els vostres AirPods, p. ex. el seu estat actual mentre estan connectats."</string>
|
||||
<string name="permission_post_notifications_label">Mostra notificacions</string>
|
||||
<string name="permission_post_notifications_description">"Permet que el CAPod mostri notificacions sobre els vostres AirPods, p. ex. el seu estat actual mentre estan connectats."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Neznámý stav</string>
|
||||
<string name="last_seen_x">Naposledy spatřeno: %s</string>
|
||||
<string name="first_seen_x">Poprvé spatřeno: %s</string>
|
||||
<string name="permission_post_notifications_label">Posílat zprávy</string>
|
||||
<string name="permission_post_notifications_label">Zobrazit oznámení</string>
|
||||
<string name="permission_post_notifications_description">"Povolte aplikaci CAPod zobrazovat oznámení o sluchátkách AirPods, např. o jejich aktuálním stavu během připojení."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Unbekannter Zustand</string>
|
||||
<string name="last_seen_x">Zuletzt gesehen: %s</string>
|
||||
<string name="first_seen_x">Zum ersten Mal gesehen: %s</string>
|
||||
<string name="permission_post_notifications_label">Benachrichtigungen posten</string>
|
||||
<string name="permission_post_notifications_description">"Erlauben Sie CAPod, Ihnen Benachrichtigungen über Ihre AirPods anzuzeigen, z.b. ihren aktuellen Status, während sie verbunden sind."</string>
|
||||
<string name="permission_post_notifications_label">Zeige Benachrichtigungen</string>
|
||||
<string name="permission_post_notifications_description">"Erlaube CAPod, Benachrichtigungen über deine AirPods anzuzeigen, z. B. deren aktuellen Status, während die Verbindung besteht."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
||||
<string name="general_error_label">Σφάλμα</string>
|
||||
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
||||
@@ -56,4 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Άγνωστη κατάσταση</string>
|
||||
<string name="last_seen_x">Τελευταία εμφάνιση: %s</string>
|
||||
<string name="first_seen_x">Πρώτη εμφάνιση: %s</string>
|
||||
<string name="permission_post_notifications_label">Εμφάνιση ειδοποιήσεων</string>
|
||||
<string name="permission_post_notifications_description">"Επιτρέψτε στην εφαρμογή να εμφανίζει ειδοποιήσεις σχετικά με τα AirPods σας, π.χ. την τρέχουσα κατάστασή τους ενώ είστε συνδεδεμένοι."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_grant_permission_action">Conceder permiso</string>
|
||||
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
|
||||
<string name="permission_bluetooth_connect_description">Esta aplicacion requiere el permiso \"Conexión Bluetooth\" para interactuar con los dispositivos vinculados e iniciar las conexiones.</string>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Estado desconocido</string>
|
||||
<string name="last_seen_x">Visto por última vez: %s</string>
|
||||
<string name="first_seen_x">Visto por primera vez: %s</string>
|
||||
<string name="permission_post_notifications_label">Publicar las notificaciones</string>
|
||||
<string name="permission_post_notifications_description">"Permite que CAPod te muestre notificaciones sobre tus AirPods, por ejemplo, su estado actual mientras están conectados."</string>
|
||||
<string name="permission_post_notifications_label">Mostrar las notificaciones</string>
|
||||
<string name="permission_post_notifications_description">"Permite que CAPod muestre las notificaciones sobre tus AirPods, por ejemplo, su estado actual mientras están conectados."</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<string name="general_value_not_available_label">Pole saadaval</string>
|
||||
<string name="pods_dual_left_label">Vasak klapp</string>
|
||||
<string name="pods_dual_right_label">Parem klapp</string>
|
||||
<string name="permission_post_notifications_label">Näita teateid</string>
|
||||
<string name="permission_post_notifications_description">"Luba CAPodil näidata AirPodsi teateid, nt praegust olekut ühendatud olles."</string>
|
||||
</resources>
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
|
||||
<string name="permission_access_fine_location_description">CAPod utilise l’autorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli n’utilisera PAS les données Bluetooth pour déterminer votre position.</string>
|
||||
<string name="permission_background_location_label">Accès à la position en arrière-plan</string>
|
||||
<string name="permission_background_location_description">CAPod utilise « l’accès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une notification » et « Connexion automatique » alors que l’appli est fermée. L’accès à la position en arrière-plan permet à l’appli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
|
||||
<string name="permission_background_location_description">CAPod utilise « l’accès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une fenêtre surgissante » et « Connexion automatique » alors que l’appli est fermée. L’accès à la position en arrière-plan permet à l’appli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Désactiver les optimisations de la pile/batterie</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Les optimisations de la pile/batterie empêchent l’appli de recevoir les données Bluetooth de manière fiable alors que l’appli est en arrière-plan.</string>
|
||||
<string name="permission_required_title">L’autorisation suivante est nécessaire :</string>
|
||||
<string name="permission_system_alert_window_label">Fenêtre d’alerte système</string>
|
||||
<string name="permission_system_alert_window_description">Permettre à CAPod d’apparaître par-dessus les autres applis pour rendre la fonction « Afficher une notification » possible.</string>
|
||||
<string name="permission_system_alert_window_description">Permettre à CAPod d’apparaître par-dessus les autres applis pour rendre la fonction « Afficher une fenêtre surgissante » possible.</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Faible puissance</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Équilibré</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Faible latence</string>
|
||||
|
||||
@@ -58,6 +58,4 @@
|
||||
<string name="pods_case_unknown_state">अज्ञात अवस्था</string>
|
||||
<string name="last_seen_x">अंतिम बार देखे गए: %s</string>
|
||||
<string name="first_seen_x">पहली बार देखा गया: %s</string>
|
||||
<string name="permission_post_notifications_label">सूचनाएं जारी करें</string>
|
||||
<string name="permission_post_notifications_description">"CAPod को अपने AirPods के बारे में सूचनाएं दिखाने की अनुमति दें, जैसे कि जुड़े रहने के दौरान उनकी वर्तमान स्थिति।"</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Status tidak diketahui</string>
|
||||
<string name="last_seen_x">Terakhir terlihat: %s</string>
|
||||
<string name="first_seen_x">Pertama kali terlihat: %s</string>
|
||||
<string name="permission_post_notifications_label">Posting pemberitahuan</string>
|
||||
<string name="permission_post_notifications_description">"Izinkan CAPod menampilkan pemberitahuan tentang AirPods Anda, mis. status saat terhubung."</string>
|
||||
<string name="permission_post_notifications_label">Tampilkan notifikasi</string>
|
||||
<string name="permission_post_notifications_description">"Izinkan CAPod untuk menampilkan notifikasi tentang AirPods anda, contoh. status saat terhubung."</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,26 +21,26 @@
|
||||
<string name="permission_background_location_label">Accesso in background alla tua posizione</string>
|
||||
<string name="permission_background_location_description">CAPod usa l\'accesso in background alla tua posizione per far funzionare opzioni come \"Mostra popup\" e \"AutoConnect\" mentre l\'applicazione é chiusa. L\'accesso in background abiliterà l\'applicazione nella ricezione di dati Bluetooth a basso consume mentre l\'app non é aperta. Questa applicazione non utilizza i dati Bluetooth per geolocalizzarti.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Disabilita ottimizzazione batteria</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">L\'ottimizzazione della batteria impedisce a questa applicazione di ricevere dati Bluetooth mentre l\'app é chiusa.</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">L\'ottimizzazione della batteria impedisce a questa applicazione di ricevere dati Bluetooth mentre l\'app è in esecuzione in background.</string>
|
||||
<string name="permission_required_title">È richiesta la seguente autorizzazione:</string>
|
||||
<string name="permission_system_alert_window_label">Finestra di avviso di sistema</string>
|
||||
<string name="permission_system_alert_window_description">Permetti a CAPod di far apprarire overlay sopra le altre applicazioni per far funzionare \"Mostra PopUp\".</string>
|
||||
<string name="permission_system_alert_window_description">Concedi a CAPod la visualizzazione in primo piano sopra le altre applicazioni per la funzione \"Mostra PopUp\".</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Basso consumo</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Bilanciato</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Bassa latenza</string>
|
||||
<string name="settings_monitor_mode_manual_label">Quando l\'applicazione è aperta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Quando il device è connesso</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Quando le cuffie sono connesse</string>
|
||||
<string name="settings_monitor_mode_always_label">Sempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Quando rilevato</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">La custodia é aperta</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Nell\'orecchio</string>
|
||||
<string name="pods_dual_left_label">Pod sinistra</string>
|
||||
<string name="pods_dual_right_label">Pod destra</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Indossato</string>
|
||||
<string name="pods_dual_left_label">Pod SX</string>
|
||||
<string name="pods_dual_right_label">Pod DX</string>
|
||||
<string name="pods_case_label">Custodia</string>
|
||||
<string name="pods_case_status_open_label">Aperto</string>
|
||||
<string name="pods_case_status_closed_label">Chiuso</string>
|
||||
<string name="pods_connection_state_disconnected_label">Non connesso</string>
|
||||
<string name="pods_connection_state_idle_label">Connesso ad un device, ma inattivo</string>
|
||||
<string name="pods_connection_state_idle_label">Connesso al dispositivo, ma inattivo</string>
|
||||
<string name="pods_connection_state_music_label">In modalità musica</string>
|
||||
<string name="pods_connection_state_call_label">In modalità chiamata</string>
|
||||
<string name="pods_connection_state_ringing_label">Squilla</string>
|
||||
@@ -48,14 +48,16 @@
|
||||
<string name="pods_connection_state_unknown_label">Stato di connessione sconosciuto</string>
|
||||
<string name="pods_unknown_raw_data_label">Dati non elaborati</string>
|
||||
<string name="pods_unknown_label">Dispositivo sconosciuto</string>
|
||||
<string name="pods_unknown_contact_dev">Questo é un dispositivo sconosciutom ma sta utilizzando un formato di comunicazione noto. Aggiungiamo il supporto per questo device, Contattami! :)</string>
|
||||
<string name="pods_unknown_contact_dev">Questo è un dispositivo sconosciuto, ma sta utilizzando un formato di comunicazione noto. Aggiungiamo il supporto per questo dispositivo, contattami :)</string>
|
||||
<string name="pods_none_label_short">Nessun dispositivo</string>
|
||||
<string name="pods_charging_label">In ricarica</string>
|
||||
<string name="pods_inear_label">Nell\'orecchio</string>
|
||||
<string name="pods_inear_label">Indossato</string>
|
||||
<string name="pods_microphone_label">Microfono</string>
|
||||
<string name="pods_yours">Tuoi</string>
|
||||
<string name="headset_being_worn_label">Indossato</string>
|
||||
<string name="pods_case_unknown_state">Stato sconosciuto</string>
|
||||
<string name="last_seen_x">Ultimo accesso: %s</string>
|
||||
<string name="first_seen_x">Primo accesso: %s</string>
|
||||
<string name="permission_post_notifications_label">Mostra notifiche</string>
|
||||
<string name="permission_post_notifications_description">"Concedi a CAPod di mostrare notifiche riguardanti le tue AirPods, ad esempio il loro stato di carica mentre sono connesse."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,4 @@
|
||||
<string name="pods_case_unknown_state">מצב לא ידוע</string>
|
||||
<string name="last_seen_x">נראה לאחרונה: %s</string>
|
||||
<string name="first_seen_x">נראה לראשונה: %s</string>
|
||||
<string name="permission_post_notifications_label">הודעות פרסום</string>
|
||||
<string name="permission_post_notifications_description">"לאשר ל-CAPod להראות לך התראות על ה-AirPods שלך, למשל. המצב הנוכחי שלהם בזמן שהם מחוברים."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">T/A</string>
|
||||
<string name="general_error_label">Ralat</string>
|
||||
<string name="general_grant_permission_action">Berikan kebenaran</string>
|
||||
@@ -55,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Keadaan tidak diketahui</string>
|
||||
<string name="last_seen_x">Terakhir dilihat: %s</string>
|
||||
<string name="first_seen_x">Pertama dilihat: %s</string>
|
||||
<string name="permission_post_notifications_label">Siarkan pemberitahuan</string>
|
||||
<string name="permission_post_notifications_description">"Benarkan CAPod menunjukkan pemberitahuan kepada anda tentang AirPods anda, mis. status semasa mereka semasa disambungkan."</string>
|
||||
<string name="permission_post_notifications_label">Tunjukkan pemberitahuanm</string>
|
||||
<string name="permission_post_notifications_description">"Benarkan CAPod memaparkan pemberitahuan tentang AirPods anda, misalnya, status semasa mereka ketika disambungkan."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Onbekende staat</string>
|
||||
<string name="last_seen_x">Laatst gezien: %s</string>
|
||||
<string name="first_seen_x">Eerst gezien: %s</string>
|
||||
<string name="permission_post_notifications_label">Post notificaties</string>
|
||||
<string name="permission_post_notifications_description">"Sta CAPod toe notificaties over je AirPods te tonen, b.v. de huidige verbindingsstatus."</string>
|
||||
<string name="permission_post_notifications_label">Toon meldingen</string>
|
||||
<string name="permission_post_notifications_description">"Sta CAPod toe om meldingen over je AirPods weer te geven, bijv. hun huidige status terwijl ze verbonden zijn."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Nieznany stan</string>
|
||||
<string name="last_seen_x">Ostatnio widziane: %s</string>
|
||||
<string name="first_seen_x">Widziane pierwszy raz: %s</string>
|
||||
<string name="permission_post_notifications_label">Wysyłanie powiadomień</string>
|
||||
<string name="permission_post_notifications_label">Wyświetl powiadomienia</string>
|
||||
<string name="permission_post_notifications_description">"Zezwól CAPod na wyświetlanie powiadomień o słuchawkach AirPods, np. ich aktualnym statusie podczas połączenia."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Estado desconhecido</string>
|
||||
<string name="last_seen_x">Visto pela última vez: %s</string>
|
||||
<string name="first_seen_x">Visto pela primeira vez: %s</string>
|
||||
<string name="permission_post_notifications_label">Post de notificações</string>
|
||||
<string name="permission_post_notifications_label">Mostrar notificações</string>
|
||||
<string name="permission_post_notifications_description">"Permita que o CAPod mostre notificações sobre seus AirPods, por exemplo. seu status atual enquanto conectado."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">N/A</string>
|
||||
<string name="general_error_label">Eroare</string>
|
||||
<string name="general_grant_permission_action">Acordați permisiuni</string>
|
||||
<string name="overview_nomaindevice_label">Niciun dispozitiv principal</string>
|
||||
<string name="overview_nomaindevice_description">Este puțin probabil ca toate dispozitivele detectate să fie ale tale. Porniți și conectați dispozitivul sau ajustați setările.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth este dezactivat</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth este dezactivat, activați-l ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Conectare Bluetooth</string>
|
||||
<string name="permission_bluetooth_connect_description">Această aplicație necesită permisiunea „Conectare Bluetooth” pentru a interacționa cu dispozitivele asociate și a iniția conexiuni.</string>
|
||||
<string name="permission_bluetooth_scan_label">Scanare Bluetooth</string>
|
||||
<string name="permission_bluetooth_scan_description">Permisiunea „Scanare Bluetooth” permite acestei aplicații să descopere și să primească date Bluetooth de la dispozitivele din apropiere, cum ar fi AirPod-urile dvs.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Această aplicație necesită permisiunea „Bluetooth” pentru a se conecta la dispozitive Bluetooth asociate.</string>
|
||||
<string name="permission_access_fine_location_label">Permiteți locație exactă</string>
|
||||
<string name="permission_access_fine_location_description">CAPod folosește permisiunea „locație exactă” pentru a primi date Bluetooth Energie Scăzută (BLE). Căștile dvs. folosesc tehnologia Bluetooth Energie Scăzută (BLE) pentru a-și transmite starea. Această aplicație NU va folosi date Bluetooth pentru a vă determina locația.</string>
|
||||
<string name="permission_background_location_label">Permiteți acces la locație în fundal</string>
|
||||
<string name="permission_background_location_description">CAPod folosește „accesul la locație în fundal” pentru a activa funcții precum „Afișare pop-up” și „Conectare Automată” în timp ce aplicația este închisă. Accesul la locație în fundal permite aplicației să primească date Bluetooth Energie Scăzută (BLE) în timp ce se află în fundal. Această aplicație NU va folosi date Bluetooth pentru a vă determina locația.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Dezactivați optimizări ale bateriei</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Optimizările bateriei împiedică această aplicație să primească în mod fiabil date Bluetooth în timp ce aplicația este în fundal.</string>
|
||||
<string name="permission_required_title">Este necesară următoarea permisiune:</string>
|
||||
<string name="permission_system_alert_window_label">Fereastra de alertă de sistem</string>
|
||||
<string name="permission_system_alert_window_description">Permiteți CAPod să apară peste alte aplicații pentru a face posibilă funcția „Afișați pop-up”.</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Putere redusă</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Echilibrat</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Latență scăzută</string>
|
||||
<string name="settings_monitor_mode_manual_label">Când aplicația este deschisă</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Când dispozitivul este conectat</string>
|
||||
<string name="settings_monitor_mode_always_label">Mereu</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Când este detectat</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Carcasa este deschisă</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">În ureche</string>
|
||||
<string name="pods_dual_left_label">Casca stângă</string>
|
||||
<string name="pods_dual_right_label">Casca dreaptă</string>
|
||||
<string name="pods_case_label">Carcasă</string>
|
||||
<string name="pods_case_status_open_label">Deschisă</string>
|
||||
<string name="pods_case_status_closed_label">Închisă</string>
|
||||
<string name="pods_connection_state_disconnected_label">Nu sunt conectate la un dispozitiv</string>
|
||||
<string name="pods_connection_state_idle_label">Conectate la un dispozitiv, dar inactiv</string>
|
||||
<string name="pods_connection_state_music_label">În modul muzică</string>
|
||||
<string name="pods_connection_state_call_label">În modul apel</string>
|
||||
<string name="pods_connection_state_ringing_label">Sună</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Închidere</string>
|
||||
<string name="pods_connection_state_unknown_label">Starea conexiunii necunoscută</string>
|
||||
<string name="pods_unknown_raw_data_label">Date neprelucrate</string>
|
||||
<string name="pods_unknown_label">Dispozitiv necunoscut</string>
|
||||
<string name="pods_unknown_contact_dev">Acesta este un dispozitiv necunoscut, dar folosește un format de mesaj similar. Să adăugăm suport pentru el, contactează-mă :)</string>
|
||||
<string name="pods_none_label_short">Niciun dispozitiv</string>
|
||||
<string name="pods_charging_label">Se încarcă</string>
|
||||
<string name="pods_inear_label">În ureche</string>
|
||||
<string name="pods_microphone_label">Microfon</string>
|
||||
<string name="pods_yours">Ale tale</string>
|
||||
<string name="headset_being_worn_label">Fiind purtate</string>
|
||||
<string name="pods_case_unknown_state">Stare necunoscută</string>
|
||||
<string name="last_seen_x">Vazute ultima dată: %s</string>
|
||||
<string name="first_seen_x">Prima dată văzute: %s</string>
|
||||
<string name="permission_post_notifications_label">Arată notificări</string>
|
||||
<string name="permission_post_notifications_description">"Permiteți ca CAPod să afișeze notificări despre AirPod-urile dvs., de exemplu, starea lor actuală în timp ce sunt conectate."</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">Неизвестное состояние</string>
|
||||
<string name="last_seen_x">Последние: %s</string>
|
||||
<string name="first_seen_x">Первые: %s</string>
|
||||
<string name="permission_post_notifications_label">Уведомления о публикации</string>
|
||||
<string name="permission_post_notifications_description">"Разрешить CAPod показывать уведомления о ваших AirPods, например текущий статус при подключении."</string>
|
||||
<string name="permission_post_notifications_label">Показывать уведомления</string>
|
||||
<string name="permission_post_notifications_description">"Разрешить CAPod показывать уведомления о Ваших AirPods, например текущий статус при подключении."</string>
|
||||
</resources>
|
||||
|
||||
@@ -59,5 +59,5 @@
|
||||
<string name="last_seen_x">Naposledy videné: %s</string>
|
||||
<string name="first_seen_x">Prvýkrát videné: %s</string>
|
||||
<string name="permission_post_notifications_label">Zobraziť oznámenia</string>
|
||||
<string name="permission_post_notifications_description">"Povoľte CAPod, aby vám zobrazoval upozornenia týkajúce sa vašich slúchadiel AirPods, napr. ich aktuálny stav počas pripojenia."</string>
|
||||
<string name="permission_post_notifications_description">"Povoľte CAPod, aby zobrazoval upozornenia týkajúce sa vašich slúchadiel AirPods, napr. ich aktuálny stav počas pripojenia."</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,33 +3,33 @@
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">Yok</string>
|
||||
<string name="general_value_not_available_label">Kapalı</string>
|
||||
<string name="general_error_label">Hata</string>
|
||||
<string name="general_grant_permission_action">İzin ver</string>
|
||||
<string name="overview_nomaindevice_label">Birincil cihaz yok</string>
|
||||
<string name="overview_nomaindevice_description">Tespit edilen tüm cihazların sizin olması muhtemel değildir. Cihazınızı açın ve bağlayın veya ayarları yapın.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth devre dışı</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth devre dışı, lütfen etkinleştirin ;)</string>
|
||||
<string name="overview_nomaindevice_label">Birincil aygıt yok</string>
|
||||
<string name="overview_nomaindevice_description">Tespit edilen tüm aygıtların sizin olması muhtemel değildir. Aygıtınızı açın ve bağlayın veya ayarları yapın.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth kapalı</string>
|
||||
<string name="overview_bluetooth_disabled_description">Lütfen etkinleştirin ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Bluetooth bağlantısı</string>
|
||||
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş cihazlarla etkileşime geçmek ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
|
||||
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş aygıtlarla etkileşime geçmek ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
|
||||
<string name="permission_bluetooth_scan_label">Bluetooth taraması</string>
|
||||
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPod\'larınız gibi yakındaki cihazlardan Bluetooth verilerini keşfetmesine ve almasına olanak tanır.</string>
|
||||
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPod\'larınız gibi yakınınızdaki aygıtlardan Bluetooth verilerini keşfetmesine ve almasına olanak tanır.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth cihazlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
|
||||
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth aygıtlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
|
||||
<string name="permission_access_fine_location_label">Tam konuma erişim</string>
|
||||
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"tam konum\" iznini kullanır. Kulaklığınız, durumlarını yayınlamak için Bluetooth Low Energy teknolojisini kullanır. Bu uygulama, konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
|
||||
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"tam konum\" iznini kullanır. Kulaklığınız, durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama, konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
|
||||
<string name="permission_background_location_label">Arka planda konum erişimi</string>
|
||||
<string name="permission_background_location_description">CAPod, uygulama kapalıyken \"Açılır pencereyi göster\" ve \"Otomatik Bağlantı\" gibi özellikleri etkinleştirmek için \"arka planda konum erişimi\"ni kullanır. Arka planda konum erişimi, uygulamanın arka plandayken Bluetooth Düşük Enerji verilerini almasına olanak tanır. Bu uygulama, konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Pil optimizasyonlarını devre dışı bırak</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Pil optimizasyonunu devre dışı bırak</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Pil optimizasyonları, uygulamanın arka planda çalışırken güvenilir bir şekilde Bluetooth verilerini almasını engeller.</string>
|
||||
<string name="permission_required_title">Aşağıdaki izin gereklidir:</string>
|
||||
<string name="permission_system_alert_window_label">Sistem Uyarı Penceresi</string>
|
||||
<string name="permission_system_alert_window_description">\"Açılır Pencere Göster\" özelliğine izin vermek için CAPod\'un diğer uygulamaların üzerinde göstermesine izin vermelisiniz.</string>
|
||||
<string name="permission_system_alert_window_description">\"Açılır pencereyi göster\" özelliğine izin vermek için lütfen CAPod uygulamasının diğer uygulamaların üzerinde gösterilmesine izin verin.</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Düşük güç</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Dengeli</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Düşük gecikme süresi</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Düşük gecikme</string>
|
||||
<string name="settings_monitor_mode_manual_label">Uygulama açıkken</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cihaz bağlandığında</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Aygıt bağlandığında</string>
|
||||
<string name="settings_monitor_mode_always_label">Her zaman</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Görüldüğünde</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Şarj kutusu açık</string>
|
||||
@@ -39,25 +39,25 @@
|
||||
<string name="pods_case_label">Şarj kutusu</string>
|
||||
<string name="pods_case_status_open_label">Açık</string>
|
||||
<string name="pods_case_status_closed_label">Kapalı</string>
|
||||
<string name="pods_connection_state_disconnected_label">Bir cihaza bağlı değil</string>
|
||||
<string name="pods_connection_state_idle_label">Bir cihaza bağlı, ancak boşta</string>
|
||||
<string name="pods_connection_state_disconnected_label">Bir aygıta bağlı değil</string>
|
||||
<string name="pods_connection_state_idle_label">Bir aygıta bağlı, ancak boşta</string>
|
||||
<string name="pods_connection_state_music_label">Müzik modunda</string>
|
||||
<string name="pods_connection_state_call_label">Arama modunda</string>
|
||||
<string name="pods_connection_state_ringing_label">Çalıyor</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Kapatılıyor</string>
|
||||
<string name="pods_connection_state_unknown_label">Bilinmeyen bağlantı durumu</string>
|
||||
<string name="pods_unknown_raw_data_label">Ham veri</string>
|
||||
<string name="pods_unknown_label">Tanımlanmamış cihaz</string>
|
||||
<string name="pods_unknown_contact_dev">Bu bilinmeyen bir cihazdır, ancak benzer bir mesaj formatı kullanıyor. Bunun için destek ekleyelim, bana ulaşın :)</string>
|
||||
<string name="pods_none_label_short">Cihaz yok</string>
|
||||
<string name="pods_charging_label">Doluyor</string>
|
||||
<string name="pods_unknown_label">Bilinmeyen aygıt</string>
|
||||
<string name="pods_unknown_contact_dev">Bu bilinmeyen bir aygıt ama benzer mesaj formatı kullanıyor. Hadi buna destek ekleyelim, bana ulaşın :)</string>
|
||||
<string name="pods_none_label_short">Aygıt yok</string>
|
||||
<string name="pods_charging_label">Şarj oluyor</string>
|
||||
<string name="pods_inear_label">Kulakta</string>
|
||||
<string name="pods_microphone_label">Mikrofon</string>
|
||||
<string name="pods_yours">Sizin</string>
|
||||
<string name="headset_being_worn_label">Giyilmek</string>
|
||||
<string name="pods_yours">Sizinki</string>
|
||||
<string name="headset_being_worn_label">Takılıyken</string>
|
||||
<string name="pods_case_unknown_state">Bilinmeyen durum</string>
|
||||
<string name="last_seen_x">Son görülme: %s</string>
|
||||
<string name="first_seen_x">İlk görülme: %s</string>
|
||||
<string name="permission_post_notifications_label">Gönderi bildirimleri</string>
|
||||
<string name="permission_post_notifications_description">"CAPod'un size AirPod'larınız hakkında bildirimler göstermesine izin verin, ör. bağlıyken geçerli durumları."</string>
|
||||
<string name="permission_post_notifications_label">Bildirimleri göster</string>
|
||||
<string name="permission_post_notifications_description">"CAPod'un AirPods'larınız hakkında bildirimler göstermesine izin vermelisiniz, ör. cihaza bağlıyken mevcut durumları."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="general_value_not_available_label">Н/д</string>
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">Н/Д</string>
|
||||
<string name="general_error_label">Помилка</string>
|
||||
<string name="general_grant_permission_action">Надати дозвіл</string>
|
||||
<string name="overview_nomaindevice_label">Відсутній первинний пристрій</string>
|
||||
<string name="overview_nomaindevice_description">Всі знайдені пристрої навряд чи ваші. Увімкніть та підʼєднайте ваш пристрій або налаштуйте параметри.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth вимкнено</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth вимкнено, увімкніть його ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Bluetooth зʼєднання</string>
|
||||
<string name="permission_bluetooth_connect_description">Цей додаток потребує дозвіл \"Bluetooth зʼєднання\" щоб взаємодіяти зі сполученими пристроями та ініціювати нові зʼєднання.</string>
|
||||
<string name="permission_bluetooth_scan_label">Сканування Bluetooth</string>
|
||||
<string name="permission_bluetooth_scan_description">Дозвіл \"Сканування Bluetooth\" дозволяє цьому додатку виявляти та отримувати Bluetooth дані з пристроїв поблизу, таких як ваші AirPods.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Цей додаток потребує дозвіл \"Bluetooth\" для зʼєднання зі сполученими Bluetooth пристроями.</string>
|
||||
<string name="permission_access_fine_location_label">Доступ до точного місцезнаходження</string>
|
||||
<string name="permission_access_fine_location_description">CAPod використовує дозвіл \"Точне місцезнаходження\" щоб отримувати дані Bluetooth Low Energy. Ваші навушники використовують технологію Bluetooth Low Energy technology щоб транслювати свій статус. Цей додаток НЕ використовує Bluetooth дані щоб визначати ваше місцеперебування.</string>
|
||||
<string name="permission_background_location_label">Доступ до місцезнаходження у тлі</string>
|
||||
<string name="permission_background_location_description">CAPod використовує фоновий доступ до геоданих для роботи таких функцій як \"Показувати спливаюче вікно\" та \"Автопідʼєднання\" коли додаток закритий. Фоновий доступ до геоданих дозволяє додатку отримувати дані Bluetooth Low Energy коли він працює у фоновому режимі. Цей додаток НЕ використовує Bluetooth дані щоб визначати ваше місцеперебування.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Вимкнути оптимізацію батареї</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Оптимізація батареї перешкоджає цьому пристрою надійно отримувати дані Bluetooth під час перебування програми у тлі.</string>
|
||||
<string name="permission_required_title">Необхідні наступні дозволи:</string>
|
||||
<string name="permission_system_alert_window_label">Вікно системного повідомлення</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Низький заряд</string>
|
||||
<string name="permission_system_alert_window_description">Надайте дозвіл CAPod на показ поверх інших додатків для роботи функції \"Показувати спливаюче вікно\".</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Економія енергії</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Збалансовано</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Низька затримка</string>
|
||||
<string name="settings_monitor_mode_manual_label">Коли програму відкрито</string>
|
||||
<string name="settings_monitor_mode_automatic_label">При під\'єднаному пристрої</string>
|
||||
<string name="settings_monitor_mode_always_label">Завжди</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">При виявленні</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Футляр відчинено</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">У вусі</string>
|
||||
<string name="pods_dual_left_label">Лівий навушник</string>
|
||||
@@ -26,9 +40,13 @@
|
||||
<string name="pods_case_status_open_label">Відкрито</string>
|
||||
<string name="pods_case_status_closed_label">Закрито</string>
|
||||
<string name="pods_connection_state_disconnected_label">Не під\'єднано до пристрою</string>
|
||||
<string name="pods_connection_state_idle_label">Підʼєднано до пристрою, в режимі очікування</string>
|
||||
<string name="pods_connection_state_music_label">У режимі музики</string>
|
||||
<string name="pods_connection_state_call_label">У режимі виклику</string>
|
||||
<string name="pods_connection_state_ringing_label">Дзвінок</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Кінець дзвінка</string>
|
||||
<string name="pods_connection_state_unknown_label">Невідомий стан з\'єднання</string>
|
||||
<string name="pods_unknown_raw_data_label">Сирі дані</string>
|
||||
<string name="pods_unknown_label">Невідомий пристрій</string>
|
||||
<string name="pods_unknown_contact_dev">Це невідомий пристрій, проте він використовує подібний формат повідомлень. Додаймо його підтримку, надішліть мені лист :)</string>
|
||||
<string name="pods_none_label_short">Немає пристрою</string>
|
||||
@@ -36,7 +54,10 @@
|
||||
<string name="pods_inear_label">У вусі</string>
|
||||
<string name="pods_microphone_label">Мікрофон</string>
|
||||
<string name="pods_yours">Ваш</string>
|
||||
<string name="headset_being_worn_label">Одягнені</string>
|
||||
<string name="pods_case_unknown_state">Невідомий стан</string>
|
||||
<string name="last_seen_x">Востаннє був: %s</string>
|
||||
<string name="first_seen_x">Уперше був: %s</string>
|
||||
<string name="permission_post_notifications_label">Показувати сповіщення</string>
|
||||
<string name="permission_post_notifications_description">"Дозволити CAPod показ сповіщень щодо ваших AirPods, наприклад, їх поточний стан при підключенні."</string>
|
||||
</resources>
|
||||
|
||||
@@ -53,4 +53,6 @@
|
||||
<string name="pods_case_unknown_state">Trạng thái không xác định</string>
|
||||
<string name="last_seen_x">Nhìn thấy lần cuối: %s</string>
|
||||
<string name="first_seen_x">Nhìn thấy lần đầu: %s</string>
|
||||
<string name="permission_post_notifications_label">Hiển thị thông báo</string>
|
||||
<string name="permission_post_notifications_description">"Cho phép CAPod hiển thị thông báo về AirPods của bạn, ví dụ: tình trạng hiện tại của họ trong khi kết nối."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">不可用</string>
|
||||
<string name="general_error_label">错误</string>
|
||||
<string name="general_grant_permission_action">授予权限</string>
|
||||
@@ -55,4 +58,6 @@
|
||||
<string name="pods_case_unknown_state">未知状态</string>
|
||||
<string name="last_seen_x">上次出现:%s</string>
|
||||
<string name="first_seen_x">首次出现:%s</string>
|
||||
<string name="permission_post_notifications_label">显示通知</string>
|
||||
<string name="permission_post_notifications_description">"允许 CAPod 显示有关您 AirPods 的通知,例如其连接时的当前状态"</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">未知狀態</string>
|
||||
<string name="last_seen_x">最後連線:%s</string>
|
||||
<string name="first_seen_x">首次連線:%s</string>
|
||||
<string name="permission_post_notifications_label">張貼通知</string>
|
||||
<string name="permission_post_notifications_description">"允許 CAPod 為您顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||
<string name="permission_post_notifications_label">顯示通知</string>
|
||||
<string name="permission_post_notifications_description">"允許 CAPod 顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,6 @@
|
||||
<string name="pods_case_unknown_state">未知狀態</string>
|
||||
<string name="last_seen_x">最後連線:%s</string>
|
||||
<string name="first_seen_x">首次連線:%s</string>
|
||||
<string name="permission_post_notifications_label">張貼通知</string>
|
||||
<string name="permission_post_notifications_description">"允許 CAPod 為您顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||
<string name="permission_post_notifications_label">顯示通知</string>
|
||||
<string name="permission_post_notifications_description">"允許 CAPod 顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||
</resources>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<string name="pods_microphone_label">Microphone</string>
|
||||
<string name="pods_yours">Yours</string>
|
||||
<string name="headset_being_worn_label">Being worn</string>
|
||||
<string name="headset_not_being_worn_label">Not being worn</string>
|
||||
<string name="pods_case_unknown_state">Unknown state</string>
|
||||
|
||||
<string name="last_seen_x">Last seen: %s</string>
|
||||
|
||||
@@ -3,80 +3,69 @@ package eu.darken.capod.common.flow
|
||||
import eu.darken.capod.common.collections.mutate
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.*
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.coroutine.runTest2
|
||||
import testhelpers.flow.test
|
||||
import java.io.IOException
|
||||
import java.lang.Thread.sleep
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
// Without an init value, there isn't a way to keep using the flow
|
||||
@Test
|
||||
fun `exceptions on initialization are rethrown`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow<String>(
|
||||
fun `exceptions on initialization are rethrown`() = runTest2(expectedError = IOException::class) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow<String>(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { throw IOException() }
|
||||
)
|
||||
runBlocking {
|
||||
withTimeoutOrNull(500) {
|
||||
// This blocking scope gets the init exception as the first caller
|
||||
hotData.flow.firstOrNull()
|
||||
} shouldBe null
|
||||
}
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class)
|
||||
// This blocking scope gets the init exception as the first caller
|
||||
hotData.flow.firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
testScope.apply {
|
||||
runTest2(autoCancel = true) {
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
}
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value updates`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
fun `value updates`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
@@ -88,7 +77,6 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
(1..16).forEach { _ ->
|
||||
thread {
|
||||
(1..200).forEach { _ ->
|
||||
sleep(10)
|
||||
hotData.updateAsync(
|
||||
onUpdate = { this + 1L },
|
||||
onError = { throw it }
|
||||
@@ -97,12 +85,16 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 3201 }
|
||||
testCollector.latestValues shouldBe (1..3201).toList()
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 3201 }
|
||||
testCollector.latestValues shouldBe (1..3201).toList()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
data class TestData(
|
||||
@@ -110,14 +102,13 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `check multi threading value updates with more complex data`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
val valueProvider =
|
||||
mockk<suspend CoroutineScope.() -> Map<String, eu.darken.capod.common.flow.DynamicStateFlowTest.TestData>>()
|
||||
coEvery { valueProvider.invoke(any()) } returns mapOf("data" to eu.darken.capod.common.flow.DynamicStateFlowTest.TestData())
|
||||
fun `check multi threading value updates with more complex data`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Map<String, TestData>>()
|
||||
coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData())
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
@@ -140,20 +131,23 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 4001 }
|
||||
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 4001 }
|
||||
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only emit new values if they actually changed updates`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
fun `only emit new values if they actually changed updates`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { "1" },
|
||||
@@ -167,26 +161,28 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
hotData.updateAsync { "2" }
|
||||
hotData.updateAsync { "1" }
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 3 }
|
||||
testCollector.latestValues shouldBe listOf("1", "2", "1")
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 3 }
|
||||
testCollector.latestValues shouldBe listOf("1", "2", "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple subscribers share the flow`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = this,
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val sub1 = hotData.flow.test(tag = "sub1", scope = this)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", scope = this)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", scope = this)
|
||||
val sub1 = hotData.flow.test(tag = "sub1", scope = testScope)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", scope = testScope)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", scope = testScope)
|
||||
|
||||
hotData.updateAsync { "A" }
|
||||
hotData.updateAsync { "B" }
|
||||
@@ -207,17 +203,19 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `value is persisted between unsubscribes`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = this,
|
||||
parentScope = testScope,
|
||||
coroutineContext = this.coroutineContext,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", scope = this)
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", scope = testScope)
|
||||
testCollector1.silent = false
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
@@ -233,7 +231,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
testCollector1.cancelAndJoin()
|
||||
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", scope = this)
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", scope = testScope)
|
||||
testCollector2.silent = false
|
||||
|
||||
advanceUntilIdle()
|
||||
@@ -246,10 +244,10 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update is actually blocking`() = runBlocking {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
fun `blocking update is actually blocking`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = testScope.coroutineContext,
|
||||
@@ -270,6 +268,8 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
hotData.updateBlocking { this - 3 } shouldBe 0
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { _, i -> i == 3 }
|
||||
testCollector.latestValues shouldBe listOf(2, 3, 0)
|
||||
|
||||
@@ -277,10 +277,10 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update rethrows error`() = runBlocking {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
fun `blocking update rethrows error`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = testScope.coroutineContext,
|
||||
@@ -300,16 +300,17 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
hotData.flow.first() shouldBe 2
|
||||
|
||||
hotData.updateBlocking { 3 } shouldBe 3
|
||||
hotData.flow.first() shouldBe 3
|
||||
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
advanceUntilIdle()
|
||||
|
||||
hotData.flow.first() shouldBe 3
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates error handler`() = runTest2(expectedError = IOException::class) {
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = this,
|
||||
startValueProvider = { 1 },
|
||||
@@ -321,14 +322,15 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
hotData.updateAsync { throw IOException("Surprise") }
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runBlocking {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { 1 },
|
||||
@@ -346,31 +348,29 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
thrownError!!.shouldBeInstanceOf<IOException>()
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean up function is called when parent scope is cancelled`() = runTest {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
|
||||
fun `clean up function is called when parent scope is cancelled`() {
|
||||
var onReleaseValue: String? = null
|
||||
|
||||
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { "Test" },
|
||||
onRelease = {
|
||||
onReleaseValue = it
|
||||
}
|
||||
)
|
||||
runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { "Test" },
|
||||
onRelease = {
|
||||
onReleaseValue = it
|
||||
}
|
||||
)
|
||||
|
||||
testScope.cancel()
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
}
|
||||
|
||||
onReleaseValue shouldBe "Test"
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsPro2UsbcTest : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* https://github.com/d4rken-org/capod/issues/164
|
||||
*/
|
||||
@Test
|
||||
fun `AirPods Pro 2 with USB-C - via #164`() = runTest {
|
||||
create<AirPodsPro2Usbc>("07 19 01 24 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x2420.toUShort()
|
||||
rawStatus shouldBe 0x0B.toUByte()
|
||||
rawPodsBattery shouldBe 0x99.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x11.toUByte()
|
||||
rawDeviceColor shouldBe 0x0.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AirPods Pro 2 with USB-C - via #164 - in case`() = runTest {
|
||||
create<AirPodsPro2Usbc>("07 19 01 24 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,4 +40,33 @@ class BeatsFitProTest : BaseAirPodsTest() {
|
||||
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extra rl test case`() = runTest {
|
||||
create<BeatsFitPro>("07 19 01 12 20 04 FA 92 54 11 24 CE B1 DF 9D 8D F5 E3 37 60 B1 23 8B 90 3B 63 3F") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1220.toUShort()
|
||||
rawStatus shouldBe 0x04.toUByte()
|
||||
rawPodsBattery shouldBe 0xFA.toUByte()
|
||||
rawFlags shouldBe 0x9.toUShort()
|
||||
rawCaseBattery shouldBe 0x2.toUShort()
|
||||
rawCaseLidState shouldBe 0x54.toUByte()
|
||||
rawDeviceColor shouldBe 0x11.toUByte()
|
||||
rawSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.2f
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats4Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `playing music`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 05 07 C5 E1 9C BD 9A 05 0E AE 9E 56 53 2F F9 75 4A") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x05.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extra test case`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 02 00 04 80 90 60 77 32 C2 0C 75 7A 3D 7E D7 1C 0E B8 43") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x02.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `disconnected state`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 00 25 DF 66 40 AF 44 7B 77 95 8F D1 92 50 26 11 74") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package testhelpers.coroutine
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class CoroutinesTestExtension(
|
||||
private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
|
||||
) : BeforeEachCallback, AfterEachCallback,
|
||||
TestCoroutineScope by createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + dispatcher) {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
cleanupTestCoroutines()
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
@@ -45,12 +45,9 @@ android {
|
||||
create("gplay") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseGplay"]
|
||||
extra["useBugsnag"] = true
|
||||
}
|
||||
}
|
||||
|
||||
setupBugsnagPlugin()
|
||||
|
||||
buildTypes {
|
||||
val customProguardRules = fileTree(File("../proguard")) {
|
||||
include("*.pro")
|
||||
@@ -133,13 +130,12 @@ dependencies {
|
||||
addBaseAndroid()
|
||||
addBaseAndroidUi()
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
|
||||
addNavigation()
|
||||
|
||||
addTesting()
|
||||
|
||||
implementation("androidx.wear:wear:1.2.0")
|
||||
implementation("androidx.wear.tiles:tiles-material:1.1.0")
|
||||
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
package="eu.darken.capod">
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-feature android:name="android.hardware.type.watch" />
|
||||
|
||||
<application
|
||||
@@ -41,4 +42,4 @@
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package eu.darken.capod.wear.core
|
||||
|
||||
import android.content.Context
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
data class UserTime(
|
||||
val time: Instant = Instant.now(),
|
||||
) {
|
||||
fun toFormatted(context: Context): String = DateTimeFormatter
|
||||
.ofLocalizedTime(FormatStyle.SHORT)
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(ZoneId.systemDefault())
|
||||
.format(time)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.wear.core.UserTime
|
||||
import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH
|
||||
import eu.darken.capod.wear.ui.overview.cards.PermissionCardVH
|
||||
@@ -86,14 +87,20 @@ class OverviewFragmentVM @Inject constructor(
|
||||
device = podToShow,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = true,
|
||||
userTime = UserTime(),
|
||||
)
|
||||
|
||||
is SinglePodDevice -> SinglePodsCardVH.Item(
|
||||
now = now,
|
||||
device = podToShow,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = true,
|
||||
userTime = UserTime(),
|
||||
)
|
||||
|
||||
else -> MissingMainDeviceVH.Item(
|
||||
userTime = UserTime(),
|
||||
)
|
||||
else -> MissingMainDeviceVH.Item
|
||||
}
|
||||
items.add(pod)
|
||||
|
||||
|
||||
+6
-2
@@ -5,6 +5,7 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.common.lists.differ.DifferItem
|
||||
import eu.darken.capod.databinding.OverviewNomaindeviceItemBinding
|
||||
import eu.darken.capod.wear.core.UserTime
|
||||
import eu.darken.capod.wear.ui.overview.OverviewAdapter
|
||||
|
||||
class MissingMainDeviceVH(parent: ViewGroup) :
|
||||
@@ -21,11 +22,14 @@ class MissingMainDeviceVH(parent: ViewGroup) :
|
||||
item: Item,
|
||||
payloads: List<Any>
|
||||
) -> Unit = binding(payload = true) { item ->
|
||||
userTime.text = item.userTime.toFormatted(context)
|
||||
|
||||
}
|
||||
|
||||
object Item : OverviewAdapter.Item {
|
||||
override val stableId: Long = Item::class.hashCode().toLong()
|
||||
data class Item(
|
||||
val userTime: UserTime,
|
||||
) : OverviewAdapter.Item {
|
||||
override val stableId: Long = this.hashCode().toLong()
|
||||
|
||||
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
|
||||
get() = { old, new -> if (new::class.isInstance(old)) new else null }
|
||||
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.wear.core.UserTime
|
||||
import java.time.Instant
|
||||
|
||||
class DualPodsCardVH(parent: ViewGroup) :
|
||||
@@ -20,7 +21,10 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
override val viewBinding = lazy { OverviewPodsDualItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
userTime.text = item.userTime.toFormatted(context)
|
||||
|
||||
val device = item.device
|
||||
|
||||
name.apply {
|
||||
val sb = StringBuilder(device.getLabel(context))
|
||||
if (device is HasPodStyle && item.showDebug) {
|
||||
@@ -127,5 +131,6 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
override val device: DualPodDevice,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
val userTime: UserTime,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsSingleItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.wear.core.UserTime
|
||||
import java.time.Instant
|
||||
|
||||
class SinglePodsCardVH(parent: ViewGroup) :
|
||||
@@ -19,6 +20,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
override val viewBinding = lazy { OverviewPodsSingleItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
userTime.text = item.userTime.toFormatted(context)
|
||||
|
||||
val device = item.device
|
||||
|
||||
name.apply {
|
||||
@@ -75,5 +78,6 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
override val device: SinglePodDevice,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
val userTime: UserTime,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M400.08,358.82l-220.33,59a58.52,58.52 0,0 1,-71.68 -41.38l-5,-18.53a171.22,171.22 0,0 1,24.44 -141.76l-23.43,-23.5a25,25 0,0 1,0 -35.35l0.82,-0.82a24.87,24.87 0,0 1,35.24 0l22.27,22.34a170.85,170.85 0,0 1,62 -31.09l0.48,-0.13a172.54,172.54 0,0 1,71.4 -3.89l8.63,-31.85a25.14,25.14 0,0 1,30.7 -17.66l1.13,0.29A24.8,24.8 0,0 1,354.42 125l-9.23,34.07a172.66,172.66 0,0 1,91.7 111l4.58,17.08A58.54,58.54 0,0 1,400.08 358.82Z"
|
||||
android:fillColor="#41eb8e"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M279.56,314.16c-13.7,-8 -31.7,-7.91 -45.66,-0.68L212,351.37c0.68,15.16 9.41,30.14 24.29,38.79a49.92,49.92 0,0 0,14.87 5.76l-20.81,36.44a8.28,8.28 0,0 0,3 11.33l12.8,7.44a8.26,8.26 0,0 0,11.31 -3l41.73,-72.75h0C310.82,354.32 306.77,330 279.56,314.16ZM247.67,347.24a6,6 0,1 1,-2.16 -8.26A6,6 0,0 1,247.67 347.24Z"
|
||||
android:fillColor="#fff"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M300.66,214.18a27.95,27.95 0,1 1,-19.8 34.24A28,28 0,0 1,300.66 214.18Z"
|
||||
android:fillColor="#26845c"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M194.53,242.62a27.95,27.95 0,1 1,-19.8 34.24A28,28 0,0 1,194.53 242.62Z"
|
||||
android:fillColor="#26845c"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M149.79,318.88l-36.3,-21.21a50.22,50.22 0,0 0,10 -12.43c8.56,-14.94 8.61,-32.3 1.59,-45.79l-38,-21.9c-15.72,0.7 -31.35,9.65 -39.22,23.41 -15.64,27.32 -6.94,50.45 13.74,62.85h0l72.64,42.25a8.26,8.26 0,0 0,11.31 -3l7.36,-12.85A8.31,8.31 0,0 0,149.79 318.88ZM100.3,251.53A6.06,6.06 0,1 1,98 243.28,6.07 6.07,0 0,1 100.3,251.53Z"
|
||||
android:fillColor="#fff"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M187.92,204.08a29.52,29.52 0,0 1,-38.6 -15.52A29.15,29.15 0,0 1,165 150.28a30.38,30.38 0,0 1,4.68 -1.52l-16.36,-38.48L148.6,99.07l-4.77,-11.21 40,-16.75 9.54,22.42L176,100.79l27.58,64.87 0.14,0.34a28.56,28.56 0,0 1,1.24 3.65A29.16,29.16 0,0 1,187.92 204.08Z"
|
||||
android:fillColor="#ffda3e"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M259.45,46l-0.77,13.75 -10.63,-0.57L245.84,99l-0.13,2.29h-0.16A16.6,16.6 0,1 1,232.87 82l1.32,-23.6h0l0.39,-6.87 0.38,-6.88Z"
|
||||
android:fillColor="#ffcf3b"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M205.22,169.54l-0.26,0.11a28.56,28.56 0,0 0,-1.24 -3.65Z"
|
||||
android:fillColor="#e7b720"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M95.05,247.69m-7.01,0a7.01,7.01 0,1 1,14.02 0a7.01,7.01 0,1 1,-14.02 0"
|
||||
android:fillColor="#231f20" />
|
||||
<path
|
||||
android:pathData="M242.42,343.4m-7.01,0a7.01,7.01 0,1 1,14.02 0a7.01,7.01 0,1 1,-14.02 0"
|
||||
android:fillColor="#231f20" />
|
||||
</vector>
|
||||
@@ -1,42 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="m256,13c134.21,0 243,108.79 243,243c0,134.21 -108.79,243 -243,243c-134.21,0 -243,-108.79 -243,-243c0,-134.21 108.79,-243 243,-243z"
|
||||
android:fillColor="#3f7aff" />
|
||||
<path
|
||||
android:pathData="m404.3,630.55l-93.18,24.97c-59.46,15.93 -120.58,-19.35 -136.51,-78.82l-18.52,-69.12c-6.81,-25.42 8.28,-51.56 33.7,-58.37l216.44,-58c25.42,-6.81 51.56,8.28 58.37,33.7l18.52,69.12c15.93,59.46 -19.35,120.58 -78.82,136.51zM255.69,692.33l-1.04,-0.28c-12.36,-3.27 -19.67,-15.89 -16.33,-28.19l13.54,-49.96c3.33,-12.3 16.05,-19.62 28.41,-16.35l1.04,0.28c12.36,3.27 19.67,15.89 16.33,28.19l-13.54,49.96c-3.33,12.3 -16.05,19.62 -28.41,16.35zM436.6,634.7l-36.62,-36.73c-9.01,-9.04 -9.03,-23.69 -0.04,-32.71l0.76,-0.76c9,-9.02 23.6,-9.01 32.61,0.04l36.62,36.73c9.01,9.04 9.03,23.69 0.04,32.71l-0.76,0.76c-9,9.02 -23.6,9.01 -32.61,-0.04z"
|
||||
android:fillColor="#41eb8e" />
|
||||
<path
|
||||
android:pathData="m229.33,167.8c88.82,-23.8 180.11,28.91 203.91,117.73l1.04,3.87c10.01,37.34 -12.15,75.73 -49.5,85.73l-186.4,49.95c-37.34,10.01 -75.73,-12.15 -85.73,-49.5l-1.04,-3.87c-23.8,-88.82 28.91,-180.11 117.73,-203.91zM335.69,116.4l1.09,0.29c12.88,3.41 20.51,16.57 17.03,29.39l-14.12,52.08c-3.48,12.82 -16.74,20.45 -29.62,17.04l-1.09,-0.29c-12.88,-3.41 -20.51,-16.57 -17.03,-29.39l14.12,-52.08c3.48,-12.82 16.74,-20.45 29.62,-17.04zM147.08,176.49l38.17,38.29c9.4,9.43 9.41,24.7 0.04,34.1l-0.79,0.79c-9.38,9.41 -24.6,9.39 -34,-0.04l-38.17,-38.29c-9.4,-9.43 -9.41,-24.7 -0.04,-34.1l0.79,-0.79c9.38,-9.41 24.6,-9.39 34,0.04z"
|
||||
android:fillColor="#41eb8e" />
|
||||
<path
|
||||
android:pathData="m171.1,170.45c14.47,-6.06 31.14,0.64 37.24,14.97c6.09,14.33 -0.7,30.87 -15.17,36.93c-14.47,6.06 -31.14,-0.64 -37.24,-14.97c-6.09,-14.33 0.7,-30.87 15.17,-36.93zM155.24,121.04l21.83,-9.15l32.8,77.13l-21.83,9.15l-32.8,-77.13zM159.84,131.85l-9.2,-21.63l38.57,-16.16l9.2,21.63l-38.57,16.16zM233.88,104.24c8.87,0.47 15.66,7.98 15.17,16.76c-0.49,8.79 -8.07,15.53 -16.94,15.06c-8.87,-0.47 -15.66,-7.98 -15.17,-16.76c0.49,-8.79 8.07,-15.53 16.94,-15.06zM238.18,75.21l13.38,0.71l-2.63,47.3l-13.38,-0.71l2.63,-47.3zM237.81,81.84l0.74,-13.26l23.64,1.25l-0.74,13.26l-23.64,-1.25z"
|
||||
android:fillColor="#ffcf3b" />
|
||||
<path
|
||||
android:pathData="m199.55,259.53c14.4,-3.86 29.2,4.67 33.06,19.06c3.85,14.38 -4.7,29.17 -19.1,33.03c-14.4,3.86 -29.2,-4.67 -33.06,-19.06c-3.85,-14.38 4.7,-29.17 19.1,-33.03z"
|
||||
android:strokeAlpha="0.8"
|
||||
android:fillColor="#1f6a50"
|
||||
android:fillAlpha="0.8" />
|
||||
<path
|
||||
android:pathData="m301.94,232.09c14.4,-3.86 29.2,4.67 33.06,19.06c3.85,14.38 -4.7,29.17 -19.1,33.03c-14.4,3.86 -29.2,-4.67 -33.06,-19.06c-3.85,-14.38 4.7,-29.17 19.1,-33.03z"
|
||||
android:strokeAlpha="0.8"
|
||||
android:fillColor="#1f6a50"
|
||||
android:fillAlpha="0.8" />
|
||||
<path
|
||||
android:pathData="m281.58,328.54c26.25,15.27 30.16,38.74 18.87,59.03l0.01,0.01l-40.26,70.19c-2.22,3.83 -7.1,5.13 -10.91,2.92l-12.35,-7.18c-3.81,-2.21 -5.1,-7.11 -2.88,-10.93l20.08,-35.15c-4.89,-1.04 -9.74,-2.87 -14.34,-5.55c-14.36,-8.35 -22.78,-22.8 -23.43,-37.43l21.17,-36.56c13.47,-6.97 30.83,-7.03 44.05,0.66z"
|
||||
android:fillColor="#f9f9f9"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="m248.73,352.49c2.77,1.61 3.7,5.18 2.08,7.96c-1.62,2.79 -5.17,3.75 -7.93,2.14c-2.77,-1.61 -3.7,-5.18 -2.08,-7.96c1.62,-2.79 5.17,-3.75 7.93,-2.14z"
|
||||
android:fillColor="#272727" />
|
||||
<path
|
||||
android:pathData="m131.01,300.65c-2.65,4.63 -5.93,8.64 -9.64,11.98l35.02,20.46c3.84,2.21 5.17,7.1 2.98,10.93l-7.1,12.39c-2.19,3.82 -7.08,5.13 -10.91,2.92l-70.08,-40.76l0.01,-0.01c-19.95,-11.96 -28.34,-34.28 -13.25,-60.64c7.6,-13.27 22.66,-21.9 37.83,-22.58l36.67,21.14c6.77,13.02 6.72,29.75 -1.54,44.17z"
|
||||
android:fillColor="#f9f9f9"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="m106.46,260.17c2.8,1.61 3.78,5.17 2.19,7.95c-1.59,2.78 -5.15,3.72 -7.95,2.11c-2.8,-1.61 -3.78,-5.17 -2.19,-7.95c1.59,-2.78 5.15,-3.72 7.95,-2.11z"
|
||||
android:fillColor="#272727" />
|
||||
</vector>
|
||||
@@ -1,10 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:bottom="40dp"
|
||||
android:drawable="@drawable/splash_graphic"
|
||||
android:left="40dp"
|
||||
android:right="40dp"
|
||||
android:top="40dp" />
|
||||
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:drawable="@mipmap/ic_launcher"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/bluetooth_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/bluetooth_description"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="32dp"
|
||||
android:id="@+id/user_time"
|
||||
style="@style/UserTimeStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="12:23" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/overview_nomaindevice_label"
|
||||
app:layout_constraintBottom_toTopOf="@id/permission_description"
|
||||
app:layout_constraintBottom_toTopOf="@id/body"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_description"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:id="@+id/body"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/overview_nomaindevice_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_label" />
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/request_description"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_description"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
@@ -44,11 +44,12 @@
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/grant_action"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/general_grant_permission_action"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||
|
||||
@@ -7,14 +7,23 @@
|
||||
android:layout_margin="8dp"
|
||||
tools:context=".wear.ui.MainActivity">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/user_time"
|
||||
style="@style/UserTimeStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="12:23" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/name"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginBottom="2dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/devic_airpods_gen1_both" />
|
||||
|
||||
@@ -36,15 +45,15 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:gravity="center"
|
||||
app:lineHeight="14sp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
android:lines="2"
|
||||
android:maxLines="2"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
app:lineHeight="14sp"
|
||||
tools:text="Music Active, Case Closed" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
@@ -60,11 +69,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.2">
|
||||
app:layout_constraintTop_toTopOf="@+id/pod_right_container">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
@@ -190,8 +197,7 @@
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.2">
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
@@ -267,7 +273,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/last_seen"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
style="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
|
||||
@@ -7,10 +7,21 @@
|
||||
android:layout_margin="8dp"
|
||||
tools:context=".wear.ui.MainActivity">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/user_time"
|
||||
style="@style/UserTimeStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="12:23" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@+id/name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -15,4 +15,10 @@
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
</style>
|
||||
|
||||
<style name="UserTimeStyle" parent="TextAppearance.Material3.LabelMedium">
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:ellipsize">marquee</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -29,9 +29,10 @@
|
||||
<item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
|
||||
</style>
|
||||
|
||||
<style name="AppThemeSplash" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">#3f7aff</item>
|
||||
<style name="AppThemeSplash" parent="Theme.SplashScreen.IconBackground">
|
||||
<item name="windowSplashScreenBackground">#000000</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_screen</item>
|
||||
<item name="windowSplashScreenIconBackgroundColor">#000000</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
|
||||
+10
-9
@@ -38,16 +38,18 @@ android {
|
||||
create("foss") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseFoss"]
|
||||
// The info block is encrypted and can only be read by google
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
create("gplay") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseGplay"]
|
||||
extra["useBugsnag"] = true
|
||||
}
|
||||
}
|
||||
|
||||
setupBugsnagPlugin()
|
||||
|
||||
buildTypes {
|
||||
val customProguardRules = fileTree(File(projectDir, "proguard")) {
|
||||
include("*.pro")
|
||||
@@ -128,7 +130,7 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":app-common"))
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
|
||||
addBaseKotlin()
|
||||
|
||||
@@ -141,14 +143,13 @@ dependencies {
|
||||
addBaseAndroid()
|
||||
addBaseAndroidUi()
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
|
||||
addNavigation()
|
||||
addBaseWorkManager()
|
||||
|
||||
addTesting()
|
||||
|
||||
"gplayImplementation"("com.android.billingclient:billing:5.1.0")
|
||||
"gplayImplementation"("com.android.billingclient:billing-ktx:5.1.0")
|
||||
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
"gplayImplementation"("com.android.billingclient:billing:6.0.1")
|
||||
"gplayImplementation"("com.android.billingclient:billing-ktx:6.0.1")
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donează</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Am donat deja</string>
|
||||
<string name="foss_upgrade_no_money_label">Mi-am cheltuit toți banii pe AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Bağış</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ben zaten bağışladım</string>
|
||||
<string name="foss_upgrade_no_money_label">Tüm paramı AirPod\'lara harcıyorum</string>
|
||||
<string name="foss_upgrade_donate_label">Bağış yap</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Zaten bağış yaptım</string>
|
||||
<string name="foss_upgrade_no_money_label">Tüm paramı AirPods\'a harcıyorum</string>
|
||||
</resources>
|
||||
|
||||
+16
-6
@@ -1,7 +1,13 @@
|
||||
package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
import android.app.Activity
|
||||
import com.android.billingclient.api.*
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -20,11 +26,15 @@ data class BillingClientConnection(
|
||||
) {
|
||||
private val purchasesLocal = MutableStateFlow<Collection<Purchase>>(emptySet())
|
||||
val purchases: Flow<Collection<Purchase>> = combine(purchasesGlobal, purchasesLocal) { global, local ->
|
||||
val combined = mutableMapOf<String, Purchase>()
|
||||
global.plus(local).toSet().sortedByDescending { it.purchaseTime }.forEach { purchase ->
|
||||
combined[purchase.orderId] = purchase
|
||||
}
|
||||
combined.values
|
||||
val combined = mutableSetOf<Purchase>()
|
||||
|
||||
combined.addAll(local)
|
||||
|
||||
global
|
||||
.let { purchases -> purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED } }
|
||||
.let { combined.addAll(it) }
|
||||
|
||||
combined.sortedByDescending { it.purchaseTime }
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "purchases" }
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Serviciile Google Play nu sunt disponibile.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nu s-au găsit achiziții. Folosești contul corect?</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
@@ -24,9 +27,9 @@
|
||||
<activity
|
||||
android:name=".main.ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppThemeSplash"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppThemeSplash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -74,7 +77,13 @@
|
||||
android:name=".common.debug.recording.ui.RecorderActivity"
|
||||
android:theme="@style/AppThemeFloating" />
|
||||
|
||||
<service android:name=".common.debug.recording.core.RecorderService" />
|
||||
<service
|
||||
android:name=".common.debug.recording.core.RecorderService"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Users can record a debug log to help developers with bugs. The debug log recording is controlled via notifications and hosted in a foreground service." />
|
||||
</service>
|
||||
|
||||
<!-- Worker stuff-->
|
||||
<service
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.preference.Preference
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
|
||||
|
||||
class IntentPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
|
||||
@StyleRes defStyleRes: Int = 0,
|
||||
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
override fun setIntent(_intent: Intent?) {
|
||||
super.setIntent(_intent)
|
||||
_intent?.let {
|
||||
intent
|
||||
setOnPreferenceClickListener {
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
log(TAG, ERROR) { "Failed to launch $intent: $e" }
|
||||
Toast.makeText(context, e.toString(), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("IntentPreference")
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ abstract class PreferenceFragment2
|
||||
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package eu.darken.capod.main.ui.onboarding
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.PrivacyPolicy
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.uix.Fragment3
|
||||
import eu.darken.capod.common.viewbinding.viewBinding
|
||||
import eu.darken.capod.databinding.OnboardingFragmentBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OnboardingFragment : Fragment3(R.layout.onboarding_fragment) {
|
||||
|
||||
override val vm: OnboardingFragmentVM by viewModels()
|
||||
override val ui: OnboardingFragmentBinding by viewBinding()
|
||||
|
||||
@Inject lateinit var webpageTool: WebpageTool
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
ui.goPrivacyPolicy.setOnClickListener { webpageTool.open(PrivacyPolicy.URL) }
|
||||
ui.continueAction.setOnClickListener { vm.finishOnboarding() }
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package eu.darken.capod.main.ui.onboarding
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OnboardingFragmentVM @Inject constructor(
|
||||
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val generalSettings: GeneralSettings,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
fun finishOnboarding() = launch {
|
||||
generalSettings.isOnboardingDone.value = true
|
||||
OnboardingFragmentDirections.actionOnboardingFragmentToOverviewFragment().navigate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Onboarding", "Fragment", "VM")
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.BuildConfig
|
||||
@@ -147,6 +149,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
vm.launchUpgradeFlow.observe2 {
|
||||
it(requireActivity())
|
||||
}
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.flow.combine
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.navigation.navVia
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
@@ -48,6 +47,12 @@ class OverviewFragmentVM @Inject constructor(
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
init {
|
||||
if (!generalSettings.isOnboardingDone.value) {
|
||||
OverviewFragmentDirections.actionOverviewFragmentToOnboardingFragment().navigate()
|
||||
}
|
||||
}
|
||||
|
||||
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
||||
val launchUpgradeFlow = SingleLiveEvent<(Activity) -> Unit>()
|
||||
|
||||
@@ -124,7 +129,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
items.add(0, BluetoothDisabledVH.Item)
|
||||
} else if (mainPod == null) {
|
||||
items.add(0, MissingMainDeviceVH.Item {
|
||||
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navVia(this)
|
||||
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navigate()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,12 +145,14 @@ class OverviewFragmentVM @Inject constructor(
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = isMainPod,
|
||||
)
|
||||
|
||||
is SinglePodDevice -> SinglePodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = isMainPod,
|
||||
)
|
||||
|
||||
else -> UnknownPodDeviceCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
@@ -166,7 +173,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
}
|
||||
|
||||
fun goToSettings() = launch {
|
||||
OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navVia(this@OverviewFragmentVM)
|
||||
OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navigate()
|
||||
}
|
||||
|
||||
fun onUpgrade() = launch {
|
||||
|
||||
@@ -2,7 +2,10 @@ package eu.darken.capod.main.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
@@ -15,6 +18,7 @@ import eu.darken.capod.common.viewbinding.viewBinding
|
||||
import eu.darken.capod.databinding.SettingsFragmentBinding
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsFragment : Fragment2(R.layout.settings_fragment),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
|
||||
+4
-4
@@ -1,19 +1,19 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
|
||||
|
||||
class DeviceSelectionDialogFactory constructor(private val context: Context) {
|
||||
|
||||
fun create(
|
||||
devices: List<BluetoothDevice>,
|
||||
current: BluetoothDevice?,
|
||||
callback: (BluetoothDevice?) -> Unit
|
||||
devices: List<BluetoothDevice2>,
|
||||
current: BluetoothDevice2?,
|
||||
callback: (BluetoothDevice2?) -> Unit
|
||||
): AlertDialog {
|
||||
return MaterialAlertDialogBuilder(context).apply {
|
||||
setTitle(R.string.settings_maindevice_address_label)
|
||||
|
||||
-1
@@ -30,7 +30,6 @@ class GeneralSettingsFragmentVM @Inject constructor(
|
||||
val events = SingleLiveEvent<GeneralSettingsEvents>()
|
||||
|
||||
init {
|
||||
|
||||
generalSettings.monitorMode.flow
|
||||
.withPrevious()
|
||||
.filter { (old, new) ->
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.Keep
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
@@ -27,17 +26,6 @@ class SupportFragment : PreferenceFragment3() {
|
||||
|
||||
@Inject lateinit var clipboardHelper: ClipboardHelper
|
||||
|
||||
private val installIdPref by lazy { findPreference<Preference>("support.installid")!! }
|
||||
|
||||
override fun onPreferencesCreated() {
|
||||
installIdPref.setOnPreferenceClickListener {
|
||||
vm.copyInstallID()
|
||||
true
|
||||
}
|
||||
|
||||
super.onPreferencesCreated()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
vm.clipboardEvent.observe2(this) { installId ->
|
||||
Snackbar.make(requireView(), installId, Snackbar.LENGTH_INDEFINITE)
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.LayoutRes
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
@@ -41,6 +42,7 @@ import kotlinx.coroutines.withTimeout
|
||||
import java.time.Duration
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
@@ -95,6 +97,27 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of cells needed for given size of the widget.
|
||||
*
|
||||
* The value is determined in accordance with official guidelines
|
||||
* for designing widgets, see:
|
||||
* https://developer.android.com/guide/practices/ui_guidelines/widget_design
|
||||
*
|
||||
* Thanks to Jakub S. on Stackoverflow for this solution:
|
||||
* https://stackoverflow.com/a/37522648/10866268
|
||||
*
|
||||
* @param size Widget size in dp.
|
||||
* @return Size in number of cells.
|
||||
*/
|
||||
private fun getCellsForSize(size: Int): Int {
|
||||
var n = 2
|
||||
while (70 * n - 30 < size) {
|
||||
++n
|
||||
}
|
||||
return n - 1
|
||||
}
|
||||
|
||||
private suspend fun updateWidget(
|
||||
context: Context,
|
||||
widgetManager: AppWidgetManager,
|
||||
@@ -107,7 +130,23 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
val layout = when {
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
|
||||
device is DualPodDevice -> createDualPodLayout(context, device)
|
||||
device is DualPodDevice -> {
|
||||
val minWidth = widgetManager.getAppWidgetOptions(widgetId)
|
||||
.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
|
||||
val columns = getCellsForSize(minWidth)
|
||||
|
||||
/* Enable wide widgets only when we are provided with 5 or
|
||||
* more columns of space.
|
||||
* Although the minimum size is only 2x1, the safeguards are
|
||||
* added if these restrictions will ever be loosened in the future.
|
||||
*/
|
||||
val layout = when (columns) {
|
||||
in 1 .. 4 -> R.layout.widget_pod_dual_compact_layout
|
||||
else -> R.layout.widget_pod_dual_wide_layout
|
||||
}
|
||||
|
||||
createDualPodLayout(context, device, layout)
|
||||
}
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device)
|
||||
else -> createNoDeviceLayout(context)
|
||||
@@ -169,8 +208,9 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
private fun createDualPodLayout(
|
||||
context: Context,
|
||||
podDevice: DualPodDevice,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_dual_layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||
@LayoutRes layout: Int
|
||||
): RemoteViews = RemoteViews(context.packageName, layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice), layout=${layout}" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
@@ -9,6 +8,7 @@ import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
@@ -30,8 +30,20 @@ import eu.darken.capod.reaction.core.autoconnect.AutoConnect
|
||||
import eu.darken.capod.reaction.core.playpause.PlayPause
|
||||
import eu.darken.capod.reaction.core.popup.PopUpReaction
|
||||
import eu.darken.capod.reaction.ui.popup.PopUpWindow
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
@HiltWorker
|
||||
@@ -68,8 +80,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
val start = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||
|
||||
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||
|
||||
doDoWork()
|
||||
|
||||
val duration = System.currentTimeMillis() - start
|
||||
@@ -96,6 +106,8 @@ class MonitorWorker @AssistedInject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||
|
||||
val monitorJob = podMonitor.mainDevice
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.distinctUntilChanged()
|
||||
@@ -130,7 +142,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
.flatMapLatest { arguments ->
|
||||
val monitorMode = arguments[0] as MonitorMode
|
||||
val mainAddress = arguments[1] as String?
|
||||
val devices = arguments[2] as Collection<BluetoothDevice>
|
||||
val devices = arguments[2] as Collection<BluetoothDevice2>
|
||||
|
||||
log(TAG) { "Monitor mode: $monitorMode" }
|
||||
when (monitorMode) {
|
||||
@@ -145,7 +157,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
|
||||
}
|
||||
devices.any { it.address == mainAddress } -> {
|
||||
log(TAG) { "MainDevice is connected ($mainAddress), aborting any timeout." }
|
||||
log(TAG) { "Main device is connected ($mainAddress), aborting any timeout." }
|
||||
}
|
||||
else -> {
|
||||
log(TAG) { "No known Pods are connected, canceling worker soon." }
|
||||
|
||||
@@ -19,7 +19,16 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.hasApiLevel
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
@@ -71,9 +80,65 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
|
||||
return builder.apply {
|
||||
|
||||
// Options here should be mutually exclusive, and are prioritized by their order of importance
|
||||
// Some options are omitted here, as they will conflict with other options
|
||||
// TODO: Implement a settings pane to allow user to customize this
|
||||
val stateText = when {
|
||||
// Pods charging state
|
||||
// This goes first as pods should not be worn if it is still charging
|
||||
device is HasChargeDetection && device.isHeadsetBeingCharged -> {
|
||||
context.getString(eu.darken.capod.common.R.string.pods_charging_label)
|
||||
}
|
||||
|
||||
// Pods wear state
|
||||
device is HasEarDetection -> {
|
||||
if (device.isBeingWorn) context.getString(eu.darken.capod.common.R.string.headset_being_worn_label)
|
||||
else context.getString(eu.darken.capod.common.R.string.headset_not_being_worn_label)
|
||||
}
|
||||
|
||||
// Case charge state
|
||||
// This is under pods wear state as we don't want it conflicting with it
|
||||
device is HasCase && device.isCaseCharging -> {
|
||||
context.getString(eu.darken.capod.common.R.string.pods_charging_label)
|
||||
}
|
||||
|
||||
else -> context.getString(eu.darken.capod.common.R.string.pods_case_unknown_state)
|
||||
}
|
||||
|
||||
val batteryText = when (device) {
|
||||
is DualPodDevice -> {
|
||||
val left = device.getBatteryLevelLeftPod(context)
|
||||
val right = device.getBatteryLevelRightPod(context)
|
||||
when {
|
||||
device is HasCase -> {
|
||||
val case = device.getBatteryLevelCase(context)
|
||||
"$left $case $right"
|
||||
}
|
||||
|
||||
else -> "$left $right"
|
||||
}
|
||||
}
|
||||
|
||||
is SinglePodDevice -> {
|
||||
val headset = device.getBatteryLevelHeadset(context)
|
||||
when {
|
||||
device is HasCase -> {
|
||||
val case = device.getBatteryLevelCase(context)
|
||||
"$headset $case"
|
||||
}
|
||||
|
||||
else -> headset
|
||||
}
|
||||
}
|
||||
|
||||
else -> "?"
|
||||
}
|
||||
|
||||
setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||
setCustomContentView(notificationViewFactory.createContentView(device))
|
||||
setCustomBigContentView(notificationViewFactory.createContentView(device))
|
||||
setSmallIcon(device.iconRes)
|
||||
setContentTitle("$batteryText ~ $stateText")
|
||||
setSubText(null)
|
||||
log(TAG, VERBOSE) { "updatingNotification(): $device" }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package eu.darken.capod.reaction.ui
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.Keep
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
@@ -36,7 +36,7 @@ class ReactionSettingsFragment : PreferenceFragment3() {
|
||||
override val preferenceFile: Int = R.xml.preferences_reactions
|
||||
|
||||
private var isPro: Boolean = false
|
||||
private var bondedDevices: List<BluetoothDevice> = emptyList()
|
||||
private var bondedDevices: List<BluetoothDevice2> = emptyList()
|
||||
private val autoConnectConditionPref by lazy { findPreference<ListPreference>(settings.autoConnectCondition.key)!! }
|
||||
|
||||
override fun onPreferencesCreated() {
|
||||
@@ -51,17 +51,17 @@ class ReactionSettingsFragment : PreferenceFragment3() {
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
if (preference.key == reactionSettings.autoPlay.key && !isPro) {
|
||||
preference as CheckBoxPreference
|
||||
preference as TwoStatePreference
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
} else if (preference.key == reactionSettings.autoPause.key && !isPro) {
|
||||
preference as CheckBoxPreference
|
||||
preference as TwoStatePreference
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
} else if (preference.key == reactionSettings.autoConnect.key) {
|
||||
preference as CheckBoxPreference
|
||||
preference as TwoStatePreference
|
||||
|
||||
if (!isPro) {
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
@@ -79,7 +79,12 @@ class ReactionSettingsFragment : PreferenceFragment3() {
|
||||
return true
|
||||
}
|
||||
} else if (preference.key == reactionSettings.showPopUpOnCaseOpen.key && !isPro) {
|
||||
preference as CheckBoxPreference
|
||||
preference as TwoStatePreference
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
} else if (preference.key == reactionSettings.showPopUpOnConnection.key && !isPro) {
|
||||
preference as TwoStatePreference
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2M20 16H5.2L4 17.2V4H20V16Z" />
|
||||
</vector>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_path"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recording_path"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_size"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="32dp"
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recording_size"
|
||||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="32dp"
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_compressed_size"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recording_size_compressed"
|
||||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="@+id/label_compressed_size"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Widget.MaterialComponents.Toolbar.Primary"
|
||||
style="@style/Widget.Material3.Toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -22,6 +22,8 @@
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:paddingBottom="48dp"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/overview_pods_dual_item"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="32dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mascot"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_marginTop="48dp"
|
||||
android:src="@drawable/splash_graphic2"
|
||||
app:layout_constraintEnd_toEndOf="@id/title"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Material3.HeadlineLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/app_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/mascot" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/body1"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/onboarding_body1"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/body2"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/onboarding_body2"
|
||||
app:layout_constraintTop_toBottomOf="@id/body1" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/body3"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/onboarding_body3"
|
||||
app:layout_constraintTop_toBottomOf="@id/body2" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/body4"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/onboarding_body4"
|
||||
app:layout_constraintTop_toBottomOf="@id/body3" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/go_privacy_policy"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="32dp"
|
||||
android:text="@string/settings_privacy_policy_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/body4" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/continue_action"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/general_continue_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/go_privacy_policy" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user