mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de609b4d05 | ||
|
|
7e6e9eee36 | ||
|
|
a321bb733c | ||
|
|
490369a3e1 | ||
|
|
a2cce044ef | ||
|
|
8884bb88c7 | ||
|
|
313b0e9131 | ||
|
|
47048cf7e7 | ||
|
|
ef1c4615aa | ||
|
|
c9587b2bf0 | ||
|
|
669fa2fd22 | ||
|
|
fb8ec72825 | ||
|
|
d1687f574f | ||
|
|
72f47bec1f | ||
|
|
20e8aecf83 | ||
|
|
ee41474670 | ||
|
|
ac29e28eda | ||
|
|
58b96cbdee | ||
|
|
0c1fc15251 | ||
|
|
7b8e0cc952 | ||
|
|
c8aa591feb | ||
|
|
afa4d6e807 | ||
|
|
e6b7eda9e7 | ||
|
|
bbc4e05c8f | ||
|
|
50bccbaa2e | ||
|
|
d71e83a2af | ||
|
|
92a67702b7 | ||
|
|
2f9185efa7 | ||
|
|
a10216520b | ||
|
|
d5354e5156 | ||
|
|
a8f9b1f732 | ||
|
|
08a796e621 | ||
|
|
15f3a68f8e | ||
|
|
e0bc6dafa0 | ||
|
|
430c1236d7 | ||
|
|
3ac357161a | ||
|
|
3c5f1bf21f | ||
|
|
5544cd3770 | ||
|
|
71a060feb9 | ||
|
|
25d643257c | ||
|
|
85900e5bff | ||
|
|
d3257d6c5e | ||
|
|
afe1399f72 | ||
|
|
20ce2d3e22 | ||
|
|
cea8f2290d | ||
|
|
1b5b59c41b | ||
|
|
8df7c19e84 | ||
|
|
88b0069b94 | ||
|
|
3b8ddc9f06 | ||
|
|
708c7f38ec | ||
|
|
139daa1cc2 | ||
|
|
b85ac778c0 | ||
|
|
a3e2c9e61d | ||
|
|
ad78a0570c | ||
|
|
7e6ee274d8 | ||
|
|
daf7411c36 | ||
|
|
d3dedbe6cd | ||
|
|
d5411586d5 | ||
|
|
94e2d40554 | ||
|
|
db80fa1b46 | ||
|
|
9b569b8d07 | ||
|
|
90288880fb | ||
|
|
870e518ff6 | ||
|
|
6d89169bae | ||
|
|
ac77ee2703 |
@@ -7,46 +7,47 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-all:
|
||||
name: Assemble all variants
|
||||
build-modules:
|
||||
name: Build apps
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [ Foss,Gplay ]
|
||||
variant: [ Debug ]
|
||||
module: [ app,app-wear ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Build (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||
run: ./gradlew assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||
- name: Build module
|
||||
run: ./gradlew ${{ matrix.module }}:assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||
|
||||
test-all:
|
||||
name: Run all tests
|
||||
test-modules:
|
||||
name: Unit tests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [ Foss,Gplay ]
|
||||
variant: [ Release ]
|
||||
flavor: [ testFoss,testGplay ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run tests (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||
run: ./gradlew test${{ matrix.flavor }}${{ matrix.variant }}
|
||||
- name: Test modules
|
||||
run: ./gradlew ${{ matrix.flavor }}${{ matrix.variant }}UnitTest
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
|
||||
- name: Assemble beta APK
|
||||
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
|
||||
- name: Set up ruby env
|
||||
uses: ruby/setup-ruby@v1
|
||||
@@ -138,6 +138,15 @@ 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 }}
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
|
||||
- name: Assemble beta and upload to Google Play
|
||||
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||
run: bundle exec fastlane beta
|
||||
@@ -147,6 +156,15 @@ jobs:
|
||||
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 }}
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
|
||||
- name: Assemble production and upload to Google Play
|
||||
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||
run: bundle exec fastlane production
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
*.jks
|
||||
.local/*
|
||||
/fastlane/report.xml
|
||||
/fastlane/Appfile
|
||||
@@ -43,12 +43,13 @@ Currently supported models:
|
||||
|
||||
## Download
|
||||
|
||||
| Source | Status |
|
||||
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Source | Status |
|
||||
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [](https://f-droid.org/en/packages/eu.darken.capod/) |
|
||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||
|
||||
|
||||
## Support the project
|
||||
|
||||
@@ -8,6 +8,7 @@ apply(plugin = "dagger.hilt.android.plugin")
|
||||
|
||||
android {
|
||||
compileSdk = ProjectConfig.compileSdk
|
||||
namespace = "${ProjectConfig.packageName}.common"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = ProjectConfig.minSdk
|
||||
@@ -33,12 +34,12 @@ android {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import android.content.BroadcastReceiver
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
|
||||
fun BroadcastReceiver.PendingResult.finish2(): Boolean = try {
|
||||
finish()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
log { "BroadcastReceiver.PendingResult.finish() failed: $e" }
|
||||
false
|
||||
}
|
||||
@@ -32,9 +32,9 @@ class BleScanner @Inject constructor(
|
||||
@SuppressLint("MissingPermission") fun scan(
|
||||
filters: Set<ScanFilter>,
|
||||
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
||||
disableOffloadFiltering: Boolean = true,
|
||||
disableOffloadBatching: Boolean = true,
|
||||
disableDirectScanCallback: Boolean = true,
|
||||
disableOffloadFiltering: Boolean = false,
|
||||
disableOffloadBatching: Boolean = false,
|
||||
disableDirectScanCallback: Boolean = false,
|
||||
): Flow<Collection<BleScanResult>> = callbackFlow {
|
||||
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@MonitorScope
|
||||
@DefineComponent(parent = SingletonComponent::class)
|
||||
interface MonitorComponent {
|
||||
|
||||
@DefineComponent.Builder
|
||||
interface Builder {
|
||||
|
||||
fun coroutineScope(@BindsInstance coroutineScope: MonitorCoroutineScope): Builder
|
||||
|
||||
fun build(): MonitorComponent
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@InstallIn(MonitorComponent::class)
|
||||
@Module()
|
||||
abstract class ProcessorModule {
|
||||
|
||||
@Binds
|
||||
@MonitorScope
|
||||
abstract fun processorScope(scope: MonitorCoroutineScope): CoroutineScope
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class MonitorScope
|
||||
@@ -14,6 +14,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.replayingShare
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
@@ -113,6 +114,7 @@ class PodMonitor @Inject constructor(
|
||||
disableDirectCallback = useIndirectScanResultCallback,
|
||||
)
|
||||
}
|
||||
.throttleLatest(1000)
|
||||
.flatMapLatest { options ->
|
||||
val filters = when {
|
||||
options.showUnfiltered -> {
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
@@ -22,21 +23,18 @@ data class AirPodsMax(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection, HasAppleColor {
|
||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||
|
||||
val isHeadphonesBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(1)
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
override val isHeadsetBeingCharged: Boolean
|
||||
get() = rawFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isHeadphonesBeingWorn
|
||||
get() = rawStatus.isBitSet(5)
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<string name="general_error_label">Fehler</string>
|
||||
<string name="general_grant_permission_action">Berechtigung erteilen</string>
|
||||
<string name="overview_nomaindevice_label">Kein primäres Gerät</string>
|
||||
<string name="overview_nomaindevice_description">Alle gefundenen Geräte sind unwahrscheinlich ihnen zu gehören. Verbinden sie ihr Gerät oder passen sie die Einstellungen an.</string>
|
||||
<string name="overview_nomaindevice_description">Alle gefundenen Geräte gehören wahrscheinlich nicht dir. Schalte deine Kopfhörer ein oder passe die Einstellungen von CAPod an.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Bluetooth verbinden</string>
|
||||
@@ -58,4 +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">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,36 +1,41 @@
|
||||
<?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">N/D</string>
|
||||
<string name="general_grant_permission_action">Conceder permiso</string>
|
||||
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
|
||||
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean suyos. Encienda y conecte su dispositivo o ajuste la configuración.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth deshabilitado</string>
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth está deshabilitado, habilítelo ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
|
||||
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"conexión Bluetooth\" para interactuar con los dispositivos pareados, e iniciar las conexiones.</string>
|
||||
<string name="permission_bluetooth_scan_label">Buscando Bluetooth</string>
|
||||
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como sus AirPods.</string>
|
||||
<string name="permission_bluetooth_description">Esta aplicacion requiere \"Bluetooth\" para conectarse a los dispositivos pareados.</string>
|
||||
<string name="permission_access_fine_location_label">Acceso a la ubicación precisa</string>
|
||||
<string name="permission_access_fine_location_description">CAPod utiliza el permiso de \"ubicación precisa\" para recibir datos de Bluetooth Low Energy. Sus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir su estado. Esta aplicación NO utilizará datos de Bluetooth para determinar su ubicación.</string>
|
||||
<string name="permission_background_location_label">Acceso a la ubicación en segundo plano</string>
|
||||
<string name="permission_background_location_description">CAPod usa el \"Acceso a la ubicación en segundo plano\" para habilitar funciones como \"Mostrar ventana emergente\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la ubicación en segundo plano permite que la aplicación reciba datos de Bluetooth Low Energy mientras está en segundo plano. Esta aplicación NO utilizará datos de Bluetooth para determinar su ubicación.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Desactivar Optimización de Batería</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">La Optimización de Batería evitan que esta aplicación reciba datos de Bluetooth de manera confiable mientras la aplicación está en segundo plano.</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">No disponible</string>
|
||||
<string name="general_error_label">Fallo</string>
|
||||
<string name="general_grant_permission_action">Conceder el permiso</string>
|
||||
<string name="overview_nomaindevice_label">Sin dispositivo principal</string>
|
||||
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean tuyos. Enciende y conecta tu dispositivo o ajusta la configuración.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth desactivado</string>
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Conexión por Bluetooth</string>
|
||||
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"Conexión por Bluetooth\" para interactuar con los dispositivos emparejados e iniciar las conexiones.</string>
|
||||
<string name="permission_bluetooth_scan_label">El Bluetooth está escaneando</string>
|
||||
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo del Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como tus AirPods.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Esta aplicación requiere el permiso \"Bluetooth\" para conectarse a dispositivos bluetooth emparejados.</string>
|
||||
<string name="permission_access_fine_location_label">Acceso preciso a tu ubicación</string>
|
||||
<string name="permission_access_fine_location_description">CAPod utiliza el permiso \"localización precisa\" para recibir datos Bluetooth Low Energy. Tus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir tu estado. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
|
||||
<string name="permission_background_location_label">Ubicación en segundo plano</string>
|
||||
<string name="permission_background_location_description">CAPod utiliza el \"la ubicación en segundo plano\" para activar funciones como \"Mostrar ventana emergente\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la localización en segundo plano permite a la aplicación recibir datos de Bluetooth Low Energy mientras está en segundo plano. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Desactivar las optimizaciones de la batería</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de la batería impiden que esta aplicación reciba datos Bluetooth de forma fiable mientras está en segundo plano.</string>
|
||||
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
|
||||
<string name="permission_system_alert_window_label">Ventana de alerta del sistema</string>
|
||||
<string name="permission_system_alert_window_description">Permita que CAPod dibuje sobre otras aplicaciones para que la función \"Mostrar ventana emergente\" sea posible.</string>
|
||||
<string name="permission_system_alert_window_description">Permite a CAPod dibujar sobre otras aplicaciones para hacer posible la función \"Mostrar una ventana emergente\".</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Balanceado</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</string>
|
||||
<string name="settings_monitor_mode_manual_label">Cuando la aplicación está abierta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
|
||||
<string name="settings_monitor_mode_always_label">Siempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Cuando se ve</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Cuando está visible</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">El estuche está abierto</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
|
||||
<string name="pods_dual_left_label">Audífono izquierdo</string>
|
||||
<string name="pods_dual_right_label">Audífono derecho</string>
|
||||
<string name="pods_dual_left_label">Auricular izquierdo</string>
|
||||
<string name="pods_dual_right_label">Auricular derecho</string>
|
||||
<string name="pods_case_label">Estuche</string>
|
||||
<string name="pods_case_status_open_label">Abierto</string>
|
||||
<string name="pods_case_status_closed_label">Cerrado</string>
|
||||
@@ -40,8 +45,8 @@
|
||||
<string name="pods_connection_state_call_label">En modo llamada</string>
|
||||
<string name="pods_connection_state_ringing_label">Sonando</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Colgar</string>
|
||||
<string name="pods_connection_state_unknown_label">Estado de la conexion desconocido</string>
|
||||
<string name="pods_unknown_raw_data_label">Datos sin procesar</string>
|
||||
<string name="pods_connection_state_unknown_label">Estado de la conexión desconocido</string>
|
||||
<string name="pods_unknown_raw_data_label">Datos no procesados</string>
|
||||
<string name="pods_unknown_label">Dispositivo desconocido</string>
|
||||
<string name="pods_unknown_contact_dev">Este es un dispositivo desconocido, pero está usando un formato de mensaje similar. Agreguemos soporte para ello, contácteme :)</string>
|
||||
<string name="pods_none_label_short">Sin dispositivo</string>
|
||||
@@ -49,7 +54,7 @@
|
||||
<string name="pods_inear_label">En el oído</string>
|
||||
<string name="pods_microphone_label">Micrófono</string>
|
||||
<string name="pods_yours">Tuyos</string>
|
||||
<string name="headset_being_worn_label">En uso</string>
|
||||
<string name="headset_being_worn_label">Utilizándose</string>
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?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">Tasuline CAPod</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">कैपोड</string>
|
||||
<string name="app_name_pro">कैपोड प्रो</string>
|
||||
<string name="app_name_foss">कैपोड फोस</string>
|
||||
<string name="general_value_not_available_label">एन/ए</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">N/A</string>
|
||||
<string name="general_error_label">त्रुटि</string>
|
||||
<string name="general_grant_permission_action">अनुदान अनुमति</string>
|
||||
<string name="overview_nomaindevice_label">कोई प्राथमिक उपकरण नहीं</string>
|
||||
@@ -51,4 +51,11 @@
|
||||
<string name="pods_unknown_contact_dev">यह एक अज्ञात डिवाइस है, लेकिन यह समान संदेश प्रारूप का उपयोग कर रहा है। आइए इसके लिए समर्थन जोड़ें, मुझसे संपर्क करें :)</string>
|
||||
<string name="pods_none_label_short">उपकरण नहीं</string>
|
||||
<string name="pods_charging_label">चार्ज</string>
|
||||
<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>
|
||||
</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 פרו</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>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<string name="settings_monitor_mode_automatic_label">Wanneer het apparaat is aangesloten</string>
|
||||
<string name="settings_monitor_mode_always_label">Altijd</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Wanneer gezien</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Case is geopend</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Zaak is geopend</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">In oor</string>
|
||||
<string name="pods_dual_left_label">Linker pod</string>
|
||||
<string name="pods_dual_right_label">Rechter pod</string>
|
||||
|
||||
@@ -2,6 +2,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">Fóssil CAPod</string>
|
||||
<string name="general_value_not_available_label">N/A</string>
|
||||
<string name="general_error_label">Erro</string>
|
||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
||||
|
||||
@@ -1,2 +1,61 @@
|
||||
<?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">Erro</string>
|
||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||
<string name="overview_nomaindevice_label">Sem dispositivo primário</string>
|
||||
<string name="overview_nomaindevice_description">É improvável que os dispositivos detetados sejam seus. Ligue e conecte o seu dispositivo ou ajuste as definições.</string>
|
||||
<string name="overview_bluetooth_disabled_label">O Bluetooth está desligado</string>
|
||||
<string name="overview_bluetooth_disabled_description">O Bluetooth está desligado, ativa-o ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">Conexão ao Bluetooth</string>
|
||||
<string name="permission_bluetooth_connect_description">Esta aplicação requer permissão para conectar com o Bluetooth para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
||||
<string name="permission_bluetooth_scan_label">Explorar Bluetooth</string>
|
||||
<string name="permission_bluetooth_scan_description">A permissão para explorar o Bluetooth autoriza esta aplicação para descobrir e detetar dados de Bluetooth de dispositivos por perto como os seus AirPods.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Esta aplicação requer a permissão do Bluetooth para conectar e emparelhar a dispositivos bluetooth.</string>
|
||||
<string name="permission_access_fine_location_label">Aceder a localização exata</string>
|
||||
<string name="permission_access_fine_location_description">CAPod utiliza a localização exata para receber dados de \'Bluetooth Low Energy\'. Os seus headphones utilizam tecnologia \'Bluetooth Low Energy\' para transmitir o seu estado. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
|
||||
<string name="permission_background_location_label">Acesso à localização de fundo</string>
|
||||
<string name="permission_background_location_description">CAPod utiliza o acesso à localização de fundo para habilitar funcionalidades como \"Mostrar pop-up\" e \"AutoConectar\" enquanto a aplicação está fechada. Acesso à localização de fundo autoriza à aplicação receber dados de \'Bluetooth Low Energy\' enquanto está de fundo. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Desativar otimizações de bateria</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Otimizações de bateria impedem que esta aplicação receba dados de Bluetooth fidedignos enquanto a aplicação está de fundo.</string>
|
||||
<string name="permission_required_title">A seguinte permissão é necessária:</string>
|
||||
<string name="permission_system_alert_window_label">Janela de Alerta do Sistema</string>
|
||||
<string name="permission_system_alert_window_description">Autorizar CAPod para aparecer por cima de outras aplicações para tornar possível a funcionalidade \"Mostrar pop-up\".</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Poupar bateria</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Baixa latência</string>
|
||||
<string name="settings_monitor_mode_manual_label">Quando a aplicação está aberta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Quando o dispositivo é conectado</string>
|
||||
<string name="settings_monitor_mode_always_label">Sempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Quando visto</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Caixa está aberta</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">No ouvido</string>
|
||||
<string name="pods_dual_left_label">Pod esquerdo</string>
|
||||
<string name="pods_dual_right_label">Pod direito</string>
|
||||
<string name="pods_case_label">Caixa</string>
|
||||
<string name="pods_case_status_open_label">Aberta</string>
|
||||
<string name="pods_case_status_closed_label">Fechada</string>
|
||||
<string name="pods_connection_state_disconnected_label">Não está conectado a um dispositivo</string>
|
||||
<string name="pods_connection_state_idle_label">Conectado a um dispositivo, mas inativo</string>
|
||||
<string name="pods_connection_state_music_label">Em modo de música</string>
|
||||
<string name="pods_connection_state_call_label">Em modo de chamada</string>
|
||||
<string name="pods_connection_state_ringing_label">A tocar</string>
|
||||
<string name="pods_connection_state_hanging_up_label">A terminar a chamada</string>
|
||||
<string name="pods_connection_state_unknown_label">Estado de conexão desconhecido</string>
|
||||
<string name="pods_unknown_raw_data_label">Dados não tratados</string>
|
||||
<string name="pods_unknown_label">Dispositivo desconhecido</string>
|
||||
<string name="pods_unknown_contact_dev">Este é um dispositivo desconhecido, mas usa um formato de mensagem semelhante. Vamos adicionar suporte para isso, entre em contacto comigo :)</string>
|
||||
<string name="pods_none_label_short">Sem dispositivo</string>
|
||||
<string name="pods_charging_label">A carregar</string>
|
||||
<string name="pods_inear_label">No ouvido</string>
|
||||
<string name="pods_microphone_label">Microfone</string>
|
||||
<string name="pods_yours">Seu</string>
|
||||
<string name="headset_being_worn_label">A ser usado</string>
|
||||
<string name="pods_case_unknown_state">Estado desconhecido</string>
|
||||
<string name="last_seen_x">Última vez visto: %s</string>
|
||||
<string name="first_seen_x">Primeira vez visto: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,56 +1,60 @@
|
||||
<?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">Yok</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">Kapalı</string>
|
||||
<string name="general_error_label">Hata</string>
|
||||
<string name="general_grant_permission_action">İzin ver</string>
|
||||
<string name="overview_nomaindevice_label">Birincil aygıt yok</string>
|
||||
<string name="overview_nomaindevice_description">Algılanan tüm cihazların sizin olması olası 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ışı, etkinleştirin ;)</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şim kurmak 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 AirPods\'unuz gibi yakındaki cihazlardan Bluetooth verilerini keşfetmesine ve almasına izin verir.</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_access_fine_location_label">Hassas konuma erişin</string>
|
||||
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"iyi konum\" iznini kullanır. Kulaklıklarınız durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama konumunuzu belirlemek için Bluetooth verilerini KULLANMAYACAKTIR.</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 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 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 plan konumu erişimi\" kullanır. Arka plan konumu erişimi, uygulamanın arka plandayken Bluetooth Düşük Enerji verilerini almasını sağlar. 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_description">Pil iyileştirmeleri, uygulama arka plandayken bu uygulamanın Bluetooth verilerini güvenilir bir şekilde almasını engeller.</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 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">CAPod\'un \"PopUp\'ı Göster\" özelliğini mümkün kılmak için diğer uygulamaların üzerinde çizmesine izin verin.</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">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">Vaka açık</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Şarj kutusu açık</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Kulakta</string>
|
||||
<string name="pods_dual_left_label">Sol bölme</string>
|
||||
<string name="pods_dual_right_label">Sağ bölme</string>
|
||||
<string name="pods_case_label">Sorun</string>
|
||||
<string name="pods_dual_left_label">Sol kulaklık</string>
|
||||
<string name="pods_dual_right_label">Sağ kulaklık</string>
|
||||
<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ıyorum</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">İşlenmemiş 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_raw_data_label">Ham veri</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">Senin</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>
|
||||
|
||||
@@ -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,6 +54,7 @@
|
||||
<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>
|
||||
|
||||
@@ -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">不適用</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">藍牙已停用</string>
|
||||
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
||||
<string name="permission_bluetooth_connect_description">這個應用程式需要「藍牙連線」權限以與已配對的裝置交互或配對新裝置。</string>
|
||||
<string name="permission_bluetooth_scan_label">藍牙掃描</string>
|
||||
<string name="permission_bluetooth_scan_description">「藍牙掃描」權限允許這個應用程式發現附近的裝置並接收藍牙資料,您的 AirPods 需要如此。</string>
|
||||
<string name="permission_bluetooth_label">藍牙</string>
|
||||
<string name="permission_bluetooth_description">這個應用程式需要「藍牙」權限與已配對裝置連線。</string>
|
||||
<string name="permission_access_fine_location_label">存取精確位置</string>
|
||||
<string name="permission_access_fine_location_description">CAPod 使用「精確位置」權限以接收低功耗藍牙資料。您的耳機使用低功耗藍牙技術以廣播其狀態。這個應用程式不會使用藍牙資料來確定您的位置。</string>
|
||||
<string name="permission_background_location_label">背景位置存取</string>
|
||||
<string name="permission_background_location_description">CAPods 在應用程式關閉時使用「背景位置存取」來啟用諸如「顯示彈出式視窗」和「自動連線」等功能。背景位置存取允許這個應用程式在背景接收低功耗藍牙資料。這個應用程式不會使用藍牙資料來確定您的位置。</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">停用電池效能最佳化</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">電池效能最佳化使這個應用程式在背景時無法可靠地接收藍牙資料。</string>
|
||||
<string name="permission_required_title">可能要求下列權限:</string>
|
||||
<string name="permission_system_alert_window_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>
|
||||
<string name="pods_dual_right_label">右耳</string>
|
||||
<string name="pods_case_label">充電盒</string>
|
||||
<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>
|
||||
<string name="pods_charging_label">充電中</string>
|
||||
<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>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<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_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_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線至您的裝置,也可以調整設定。</string>
|
||||
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
|
||||
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
||||
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
||||
@@ -25,7 +25,7 @@
|
||||
<string name="permission_required_title">可能要求下列權限:</string>
|
||||
<string name="permission_system_alert_window_label">系統警報視窗</string>
|
||||
<string name="permission_system_alert_window_description">允許 CAPod 在其他應用程式上繪圖,使「顯示彈出式視窗」功能成為可能。</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">節電</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>
|
||||
@@ -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">"允許 CAPod 顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||
</resources>
|
||||
|
||||
@@ -65,6 +65,6 @@
|
||||
|
||||
<string name="last_seen_x">Last seen: %s</string>
|
||||
<string name="first_seen_x">First seen: %s</string>
|
||||
<string name="permission_post_notifications_label">Post notifications</string>
|
||||
<string name="permission_post_notifications_description">"Allow CAPod to show you notifications about your AirPods, e.g. their current status while connected."</string>
|
||||
<string name="permission_post_notifications_label">Show notifications</string>
|
||||
<string name="permission_post_notifications_description">"Allow CAPod to show notifications about your AirPods, e.g. their current status while connected."</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -21,15 +20,4 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test values based on AirPodMax`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
@@ -27,8 +28,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_MAX
|
||||
}
|
||||
}
|
||||
@@ -50,8 +49,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +69,25 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
rawStatus.isBitSet(5) shouldBe true
|
||||
|
||||
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wear status`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 03 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawStatus shouldBe 0x03.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe false
|
||||
isBeingWorn shouldBe false
|
||||
}
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawStatus shouldBe 0x23.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe true
|
||||
isBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ apply(plugin = "androidx.navigation.safeargs.kotlin")
|
||||
|
||||
android {
|
||||
compileSdk = ProjectConfig.compileSdk
|
||||
namespace = "${ProjectConfig.packageName}"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = ProjectConfig.packageName
|
||||
@@ -16,7 +17,7 @@ android {
|
||||
minSdk = ProjectConfig.minSdk
|
||||
targetSdk = ProjectConfig.targetSdk
|
||||
|
||||
versionCode = ProjectConfig.Version.code + 1 // Wear app
|
||||
versionCode = ProjectConfig.Version.code
|
||||
versionName = ProjectConfig.Version.name
|
||||
|
||||
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
||||
@@ -102,12 +103,12 @@ android {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
@@ -132,6 +133,8 @@ dependencies {
|
||||
addBaseAndroid()
|
||||
addBaseAndroidUi()
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
|
||||
addNavigation()
|
||||
|
||||
addTesting()
|
||||
|
||||
@@ -6,8 +6,6 @@ import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.EntryPoints
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
@@ -15,9 +13,7 @@ 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.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.monitor.core.MonitorComponent
|
||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import kotlinx.coroutines.*
|
||||
@@ -31,22 +27,12 @@ import kotlinx.coroutines.flow.take
|
||||
class MonitorWorker @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted private val params: WorkerParameters,
|
||||
monitorComponentBuilder: MonitorComponent.Builder,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val permissionTool: PermissionTool,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
private val workerScope = MonitorCoroutineScope()
|
||||
private val monitorComponent = monitorComponentBuilder
|
||||
.coroutineScope(workerScope)
|
||||
.build()
|
||||
|
||||
private val entryPoint by lazy {
|
||||
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
|
||||
}
|
||||
|
||||
private var finishedWithError = false
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.darken.capod.wear.core
|
||||
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import eu.darken.capod.monitor.core.MonitorComponent
|
||||
|
||||
@InstallIn(MonitorComponent::class)
|
||||
@EntryPoint
|
||||
interface MonitorWorkerEntryPoint
|
||||
@@ -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)
|
||||
}
|
||||
@@ -13,14 +13,11 @@ import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.monitor.core.PodDeviceCache
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
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
|
||||
@@ -38,11 +35,8 @@ class OverviewFragmentVM @Inject constructor(
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val permissionTool: PermissionTool,
|
||||
private val generalSettings: GeneralSettings,
|
||||
debugSettings: DebugSettings,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val podDeviceCache: PodDeviceCache,
|
||||
private val podFactory: PodFactory
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
private val updateTicker = channelFlow<Unit> {
|
||||
@@ -54,30 +48,14 @@ class OverviewFragmentVM @Inject constructor(
|
||||
|
||||
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
||||
|
||||
private val pods: Flow<List<PodDevice>> = permissionTool.missingPermissions
|
||||
.flatMapLatest { permissions ->
|
||||
if (permissions.isNotEmpty()) {
|
||||
return@flatMapLatest flowOf(emptyList())
|
||||
}
|
||||
|
||||
generalSettings.showAll.flow.flatMapLatest { showAll ->
|
||||
if (showAll) {
|
||||
podMonitor.devices
|
||||
} else {
|
||||
podMonitor.mainDevice.map { mainDevice ->
|
||||
mainDevice?.let { listOf(it) } ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch { errorEvents.postValue(it) }
|
||||
private val mainDevice = podMonitor.mainDevice.throttleLatest(1000)
|
||||
|
||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||
updateTicker,
|
||||
permissionTool.missingPermissions,
|
||||
debugSettings.isDebugModeEnabled.flow,
|
||||
bluetoothManager.isBluetoothEnabled,
|
||||
podMonitor.mainDevice.throttleLatest(1000),
|
||||
mainDevice,
|
||||
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
|
||||
val items = mutableListOf<OverviewAdapter.Item>()
|
||||
|
||||
@@ -109,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
|
||||
}
|
||||
@@ -3,13 +3,12 @@ package eu.darken.capod.wear.ui.overview.cards.pods
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.BindableVH
|
||||
import eu.darken.capod.common.lists.differ.DifferItem
|
||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||
import eu.darken.capod.wear.ui.overview.OverviewAdapter
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
import eu.darken.capod.wear.ui.overview.OverviewAdapter
|
||||
import java.time.Instant
|
||||
|
||||
abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
||||
@@ -19,7 +18,7 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
||||
|
||||
fun Item.getReceptionText(): String = device.getSignalQuality(context)
|
||||
.let { if (showDebug) "$it ${device.seenCounter}" else it }
|
||||
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
|
||||
.let { if (isMainPod) "$it\n(${getString(eu.darken.capod.common.R.string.pods_yours)})" else it }
|
||||
|
||||
interface Item : OverviewAdapter.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>
|
||||
@@ -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"
|
||||
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.MaterialComponents.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="32dp"
|
||||
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"
|
||||
android:id="@+id/body"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
@@ -267,7 +276,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>
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ apply(plugin = "dagger.hilt.android.plugin")
|
||||
apply(plugin = "androidx.navigation.safeargs.kotlin")
|
||||
|
||||
android {
|
||||
|
||||
compileSdk = ProjectConfig.compileSdk
|
||||
namespace = "${ProjectConfig.packageName}"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = ProjectConfig.packageName
|
||||
@@ -100,12 +100,12 @@ android {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
@@ -141,12 +141,15 @@ dependencies {
|
||||
addBaseAndroid()
|
||||
addBaseAndroidUi()
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
|
||||
addNavigation()
|
||||
addBaseWorkManager()
|
||||
|
||||
addTesting()
|
||||
|
||||
"gplayImplementation"("com.android.billingclient:billing:4.0.0")
|
||||
"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")
|
||||
|
||||
@@ -32,7 +32,7 @@ class UpgradeControlFoss @Inject constructor(
|
||||
|
||||
override fun launchBillingFlow(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity).apply {
|
||||
setIcon(R.drawable.ic_heart)
|
||||
setIcon(eu.darken.capod.common.R.drawable.ic_heart)
|
||||
setTitle(R.string.upgrade_capod_label)
|
||||
setMessage(R.string.upgrade_capod_description)
|
||||
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
|
||||
@@ -68,6 +68,6 @@ class UpgradeControlFoss @Inject constructor(
|
||||
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
|
||||
}
|
||||
|
||||
override fun getSponsorUrl(): String? = "https://github.com/sponsors/d4rken"
|
||||
override fun getSponsorUrl(): String = "https://github.com/sponsors/d4rken"
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">दान करना</string>
|
||||
<string name="foss_upgrade_donate_label">दान करें</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">मैंने पहले ही दान कर दिया है</string>
|
||||
<string name="foss_upgrade_no_money_label">मैं अपना सारा पैसा AirPods पर खर्च करता हूं</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
|
||||
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em 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>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">資助</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">我已經資助了</string>
|
||||
<string name="foss_upgrade_no_money_label">我已經為 AirPods 傾家蕩產了</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package eu.darken.capod.common.upgrade.core
|
||||
|
||||
import com.android.billingclient.api.Purchase
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
|
||||
import eu.darken.capod.common.upgrade.core.data.Sku
|
||||
|
||||
|
||||
fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
|
||||
PurchasedSku(Sku(it), this)
|
||||
}
|
||||
|
||||
private val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Extensions")
|
||||
@@ -49,7 +49,7 @@ class UpgradeRepoGplay @Inject constructor(
|
||||
lastProStateAt = now
|
||||
Info(billingData = data)
|
||||
}
|
||||
(now - lastProStateAt) < 6 * 60 * 1000L -> { // 6 hours
|
||||
(now - lastProStateAt) < 6 * 60 * 60 * 1000L -> { // 6 hours
|
||||
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
|
||||
Info(gracePeriod = true, billingData = null)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class UpgradeRepoGplay @Inject constructor(
|
||||
// Ignore Google Play errors if the last pro state was recent
|
||||
val now = System.currentTimeMillis()
|
||||
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
|
||||
if ((now - lastProStateAt) < 6 * 60 * 60 * 1000L) { // 6 hours
|
||||
if ((now - lastProStateAt) < 24 * 60 * 60 * 1000L) { // 24 hours
|
||||
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
|
||||
emit(Info(gracePeriod = true, billingData = null))
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ class UpgradeRepoGplay @Inject constructor(
|
||||
|
||||
override fun launchBillingFlow(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity).apply {
|
||||
setIcon(R.drawable.ic_heart)
|
||||
setIcon(eu.darken.capod.common.R.drawable.ic_heart)
|
||||
setTitle(R.string.upgrade_capod_label)
|
||||
setMessage(R.string.upgrade_capod_description)
|
||||
setPositiveButton(R.string.general_upgrade_action) { _, _ ->
|
||||
@@ -123,7 +123,7 @@ class UpgradeRepoGplay @Inject constructor(
|
||||
get() = UpgradeRepo.Type.GPLAY
|
||||
|
||||
override val isPro: Boolean
|
||||
get() = billingData?.getProSku() != null
|
||||
get() = billingData?.getProSku() != null || gracePeriod
|
||||
|
||||
override val upgradedAt: Instant?
|
||||
get() = billingData
|
||||
|
||||
+25
-21
@@ -2,6 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
import android.app.Activity
|
||||
import com.android.billingclient.api.*
|
||||
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
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
@@ -38,7 +39,7 @@ data class BillingClientConnection(
|
||||
|
||||
if (!result.isSuccess) {
|
||||
log(TAG, WARN) { "queryPurchases() failed" }
|
||||
throw BillingClientException(result)
|
||||
throw BillingResultException(result)
|
||||
} else {
|
||||
requireNotNull(purchases)
|
||||
}
|
||||
@@ -47,34 +48,31 @@ data class BillingClientConnection(
|
||||
return purchases
|
||||
}
|
||||
|
||||
suspend fun acknowledgePurchase(purchase: Purchase): BillingResult {
|
||||
suspend fun acknowledgePurchase(purchase: Purchase) {
|
||||
val ack = AcknowledgePurchaseParams.newBuilder().apply {
|
||||
setPurchaseToken(purchase.purchaseToken)
|
||||
}.build()
|
||||
|
||||
val ackResult = suspendCoroutine<BillingResult> { continuation ->
|
||||
val result = suspendCoroutine<BillingResult> { continuation ->
|
||||
client.acknowledgePurchase(ack) { continuation.resume(it) }
|
||||
}
|
||||
log(TAG) {
|
||||
"acknowledgePurchase(purchase=$purchase): code=${ackResult.responseCode}, message=${ackResult.debugMessage})"
|
||||
}
|
||||
|
||||
if (!ackResult.isSuccess) {
|
||||
throw BillingClientException(ackResult)
|
||||
}
|
||||
log(TAG, INFO) { "acknowledgePurchase($purchase): code=${result.responseCode} (${result.debugMessage})" }
|
||||
|
||||
return ackResult
|
||||
if (!result.isSuccess) throw BillingResultException(result)
|
||||
}
|
||||
|
||||
suspend fun querySku(sku: Sku): Sku.Details {
|
||||
val skuParams = SkuDetailsParams.newBuilder().apply {
|
||||
setType(BillingClient.SkuType.INAPP)
|
||||
setSkusList(listOf(sku.id))
|
||||
val productDetails = QueryProductDetailsParams.Product.newBuilder().apply {
|
||||
setProductType(BillingClient.ProductType.INAPP)
|
||||
setProductId(sku.id)
|
||||
}.build()
|
||||
|
||||
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<SkuDetails>?>> { continuation ->
|
||||
client.querySkuDetailsAsync(skuParams) { skuResult, skuDetails ->
|
||||
continuation.resume(skuResult to skuDetails)
|
||||
val params = QueryProductDetailsParams.newBuilder().setProductList(listOf(productDetails)).build()
|
||||
|
||||
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<ProductDetails>?>> { continuation ->
|
||||
client.queryProductDetailsAsync(params) { result, skuDetails ->
|
||||
continuation.resume(result to skuDetails)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +80,7 @@ data class BillingClientConnection(
|
||||
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
|
||||
}
|
||||
|
||||
if (!result.isSuccess) throw BillingClientException(result)
|
||||
if (!result.isSuccess) throw BillingResultException(result)
|
||||
|
||||
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
|
||||
|
||||
@@ -97,10 +95,16 @@ data class BillingClientConnection(
|
||||
|
||||
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
|
||||
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
|
||||
return client.launchBillingFlow(
|
||||
activity,
|
||||
BillingFlowParams.newBuilder().setSkuDetails(skuDetails.details.single()).build()
|
||||
)
|
||||
|
||||
val productParams = BillingFlowParams.ProductDetailsParams.newBuilder().apply {
|
||||
setProductDetails(skuDetails.details.first())
|
||||
}.build()
|
||||
|
||||
val billingFlowParams = BillingFlowParams.newBuilder().apply {
|
||||
setProductDetailsParamsList(listOf(productParams))
|
||||
}.build()
|
||||
|
||||
return client.launchBillingFlow(activity, billingFlowParams)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+44
-38
@@ -14,15 +14,15 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Singleton
|
||||
class BillingClientConnectionProvider @Inject constructor(
|
||||
@@ -48,36 +48,40 @@ class BillingClientConnectionProvider @Inject constructor(
|
||||
}
|
||||
}.build()
|
||||
|
||||
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
|
||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
log(TAG, VERBOSE) {
|
||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
|
||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
log(TAG, VERBOSE) {
|
||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
}
|
||||
|
||||
when (result.responseCode) {
|
||||
BillingResponseCode.OK -> {
|
||||
val connection = BillingClientConnection(client, purchasePublisher)
|
||||
|
||||
trySendBlocking(connection)
|
||||
|
||||
launch {
|
||||
try {
|
||||
purchasePublisher.value = connection.queryPurchases()
|
||||
log(TAG) { "Initial IAP query successful." }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
close(BillingResultException(result))
|
||||
}
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||
close(CancellationException("Billing service disconnected"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val billingClientConnection = when (connectionResult.responseCode) {
|
||||
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
|
||||
else -> throw BillingClientException(connectionResult)
|
||||
}
|
||||
|
||||
try {
|
||||
purchasePublisher.value = billingClientConnection.queryPurchases()
|
||||
log(TAG) { "Initial IAP query successful." }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||
}
|
||||
|
||||
send(billingClientConnection)
|
||||
override fun onBillingServiceDisconnected() {
|
||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||
close(BillingException("Billing service disconnected"))
|
||||
}
|
||||
})
|
||||
|
||||
log(TAG) { "Awaiting close." }
|
||||
awaitClose {
|
||||
@@ -89,26 +93,28 @@ class BillingClientConnectionProvider @Inject constructor(
|
||||
val connection: Flow<BillingClientConnection> = connectionProvider
|
||||
.setupCommonEventHandlers(TAG) { "connection" }
|
||||
.retryWhen { cause, attempt ->
|
||||
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
|
||||
|
||||
if (cause is CancellationException) {
|
||||
log(TAG) { "BillingClient connection cancelled." }
|
||||
return@retryWhen false
|
||||
}
|
||||
if (attempt > 5) {
|
||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||
|
||||
if (cause !is BillingException) {
|
||||
log(TAG, WARN) { "Unknown exception type: $cause" }
|
||||
return@retryWhen false
|
||||
}
|
||||
if (cause !is BillingClientException) {
|
||||
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
|
||||
return@retryWhen false
|
||||
} else {
|
||||
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
|
||||
}
|
||||
|
||||
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
|
||||
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
if (attempt > 5) {
|
||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
log(TAG) { "Will retry BillingClient connection... *sigh*" }
|
||||
delay(3000 * attempt)
|
||||
true
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
import com.android.billingclient.api.BillingResult
|
||||
|
||||
class BillingClientException(val result: BillingResult) : Exception() {
|
||||
override val message: String?
|
||||
get() = result.debugMessage
|
||||
|
||||
override fun toString(): String =
|
||||
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
}
|
||||
+11
-1
@@ -4,4 +4,14 @@ import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingResult
|
||||
|
||||
internal val BillingResult.isSuccess: Boolean
|
||||
get() = responseCode == BillingClient.BillingResponseCode.OK
|
||||
get() = responseCode == BillingClient.BillingResponseCode.OK
|
||||
|
||||
internal val BillingResult.isGplayUnavailableTemporary: Boolean
|
||||
get() = setOf(
|
||||
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
|
||||
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
|
||||
BillingClient.BillingResponseCode.SERVICE_TIMEOUT
|
||||
).contains(responseCode)
|
||||
|
||||
internal val BillingResult.isGplayUnavailablePermanent: Boolean
|
||||
get() = responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
open class BillingException(override val message: String) : Exception()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
import com.android.billingclient.api.BillingResult
|
||||
|
||||
class BillingResultException(val result: BillingResult) : BillingException(result.debugMessage) {
|
||||
|
||||
override fun toString(): String =
|
||||
"BillingResultException(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
}
|
||||
+5
-9
@@ -7,13 +7,9 @@ import eu.darken.capod.common.error.LocalizedError
|
||||
|
||||
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
|
||||
HasLocalizedError {
|
||||
override fun getLocalizedError(context: Context): LocalizedError {
|
||||
return LocalizedError(
|
||||
throwable = this,
|
||||
label = "Google Play Services Unavailable",
|
||||
description = context.getString(R.string.upgrades_gplay_unavailable_error)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
|
||||
throwable = this,
|
||||
label = "Google Play Services Unavailable",
|
||||
description = context.getString(R.string.upgrades_gplay_unavailable_error)
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package eu.darken.capod.common.upgrade.core.data
|
||||
|
||||
import com.android.billingclient.api.Purchase
|
||||
import eu.darken.capod.common.upgrade.core.toPurchasedSku
|
||||
|
||||
data class BillingData(
|
||||
val purchases: Collection<Purchase>
|
||||
) {
|
||||
val purchasedSkus: Collection<PurchasedSku>
|
||||
get() = purchases.map { it.toPurchasedSku() }.flatten()
|
||||
|
||||
private fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
|
||||
PurchasedSku(Sku(it), this)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.darken.capod.common.upgrade.core.data
|
||||
|
||||
import android.app.Activity
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
@@ -10,9 +9,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.replayingShare
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
|
||||
import eu.darken.capod.common.upgrade.core.client.BillingClientException
|
||||
import eu.darken.capod.common.upgrade.core.client.GplayServiceUnavailableException
|
||||
import eu.darken.capod.common.upgrade.core.client.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -53,37 +50,34 @@ class BillingDataRepo @Inject constructor(
|
||||
}
|
||||
.forEach {
|
||||
log(TAG, INFO) { "Acknowledging purchase: $it" }
|
||||
|
||||
try {
|
||||
client.acknowledgePurchase(it)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Failed to ancknowledge purchase: $it\n${e.asLog()}" }
|
||||
}
|
||||
client.acknowledgePurchase(it)
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "connection-acks" }
|
||||
.retryWhen { cause, attempt ->
|
||||
log(TAG, ERROR) { "Failed to acknowledge purchase: ${cause.asLog()}" }
|
||||
|
||||
if (cause is CancellationException) {
|
||||
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
if (attempt > 5) {
|
||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||
return@retryWhen false
|
||||
}
|
||||
if (cause !is BillingClientException) {
|
||||
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
|
||||
|
||||
if (cause !is BillingException) {
|
||||
log(TAG, WARN) { "Unknown exception type: $cause" }
|
||||
return@retryWhen false
|
||||
} else {
|
||||
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
|
||||
}
|
||||
|
||||
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
|
||||
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
log(TAG) { "Will retry ACK" }
|
||||
log(TAG) { "Will retry ACK (attempt=$attempt)" }
|
||||
delay(3000 * attempt)
|
||||
true
|
||||
}
|
||||
@@ -108,7 +102,7 @@ class BillingDataRepo @Inject constructor(
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
|
||||
val ignoredCodes = listOf(3, 6)
|
||||
if (e !is BillingClientException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
|
||||
if (e !is BillingResultException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
|
||||
Bugs.report(TAG, "IAP flow failed for $sku", e)
|
||||
}
|
||||
|
||||
@@ -119,16 +113,14 @@ class BillingDataRepo @Inject constructor(
|
||||
companion object {
|
||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
|
||||
|
||||
internal fun Throwable.tryMapUserFriendly(): Throwable {
|
||||
if (this !is BillingClientException) return this
|
||||
|
||||
return when (result.responseCode) {
|
||||
BillingResponseCode.BILLING_UNAVAILABLE,
|
||||
BillingResponseCode.SERVICE_UNAVAILABLE,
|
||||
BillingResponseCode.SERVICE_DISCONNECTED,
|
||||
BillingResponseCode.SERVICE_TIMEOUT -> GplayServiceUnavailableException(this)
|
||||
else -> this
|
||||
internal fun Throwable.tryMapUserFriendly(): Throwable = when {
|
||||
this is BillingResultException && this.result.isGplayUnavailableTemporary -> {
|
||||
GplayServiceUnavailableException(this)
|
||||
}
|
||||
this is BillingResultException && this.result.isGplayUnavailablePermanent -> {
|
||||
GplayServiceUnavailableException(this)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.darken.capod.common.upgrade.core.data
|
||||
|
||||
import com.android.billingclient.api.SkuDetails
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
|
||||
data class Sku(
|
||||
val id: String
|
||||
) {
|
||||
data class Details(
|
||||
val sku: Sku,
|
||||
val details: Collection<SkuDetails>,
|
||||
val details: Collection<ProductDetails>,
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play सेवाएं अनुपलब्ध हैं।</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली. क्या आप सही खाते का उपयोग कर रहे हैं?</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली, क्या आप सही खाते का उपयोग कर रहे हैं?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play estão indisponíveis.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Está a usar a conta correta?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務無法使用。</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務不可用。</string>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務無法使用。</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -41,7 +40,7 @@ class ClipboardHelper @Inject constructor(
|
||||
}
|
||||
|
||||
fun copyToClipboard(text: String) {
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.app_name), text)
|
||||
val clip = ClipData.newPlainText(context.getString(eu.darken.capod.common.R.string.app_name), text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class RecorderService : Service2() {
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setSmallIcon(R.drawable.ic_baseline_bug_report_24)
|
||||
.setContentText("Idle")
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentTitle(getString(eu.darken.capod.common.R.string.app_name))
|
||||
.addAction(NotificationCompat.Action.Builder(0, getString(R.string.general_done_action), stopPi).build())
|
||||
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
|
||||
@@ -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
|
||||
@@ -115,18 +117,18 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
val baseTitle = when (info.type) {
|
||||
UpgradeRepo.Type.GPLAY -> {
|
||||
if (info.isPro) {
|
||||
getString(R.string.app_name_pro)
|
||||
getString(eu.darken.capod.common.R.string.app_name_pro)
|
||||
} else {
|
||||
gplay.isVisible = true
|
||||
getString(R.string.app_name)
|
||||
getString(eu.darken.capod.common.R.string.app_name)
|
||||
}
|
||||
}
|
||||
UpgradeRepo.Type.FOSS -> {
|
||||
if (info.isPro) {
|
||||
getString(R.string.app_name_foss)
|
||||
getString(eu.darken.capod.common.R.string.app_name_foss)
|
||||
} else {
|
||||
donate.isVisible = true
|
||||
getString(R.string.app_name)
|
||||
getString(eu.darken.capod.common.R.string.app_name)
|
||||
}
|
||||
}
|
||||
}.split(" ".toRegex())
|
||||
@@ -141,12 +143,13 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
}
|
||||
builder.append(colorString(requireContext(), color, baseTitle[1]))
|
||||
} else {
|
||||
getString(R.string.app_name)
|
||||
getString(eu.darken.capod.common.R.string.app_name)
|
||||
}
|
||||
}
|
||||
vm.launchUpgradeFlow.observe2 {
|
||||
it(requireActivity())
|
||||
}
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,9 @@ class OverviewFragmentVM @Inject constructor(
|
||||
if (!isBluetoothEnabled) {
|
||||
items.add(0, BluetoothDisabledVH.Item)
|
||||
} else if (mainPod == null) {
|
||||
items.add(0, MissingMainDeviceVH.Item)
|
||||
items.add(0, MissingMainDeviceVH.Item {
|
||||
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navVia(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@ class MissingMainDeviceVH(parent: ViewGroup) :
|
||||
item: Item,
|
||||
payloads: List<Any>
|
||||
) -> Unit = binding(payload = true) { item ->
|
||||
|
||||
troubleshootAction.setOnClickListener { item.onTroubleShoot() }
|
||||
}
|
||||
|
||||
object Item : OverviewAdapter.Item {
|
||||
data class Item(
|
||||
val onTroubleShoot: () -> Unit,
|
||||
) : OverviewAdapter.Item {
|
||||
override val stableId: Long = Item::class.hashCode().toLong()
|
||||
|
||||
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
|
||||
|
||||
@@ -7,9 +7,21 @@ import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.HasPodStyle
|
||||
import eu.darken.capod.pods.core.HasStateDetection
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -42,8 +54,10 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||
podRightIcon.setImageResource(device.rightPodIcon)
|
||||
|
||||
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
|
||||
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||
lastSeen.text =
|
||||
context.getString(eu.darken.capod.common.R.string.last_seen_x, device.lastSeenFormatted(item.now))
|
||||
firstSeen.text =
|
||||
context.getString(eu.darken.capod.common.R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
@@ -131,9 +145,9 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
device.apply {
|
||||
if (this is DualApplePods) {
|
||||
podCaseLidLabel.text = when (caseLidState) {
|
||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||
else -> context.getString(R.string.pods_case_unknown_state)
|
||||
LidState.OPEN -> context.getString(eu.darken.capod.common.R.string.pods_case_status_open_label)
|
||||
LidState.CLOSED -> context.getString(eu.darken.capod.common.R.string.pods_case_status_closed_label)
|
||||
else -> context.getString(eu.darken.capod.common.R.string.pods_case_unknown_state)
|
||||
}
|
||||
|
||||
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.BindableVH
|
||||
import eu.darken.capod.common.lists.differ.DifferItem
|
||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||
@@ -19,7 +18,7 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
||||
|
||||
fun Item.getReceptionText(): String = device.getSignalQuality(context)
|
||||
.let { if (showDebug) "$it ${device.seenCounter}" else it }
|
||||
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
|
||||
.let { if (isMainPod) "$it\n(${getString(eu.darken.capod.common.R.string.pods_yours)})" else it }
|
||||
|
||||
interface Item : OverviewAdapter.Item {
|
||||
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
details.text = when (item.device) {
|
||||
is ApplePods -> getString(R.string.pods_unknown_contact_dev)
|
||||
else -> getString(R.string.pods_unknown_label)
|
||||
is ApplePods -> getString(eu.darken.capod.common.R.string.pods_unknown_contact_dev)
|
||||
else -> getString(eu.darken.capod.common.R.string.pods_unknown_label)
|
||||
}
|
||||
|
||||
rawdata.text = device.rawDataHex.joinToString("\n")
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -21,7 +21,20 @@ import eu.darken.capod.common.upgrade.isPro
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.monitor.core.PodDeviceCache
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
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 finish2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -39,7 +52,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
private fun executeAsync(
|
||||
tag: String,
|
||||
timeout: Duration = Duration.ofSeconds(8),
|
||||
timeout: Duration = Duration.ofSeconds(7),
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
val start = System.currentTimeMillis()
|
||||
@@ -52,7 +65,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||
} finally {
|
||||
asyncBarrier.finish()
|
||||
asyncBarrier.finish2()
|
||||
val stop = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||
}
|
||||
@@ -134,7 +147,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
|
||||
setTextViewText(R.id.primary, context.getString(eu.darken.capod.common.R.string.pods_unknown_label))
|
||||
}
|
||||
|
||||
private fun createNoDeviceLayout(
|
||||
@@ -150,7 +163,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.overview_nomaindevice_label))
|
||||
setTextViewText(R.id.primary, context.getString(eu.darken.capod.common.R.string.overview_nomaindevice_label))
|
||||
}
|
||||
|
||||
private fun createDualPodLayout(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.EntryPoints
|
||||
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
|
||||
@@ -23,7 +23,6 @@ import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.main.ui.widget.WidgetManager
|
||||
import eu.darken.capod.monitor.core.MonitorComponent
|
||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||
@@ -31,15 +30,26 @@ 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
|
||||
class MonitorWorker @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted private val params: WorkerParameters,
|
||||
monitorComponentBuilder: MonitorComponent.Builder,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val monitorNotifications: MonitorNotifications,
|
||||
private val notificationManager: NotificationManager,
|
||||
@@ -55,13 +65,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
private val workerScope = MonitorCoroutineScope()
|
||||
private val monitorComponent = monitorComponentBuilder
|
||||
.coroutineScope(workerScope)
|
||||
.build()
|
||||
|
||||
private val entryPoint by lazy {
|
||||
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
|
||||
}
|
||||
|
||||
private var finishedWithError = false
|
||||
|
||||
@@ -69,10 +72,16 @@ class MonitorWorker @AssistedInject constructor(
|
||||
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return monitorNotifications.getForegroundInfo(null)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val start = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||
|
||||
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||
|
||||
doDoWork()
|
||||
|
||||
val duration = System.currentTimeMillis() - start
|
||||
@@ -101,7 +110,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
|
||||
val monitorJob = podMonitor.mainDevice
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
@@ -134,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) {
|
||||
@@ -149,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." }
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import eu.darken.capod.monitor.core.MonitorComponent
|
||||
|
||||
@InstallIn(MonitorComponent::class)
|
||||
@EntryPoint
|
||||
interface MonitorWorkerEntryPoint
|
||||
@@ -20,6 +20,8 @@ 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.PodDevice
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class MonitorNotifications @Inject constructor(
|
||||
private val notificationViewFactory: MonitorNotificationViewFactory
|
||||
) {
|
||||
|
||||
private val builderLock = Mutex()
|
||||
private val builder: NotificationCompat.Builder
|
||||
|
||||
init {
|
||||
@@ -50,20 +53,20 @@ class MonitorNotifications @Inject constructor(
|
||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
setContentIntent(openPi)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
setSmallIcon(R.drawable.devic_earbuds_generic_both)
|
||||
setSmallIcon(eu.darken.capod.common.R.drawable.devic_earbuds_generic_both)
|
||||
setOngoing(true)
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentTitle(context.getString(eu.darken.capod.common.R.string.app_name))
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
private fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
if (device == null) {
|
||||
return builder.apply {
|
||||
setCustomContentView(null)
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
setContentTitle(context.getString(R.string.pods_none_label_short))
|
||||
setSubText(context.getString(R.string.app_name))
|
||||
setSmallIcon(R.drawable.devic_earbuds_generic_both)
|
||||
setContentTitle(context.getString(eu.darken.capod.common.R.string.pods_none_label_short))
|
||||
setSubText(context.getString(eu.darken.capod.common.R.string.app_name))
|
||||
setSmallIcon(eu.darken.capod.common.R.drawable.devic_earbuds_generic_both)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +79,13 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotification(podDevice: PodDevice?): Notification = getBuilder(podDevice).build()
|
||||
suspend fun getNotification(podDevice: PodDevice?): Notification = builderLock.withLock {
|
||||
getBuilder(podDevice).build()
|
||||
}
|
||||
|
||||
fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = getBuilder(podDevice).toForegroundInfo()
|
||||
suspend fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = builderLock.withLock {
|
||||
getBuilder(podDevice).toForegroundInfo()
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.reaction.ui
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.Keep
|
||||
@@ -11,6 +10,7 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
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() {
|
||||
@@ -83,6 +83,11 @@ class ReactionSettingsFragment : PreferenceFragment3() {
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
} else if (preference.key == reactionSettings.showPopUpOnConnection.key && !isPro) {
|
||||
preference as CheckBoxPreference
|
||||
upgradeRepo.launchBillingFlow(requireActivity())
|
||||
preference.isChecked = false
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package eu.darken.capod.troubleshooter.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.navigation.popBackStack
|
||||
import eu.darken.capod.common.uix.Fragment3
|
||||
import eu.darken.capod.common.viewbinding.viewBinding
|
||||
import eu.darken.capod.databinding.TroubleshooterFragmentBinding
|
||||
import eu.darken.capod.troubleshooter.ui.TroubleShooterFragmentVM.BleState
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TroubleShooterFragment : Fragment3(R.layout.troubleshooter_fragment) {
|
||||
|
||||
override val vm: TroubleShooterFragmentVM by viewModels()
|
||||
override val ui: TroubleshooterFragmentBinding by viewBinding()
|
||||
|
||||
@Inject lateinit var webpageTool: WebpageTool
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
ui.toolbar.apply {
|
||||
setupWithNavController(findNavController())
|
||||
}
|
||||
|
||||
ui.bleIntroStartAction.setOnClickListener { vm.troubleShootBle() }
|
||||
|
||||
vm.bleState.observe2(ui) { state ->
|
||||
bleIntroContainer.isVisible = state is BleState.Intro
|
||||
bleProcessContainer.isVisible = state is BleState.Working
|
||||
bleResultContainer.isVisible = state is BleState.Result
|
||||
|
||||
when (state) {
|
||||
is BleState.Intro -> {}
|
||||
is BleState.Working -> {
|
||||
bleProcessHistory.apply {
|
||||
text = ""
|
||||
state.allSteps.forEachIndexed { index, s -> append("#$index: $s\n") }
|
||||
}
|
||||
}
|
||||
is BleState.Result -> {
|
||||
bleResultHistory.apply {
|
||||
text = ""
|
||||
state.history.forEachIndexed { index, s -> append("#$index: $s\n") }
|
||||
}
|
||||
|
||||
|
||||
when (state) {
|
||||
is BleState.Result.Success -> {
|
||||
bleResultAction.isVisible = false
|
||||
bleResultTitle.text = getString(R.string.troubleshooter_ble_result_success_title)
|
||||
bleResultBody.text = getString(R.string.troubleshooter_ble_result_success_body)
|
||||
bleResultAction.text = getString(R.string.general_close_action)
|
||||
bleResultAction.setOnClickListener { popBackStack() }
|
||||
}
|
||||
is BleState.Result.Failure -> {
|
||||
bleResultTitle.text = getString(R.string.troubleshooter_ble_result_failure_title)
|
||||
bleResultBody.text = when (state.failureType) {
|
||||
BleState.Result.Failure.Type.PHONE -> getString(R.string.troubleshooter_ble_result_failure_phone_body)
|
||||
BleState.Result.Failure.Type.HEADPHONES -> getString(R.string.troubleshooter_ble_result_failure_phone_headphones)
|
||||
}
|
||||
bleResultAction.text = getString(R.string.general_check_action)
|
||||
bleResultAction.setOnClickListener { vm.troubleShootBle() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package eu.darken.capod.troubleshooter.ui
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
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.unknown.UnknownDevice
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TroubleShooterFragmentVM @Inject constructor(
|
||||
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
private val _bleState = MutableStateFlow<BleState>(BleState.Intro())
|
||||
val bleState = _bleState.asLiveData2()
|
||||
|
||||
init {
|
||||
_bleState
|
||||
.onEach { log(TAG) { "New BLE State: $it" } }
|
||||
.launchIn(vmScope)
|
||||
}
|
||||
|
||||
private fun progress(message: String) {
|
||||
var state = _bleState.value
|
||||
if (state !is BleState.Working) {
|
||||
state = BleState.Working("Starting...")
|
||||
}
|
||||
|
||||
_bleState.value = state.nextStep(message)
|
||||
}
|
||||
|
||||
private fun success(message: String) {
|
||||
var state = _bleState.value
|
||||
if (state !is BleState.Working) {
|
||||
state = BleState.Working("Starting...")
|
||||
}
|
||||
_bleState.value = state.toSuccess(message)
|
||||
}
|
||||
|
||||
private fun failure(message: String, type: BleState.Result.Failure.Type) {
|
||||
var state = _bleState.value
|
||||
if (state !is BleState.Working) {
|
||||
state = BleState.Working("Starting...")
|
||||
}
|
||||
_bleState.value = state.toFailure(message, type)
|
||||
}
|
||||
|
||||
fun troubleShootBle() = launch(context = dispatcherProvider.IO) {
|
||||
log(TAG) { "troubleShootBle()" }
|
||||
|
||||
generalSettings.scannerMode.value = ScannerMode.LOW_LATENCY
|
||||
|
||||
run {
|
||||
progress("Checking for headphones...")
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.mainDevice.filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
success("Headphones found, nothing to troubleshoot.")
|
||||
return@launch
|
||||
} else {
|
||||
progress("Headphones not detected.\n")
|
||||
}
|
||||
}
|
||||
|
||||
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<PodDevice> = { hardwareFilteringDisabled,
|
||||
hardwareBatchingDisabled,
|
||||
indirectCallback,
|
||||
unfiltered ->
|
||||
val sb = StringBuilder("SCAN - Settings: ")
|
||||
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
||||
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
||||
sb.append("indirectCallback=$indirectCallback, ")
|
||||
sb.append("unfiltered=$unfiltered")
|
||||
progress(sb.toString())
|
||||
generalSettings.isOffloadedFilteringDisabled.value = hardwareFilteringDisabled
|
||||
generalSettings.isOffloadedBatchingDisabled.value = hardwareBatchingDisabled
|
||||
generalSettings.useIndirectScanResultCallback.value = indirectCallback
|
||||
debugSettings.showUnfiltered.value = unfiltered
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
val devices = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
log(TAG) { "SCAN: BLE Devices: $devices" }
|
||||
if (devices.isNotEmpty()) {
|
||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||
devices
|
||||
} else {
|
||||
progress("SCAN: No data received")
|
||||
devices
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
run {
|
||||
progress("Checking if we can receive BLE data at all.")
|
||||
if (doScan(false, false, false, true).isNotEmpty()) return@run
|
||||
if (doScan(false, false, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, true, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, true, false, true).isNotEmpty()) return@run
|
||||
if (doScan(true, false, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, false, false, true).isNotEmpty()) return@run
|
||||
if (doScan(false, true, true, true).isNotEmpty()) return@run
|
||||
if (doScan(false, true, false, true).isNotEmpty()) return@run
|
||||
|
||||
failure("Phone is not receiving BLE data.", BleState.Result.Failure.Type.PHONE)
|
||||
|
||||
generalSettings.isOffloadedFilteringDisabled.value = false
|
||||
generalSettings.isOffloadedBatchingDisabled.value = false
|
||||
generalSettings.useIndirectScanResultCallback.value = false
|
||||
debugSettings.showUnfiltered.value = false
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress("We received at least some BLE data.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for supported headphones.")
|
||||
|
||||
if (doScan(false, false, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, false, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, true, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, true, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, false, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, false, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, true, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, true, false, false).any { it !is UnknownDevice }) return@run
|
||||
|
||||
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
||||
|
||||
generalSettings.isOffloadedFilteringDisabled.value = false
|
||||
generalSettings.isOffloadedBatchingDisabled.value = false
|
||||
generalSettings.useIndirectScanResultCallback.value = false
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress("Found some headphones that are supported by CAPod.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for your headphones with new BLE settings...")
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.mainDevice.filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
success("Found your headphones, new BLE settings worked :)!")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
progress("Still no headphones detected that count as yours.\n")
|
||||
|
||||
run {
|
||||
progress("Checking all closeby headphones.")
|
||||
|
||||
val otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||
val start = System.currentTimeMillis()
|
||||
podMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
|
||||
if (otherDevices.isEmpty()) {
|
||||
failure("No supported headphones found near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress("Headphones found nearby, but not detected as yours. Resetting filters.\n")
|
||||
|
||||
generalSettings.mainDeviceModel.value = PodDevice.Model.UNKNOWN
|
||||
generalSettings.mainDeviceAddress.value = null
|
||||
generalSettings.minimumSignalQuality.value = 0.25f
|
||||
|
||||
progress("Setting headphone with strongest signal as yours.")
|
||||
|
||||
generalSettings.mainDeviceModel.value = otherDevices.maxBy { it.signalQuality }.model
|
||||
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.mainDevice.filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
generalSettings.mainDeviceModel.value = mainDevice.model
|
||||
generalSettings.scannerMode.value = ScannerMode.BALANCED
|
||||
success("Success! Detected your headphones.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class BleState {
|
||||
class Intro : BleState()
|
||||
|
||||
data class Working(
|
||||
val current: String,
|
||||
val history: List<String> = emptyList(),
|
||||
) : BleState() {
|
||||
|
||||
val allSteps: List<String>
|
||||
get() = history + current
|
||||
|
||||
fun nextStep(message: String) = this.copy(
|
||||
current = message,
|
||||
history = history + current
|
||||
)
|
||||
|
||||
fun toSuccess(message: String) = Result.Success(
|
||||
history = history + message
|
||||
)
|
||||
|
||||
fun toFailure(message: String, type: Result.Failure.Type) = Result.Failure(
|
||||
failureType = type,
|
||||
history = history + message,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Result : BleState() {
|
||||
|
||||
abstract val history: List<String>
|
||||
|
||||
data class Success(
|
||||
override val history: List<String>,
|
||||
) : Result()
|
||||
|
||||
data class Failure(
|
||||
val failureType: Type,
|
||||
override val history: List<String>,
|
||||
) : Result() {
|
||||
enum class Type {
|
||||
PHONE,
|
||||
HEADPHONES,
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val STEP_TIME = 10 * 1000L // 6 scans per 30 seconds max
|
||||
|
||||
val TAG = logTag("TroubleShooter", "Fragment", "VM")
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -32,12 +32,22 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/overview_nomaindevice_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/troubleshoot_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_label" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/troubleshoot_action"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/troubleshoot_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Widget.MaterialComponents.Toolbar.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:title="@string/troubleshooter_title" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/ble_card"
|
||||
style="@style/Widget.Material3.CardView.Filled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ble_intro_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/troubleshooter_ble_intro_title" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/troubleshooter_ble_intro_body1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ble_intro_start_action"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/troubleshooter_ble_intro_start_action" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/ble_process_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/ble_process_progress"
|
||||
style="@style/Widget.Material3.CircularProgressIndicator.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/ble_process_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/ble_process_primary" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_process_primary"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/troubleshooter_ble_process_title"
|
||||
app:layout_constraintBottom_toTopOf="@id/ble_process_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/ble_process_progress"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_process_secondary"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/troubleshooter_ble_process_subtile"
|
||||
app:layout_constraintBottom_toTopOf="@id/ble_process_history"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/ble_process_primary"
|
||||
app:layout_constraintTop_toBottomOf="@id/ble_process_primary" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_process_history"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ble_process_secondary"
|
||||
tools:text="Step 1\nStep 2\n\Step 3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ble_result_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_result_title"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/troubleshooter_ble_result_success_title" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_result_body"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/troubleshooter_ble_result_success_body" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ble_result_action"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/troubleshooter_ble_result_failure_action" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ble_result_history"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="Step 1\nStep 2\n\Step 3" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -4,17 +4,6 @@
|
||||
style="@style/PodWidget.Container"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/PodWidget.TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -110,4 +99,14 @@
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/PodWidget.TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -13,6 +13,9 @@
|
||||
<action
|
||||
android:id="@+id/action_overviewFragment_to_settingsFragment"
|
||||
app:destination="@id/settingsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_overviewFragment_to_troubleShooterFragment"
|
||||
app:destination="@id/troubleShooterFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@@ -20,5 +23,9 @@
|
||||
android:name="eu.darken.capod.main.ui.settings.SettingsFragment"
|
||||
android:label="SettingsFragment"
|
||||
tools:layout="@layout/settings_fragment" />
|
||||
<fragment
|
||||
android:id="@+id/troubleShooterFragment"
|
||||
android:name="eu.darken.capod.troubleshooter.ui.TroubleShooterFragment"
|
||||
tools:layout="@layout/troubleshooter_fragment" />
|
||||
|
||||
</navigation>
|
||||
@@ -37,10 +37,9 @@
|
||||
<string name="settings_maindevice_address_none">غير موجود</string>
|
||||
<string name="settings_maindevice_model_label">طِراز جهازك</string>
|
||||
<string name="settings_maindevice_model_description">طِراز جهازك الرئيسي. يساعد هذا التطبيق في التعرف على جهازك عندما لا يكون متصلاً بهاتفك.</string>
|
||||
<string name="settings_popup_caseopen_label">عرض إشعار منبثق</string>
|
||||
<string name="settings_popup_caseopen_description">عرض إشعار منبثق عند فتح علبة الجهاز (تجريبي).</string>
|
||||
<string name="settings_onepod_mode_label">وضعية السماعة الواحدة</string>
|
||||
<string name="settings_onepod_mode_description">ليس بالضرورة ارتداء كِلتا السمَّاعتين، ارتداء سمَّاعة واحدة يكفي لتحريك ردود الفعل.</string>
|
||||
<string name="settings_popup_caseopen_description">عرض إشعار منبثق عند فتح علبة الجهاز (تجريبي).</string>
|
||||
<string name="notification_channel_device_status_label">حالة الجهاز</string>
|
||||
<string name="debug_debuglog_size_label">الحجم</string>
|
||||
<string name="debug_debuglog_size_compressed_label">الحجم المضغوط</string>
|
||||
|
||||
@@ -33,10 +33,9 @@
|
||||
<string name="settings_maindevice_address_none">Heç biri</string>
|
||||
<string name="settings_maindevice_model_label">Cihazınızın modeli</string>
|
||||
<string name="settings_maindevice_model_description">Əsas cihazının modeli. Bu, telefonunuzla bağlantı qurulmadığı vaxtlarda tətbiqin cihazınızı tanımasına kömək edir.</string>
|
||||
<string name="settings_popup_caseopen_label">Açılan pəncərəni göstər</string>
|
||||
<string name="settings_popup_caseopen_description">Cihazın qutusu açılanda bir açılan pəncərə göstər (təcrübi).</string>
|
||||
<string name="settings_onepod_mode_label">Tək tərəf rejimi</string>
|
||||
<string name="settings_onepod_mode_description">Hər iki tərəfi də taxmağa ehtiyac yoxdur, reaksiyaları tətikləmək üçün tək tərəfi taxmaq yetərlidir.</string>
|
||||
<string name="settings_popup_caseopen_description">Cihazın qutusu açılanda bir açılan pəncərə göstər (təcrübi).</string>
|
||||
<string name="notification_channel_device_status_label">Cihaz vəziyyəti</string>
|
||||
<string name="debug_debuglog_size_label">Həcm</string>
|
||||
<string name="debug_debuglog_size_compressed_label">Sıxışdırılmış həcm</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user