mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
23
Commits
v2.8.0-rc0
...
v2.9.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afe1399f72 | ||
|
|
20ce2d3e22 | ||
|
|
cea8f2290d | ||
|
|
1b5b59c41b | ||
|
|
8df7c19e84 | ||
|
|
88b0069b94 | ||
|
|
3b8ddc9f06 | ||
|
|
708c7f38ec | ||
|
|
139daa1cc2 | ||
|
|
b85ac778c0 | ||
|
|
a3e2c9e61d | ||
|
|
ad78a0570c | ||
|
|
7e6ee274d8 | ||
|
|
daf7411c36 | ||
|
|
d3dedbe6cd | ||
|
|
d5411586d5 | ||
|
|
94e2d40554 | ||
|
|
db80fa1b46 | ||
|
|
9b569b8d07 | ||
|
|
90288880fb | ||
|
|
870e518ff6 | ||
|
|
6d89169bae | ||
|
|
ac77ee2703 |
@@ -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
|
||||
|
||||
@@ -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)" }
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
<string name="general_value_not_available_label">Yok</string>
|
||||
<string name="general_error_label">Hata</string>
|
||||
<string name="general_grant_permission_action">İzin ver</string>
|
||||
@@ -11,6 +14,7 @@
|
||||
<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_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_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth cihazlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
|
||||
<string name="permission_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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -13,13 +13,9 @@ 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.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH
|
||||
@@ -38,11 +34,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 +47,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>()
|
||||
|
||||
|
||||
@@ -146,7 +146,8 @@ dependencies {
|
||||
|
||||
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")
|
||||
|
||||
@@ -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,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 {
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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?)
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 finish2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -39,7 +40,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 +53,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) " }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ 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.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
|
||||
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.*
|
||||
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 +53,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 +60,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 +98,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
|
||||
val monitorJob = podMonitor.mainDevice
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
|
||||
@@ -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 {
|
||||
@@ -56,7 +59,7 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
private fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
if (device == null) {
|
||||
return builder.apply {
|
||||
setCustomContentView(null)
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,275 @@
|
||||
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.")
|
||||
|
||||
var otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.devices.take(10).toList().flatten()
|
||||
} ?: 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
|
||||
|
||||
otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||
withTimeoutOrNull(STEP_TIME) {
|
||||
val start = System.currentTimeMillis()
|
||||
podMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
} ?: emptyList()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,2 +1,79 @@
|
||||
<?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="general_share_action">Partilhar</string>
|
||||
<string name="general_done_action">OK</string>
|
||||
<string name="general_copy_action">Copiar</string>
|
||||
<string name="general_thank_you_label">Obrigado</string>
|
||||
<string name="general_upgrade_action">Melhorar</string>
|
||||
<string name="general_check_action">Verificar</string>
|
||||
<string name="general_close_action">Fechar</string>
|
||||
<string name="upgrade_capod_label">Melhorar CAPod</string>
|
||||
<string name="upgrade_capod_description">Obtenha funcionalidades adicionais e apoie o programador.</string>
|
||||
<string name="settings_monitor_mode_label">Modo de monitorização</string>
|
||||
<string name="settings_monitor_mode_description">Sob quais circunstâncias esta aplicação monitora dados Bluetooth.</string>
|
||||
<string name="settings_scanner_mode_label">Modo de scanner</string>
|
||||
<string name="settings_scanner_mode_description">O scanner de dados \'Bluetooth Low Energy\' deve priorizar desempenho ou economizar energia?</string>
|
||||
<string name="settings_autopause_label">Pausa automática</string>
|
||||
<string name="settings_autopause_description">Pause o áudio ao remover o dispositivo do ouvido.</string>
|
||||
<string name="settings_showall_label">Mostrar todos os dispositivos</string>
|
||||
<string name="settings_showall_description">Mostrar os dispositivos de outras pessoas que estão perto de você.</string>
|
||||
<string name="settings_autopplay_label">Reprodução automática</string>
|
||||
<string name="settings_autoplay_description">Iniciar a reprodução de áudio quando o dispositivo for usado.</string>
|
||||
<string name="settings_fake_data_label">Dados falsos</string>
|
||||
<string name="settings_fake_data_description">Mostrar dados falsos, por exemplo, mostrar dispositivos que não existem.</string>
|
||||
<string name="settings_debug_label">Definições de debug</string>
|
||||
<string name="settings_debug_description">Definições adicionais para ajudar a resolver problemas com a aplicação.</string>
|
||||
<string name="settings_signal_minimum_label">Qualidade de sinal mínima</string>
|
||||
<string name="settings_signal_minimum_description">A qualidade de sinal mínima para o dispositivo ser considerado seu.</string>
|
||||
<string name="settings_autoconnect_label">AutoConectar</string>
|
||||
<string name="settings_autoconnect_description">Se o Android não conectar automaticamente, nós podemos pedir também. Isto irá alterar o modo de monitorização para \'Sempre\'.</string>
|
||||
<string name="settings_autoconnect_condition_label">Condição para AutoConectar</string>
|
||||
<string name="settings_autoconnect_condition_description">Quando é que devemos tentar conectar ao seu dispositivo?</string>
|
||||
<string name="settings_reaction_label">Reações</string>
|
||||
<string name="settings_reaction_description">Reagir a eventos e comportamentos.</string>
|
||||
<string name="settings_category_yourdevice_label">O seu dispositivo</string>
|
||||
<string name="settings_maindevice_address_label">Endereço do seu dispositivo</string>
|
||||
<string name="settings_maindevice_address_description">O endereço do seu dispositivo emparelhado. A aplicação utiliza isto para determinar quando é que está conectado ao seu telemóvel.</string>
|
||||
<string name="settings_maindevice_address_none">Nenhum</string>
|
||||
<string name="settings_maindevice_model_label">Modelo do seu dispositivo</string>
|
||||
<string name="settings_maindevice_model_description">O modelo do seu dispositivo principal. Isto ajuda a aplicação a reconhecer o seu dispositivo quando não está conectado ao seu telemóvel.</string>
|
||||
<string name="settings_popup_caseopen_label">Mostrar pop-up</string>
|
||||
<string name="settings_popup_caseopen_description">Mostrar pop-up quando a caixa do dispositivo está aberta (experimental).</string>
|
||||
<string name="settings_onepod_mode_label">Modo de um pod</string>
|
||||
<string name="settings_onepod_mode_description">Usar os dois pods não é necessário, usar um único pod é suficiente para desencadear reações.</string>
|
||||
<string name="notification_channel_device_status_label">Estado do dispositivo</string>
|
||||
<string name="debug_debuglog_size_label">Tamanho</string>
|
||||
<string name="debug_debuglog_size_compressed_label">Tamanho comprimido</string>
|
||||
<string name="debug_notification_channel_label">Notificações de debug</string>
|
||||
<string name="debug_debuglog_file_label">Ficheiro de registo gravado</string>
|
||||
<string name="debug_debuglog_record_action">Gravar ficheiro de registo</string>
|
||||
<string name="settings_support_installid_label">ID de instalação</string>
|
||||
<string name="settings_support_installid_desc">Os relatórios de erros automáticos são anónimos. Compartilhe o seu ID de instalação se o programador precisar encontrar os seus relatórios de erro.</string>
|
||||
<string name="settings_support_label">Suporte</string>
|
||||
<string name="settings_support_description">Se precisar de ajuda.</string>
|
||||
<string name="issue_tracker_label">Rastreador de problemas</string>
|
||||
<string name="issue_tracker_description">Um rastreador de problemas públicos para relatórios de erros e solicitações de recursos (somente em inglês).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">Um lugar para conviver e tirar dúvidas.</string>
|
||||
<string name="changelog_label">Registo de alterações</string>
|
||||
<string name="settings_label">Definições</string>
|
||||
<string name="settings_privacy_policy_label">Política de privacidade</string>
|
||||
<string name="settings_privacy_policy_desc">Manuseio de dados com responsabilidade.</string>
|
||||
<string name="settings_licenses_label">Licenças</string>
|
||||
<string name="settings_category_other_label">Outro</string>
|
||||
<string name="settings_general_label">Definições</string>
|
||||
<string name="settings_general_description">Ajustes gerais que afetam a aplicação toda.</string>
|
||||
<string name="settings_acknowledgements_label">Reconhecimentos</string>
|
||||
<string name="settings_debug_autoreports_label">Relatar de erros automaticamente</string>
|
||||
<string name="settings_debug_autoreports_description">Relata problemas automaticamente, por exemplo, detalhes sobre uma falha na aplicação para eu poder perceber como corrigi-lo.</string>
|
||||
<string name="settings_debug_mode_label">Modo de debug</string>
|
||||
<string name="settings_debug_mode_description">Mostrar informações adicionais para resolver problemas.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Dados BLE sem filtro</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Remova qualquer filtro do scanner BLE para mostrar todos os dados BLE transmitidos. Útil para adicionar suporte para novos headphones.</string>
|
||||
<string name="settings_compatibility_mode_label">Modo de compatibilidade</string>
|
||||
<string name="settings_compatibility_mode_description">Desative as otimizações para melhorar a compatibilidade. Tente isso se não estiver a ver nenhum dado.</string>
|
||||
<string name="help_translate_label">Tradução</string>
|
||||
<string name="help_translate_description">Ajude a traduzir esta aplicação para o seu idioma favorito.</string>
|
||||
<string name="translators_thanks_title">Tradutores</string>
|
||||
<string name="translators_thanks_description">darken</string>
|
||||
</resources>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<string name="settings_support_description">Yardıma ihtiyacın olursa.</string>
|
||||
<string name="issue_tracker_label">Sorun izleyicisi</string>
|
||||
<string name="issue_tracker_description">Hata raporları ve özellik istekleri için genel bir sorun izleyicisi (yalnızca İngilizce).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">Takılıp soru sormak için bir yer.</string>
|
||||
<string name="changelog_label">Değişiklik Günlüğü</string>
|
||||
<string name="settings_label">Ayarlar</string>
|
||||
@@ -73,4 +74,6 @@
|
||||
<string name="settings_compatibility_mode_description">Uyumluluğu artırmak için iyileştirmeleri devre dışı bırakın. Herhangi bir veri görmüyorsanız bunu deneyin.</string>
|
||||
<string name="help_translate_label">Tercüme</string>
|
||||
<string name="help_translate_description">Bu uygulamayı en sevdiğiniz dile çevirmeye yardımcı olun.</string>
|
||||
<string name="translators_thanks_title">Çevirmenler</string>
|
||||
<string name="translators_thanks_description">karanlık</string>
|
||||
</resources>
|
||||
|
||||
@@ -102,4 +102,19 @@
|
||||
<string name="widget_description">A widget showing the last known device status.</string>
|
||||
<string name="settings_compat_indirectcallback_title">Indirect data delivery</string>
|
||||
<string name="settings_compat_indirectcallback_summary">Use an alternative method to receive BLE data from the system (broadcast instead of callback).</string>
|
||||
|
||||
<string name="troubleshooter_title">Troubleshooter</string>
|
||||
<string name="troubleshooter_ble_intro_body1">AirPods broadcast their status information using a BLE technology called \"advertisements\". Some devices don\'t implement this technology correctly. This process will try different compatibility settings to fix the issue on your device. Setup your headphones to play music and place them close to your phone while this process is running.</string>
|
||||
<string name="troubleshooter_ble_intro_title">Bluetooth Low Energy Broadcasts</string>
|
||||
<string name="troubleshooter_ble_intro_start_action">Start investigating</string>
|
||||
<string name="troubleshooter_ble_process_title">Troubleshoot in progress</string>
|
||||
<string name="troubleshooter_ble_process_subtile">Each step takes 5–10 seconds</string>
|
||||
<string name="troubleshooter_ble_result_success_title">Success</string>
|
||||
<string name="troubleshooter_ble_result_success_body">BLE advertisement broadcasts are being received by CAPod.</string>
|
||||
<string name="troubleshooter_ble_result_failure_title">Unsuccessful</string>
|
||||
<string name="troubleshooter_ble_result_failure_body">No combination of compatibility options helped.</string>
|
||||
<string name="troubleshooter_ble_result_failure_phone_body">Your phone didn\'t receive any BLE data at all. You can retry this test in a crowded area to see if data sources (other than your headphones) can be received. No data being received points towards an issue with your phones operating system.</string>
|
||||
<string name="troubleshooter_ble_result_failure_phone_headphones">Your phone received BLE data, but the data does not come from any supported device. Are your headphones powered on? Does CAPod support your headphone?</string>
|
||||
<string name="troubleshooter_ble_result_failure_action">Discuss on Discord</string>
|
||||
<string name="troubleshoot_action">Troubleshoot</string>
|
||||
</resources>
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.2.2")
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
||||
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
||||
|
||||
@@ -8,7 +8,7 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
implementation("com.android.tools.build:gradle:7.2.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
|
||||
implementation("com.android.tools.build:gradle:7.3.1")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
|
||||
implementation("com.squareup:javapoet:1.13.0")
|
||||
}
|
||||
@@ -84,14 +84,14 @@ fun DependencyHandlerScope.addBaseWorkManager() {
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.addBaseAndroid() {
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
implementation("androidx.core:core-ktx:1.10.0-rc01")
|
||||
implementation("androidx.annotation:annotation:1.4.0")
|
||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.addBaseAndroidUi() {
|
||||
implementation("androidx.appcompat:appcompat:1.6.0-alpha04")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ object Versions {
|
||||
}
|
||||
|
||||
object Kotlin {
|
||||
const val core = "1.7.10"
|
||||
const val core = "1.8.0"
|
||||
const val coroutines = "1.6.2"
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ object Versions {
|
||||
}
|
||||
|
||||
object Dagger {
|
||||
const val core = "2.42"
|
||||
const val core = "2.45"
|
||||
}
|
||||
|
||||
object Moshi {
|
||||
const val core = "1.13.0"
|
||||
const val core = "1.14.0"
|
||||
}
|
||||
|
||||
object AndroidX {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -1,3 +1,2 @@
|
||||
Now with more widgets!
|
||||
But also bugfixes and performance improvements.
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -10,6 +10,9 @@ Features:
|
||||
* Automatically connect phone and AirPods.
|
||||
* Show popup when case is opened.
|
||||
|
||||
CAPod has a Wear-OS version that shows the status of the pod device that is closest to you.
|
||||
CAPod for Wear-OS is a stand-alone app and does not require a phone.
|
||||
|
||||
CAPod is ad-free. Some features require an in-app purchase.
|
||||
|
||||
Most popular AirPods and Beats devices are supported.
|
||||
|
||||
@@ -1 +1 @@
|
||||
A CAPod az AirPods kísérőalkalmazása
|
||||
Az AirPods kísérőalkalmazása
|
||||
@@ -1 +1 @@
|
||||
CAPod - Companion app per le Airpods
|
||||
Companion app per le Airpods
|
||||
@@ -1 +1 @@
|
||||
CAPod - Complemento para AirPods
|
||||
CAPod - Para AirPods
|
||||
@@ -1,18 +1,18 @@
|
||||
CAPod is a companion app for AirPods.
|
||||
CAPod é uma aplicação complementar aos AirPods.
|
||||
|
||||
Features:
|
||||
Funcionalidades:
|
||||
|
||||
* Battery level for pods and cases.
|
||||
* Charging status for pods and case.
|
||||
* Additional infos about connection, microphone and case.
|
||||
* Can receive and show all nearby devices.
|
||||
* Ear detection with automatic play/pause.
|
||||
* Automatically connect phone and AirPods.
|
||||
* Show popup when case is opened.
|
||||
* Nível de bateria para os pods e a caixa.
|
||||
* Estado de carregamento para os pods e a caixa.
|
||||
* Informações adicionais sobre a conexão, o microfone e a caixa.
|
||||
* Pode detetar e mostrar todos os dispositivos por perto.
|
||||
* Deteção do ouvido com reprodução/pausa automática.
|
||||
* Automaticamente conectar telemóvel e AirPods.
|
||||
* Mostrar pop-up quando a caixa é aberta.
|
||||
|
||||
CAPod is ad-free. Some features require an in-app purchase.
|
||||
CAPods é livre de anúncios. Algumas funcionalidades exigem uma compra na aplicação.
|
||||
|
||||
Most popular AirPods and Beats devices are supported.
|
||||
If your device is similar to AirPods but not yet supported, send me a short mail.
|
||||
Os dispositivos AirPods e Beats mais populares são suportados.
|
||||
Se o seu dispositivo é semelhante aos AirPods, mas ainda não é suportado, envie-me um pequeno email.
|
||||
|
||||
Got a cool idea for a new feature? Reach out!
|
||||
Tem alguma ideia fixe para uma nova funcionalidade? Contacte-me!
|
||||
@@ -1 +1 @@
|
||||
CAPod is a companion app for AirPods on Android.
|
||||
CAPod é uma aplicação complementar aos AirPods no Android.
|
||||
@@ -1 +1 @@
|
||||
CAPod - Companion for AirPods
|
||||
CAPod - Para AirPods
|
||||
@@ -1 +1 @@
|
||||
CAPod - Bạn đồng hành cho AirPods
|
||||
Bạn đồng hành cho AirPods
|
||||
+20
@@ -360,6 +360,25 @@ do-versionfile() {
|
||||
git add VERSION
|
||||
}
|
||||
|
||||
# Update a version file that can be parsed by third-parties, e.g. F-Droid
|
||||
do-fastlane-changelog() {
|
||||
CHANGELOGS_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||
NEW_CHANGELOG="$CHANGELOGS_DIR/$V_CODE.txt"
|
||||
|
||||
[ -f "$NEW_CHANGELOG" ] && return
|
||||
|
||||
echo -e "\n${S_NOTICE}Creating new default fastlane changelog file for $V_CODE...\n"
|
||||
|
||||
cp "$CHANGELOGS_DIR/default.txt" "$NEW_CHANGELOG"
|
||||
|
||||
cat "$NEW_CHANGELOG"
|
||||
|
||||
echo -e "\n\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}$NEW_CHANGELOG${S_NOTICE}] file"
|
||||
|
||||
# Stage file for commit
|
||||
git add "$CHANGELOGS_DIR/$V_CODE.txt"
|
||||
}
|
||||
|
||||
# Does the release branch already exist?
|
||||
check-branch-exist() {
|
||||
[ "$FLAG_NOBRANCH" = true ] && return
|
||||
@@ -448,6 +467,7 @@ echo -e "\n${S_LIGHT}––––––"
|
||||
# Update steps
|
||||
do-version-properties
|
||||
do-versionfile
|
||||
do-fastlane-changelog
|
||||
do-branch
|
||||
do-commit
|
||||
create-tag "${V_NAME}" "${REL_NOTE}"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=8
|
||||
project.versioning.minor=9
|
||||
project.versioning.patch=0
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
|
||||
Reference in New Issue
Block a user