Compare commits

...
26 Commits
Author SHA1 Message Date
darken 63f37e51ad Release: 2.2.1-rc1 2022-10-27 17:40:57 +02:00
darken 01ea08a5cb Update release script 2022-10-27 17:38:50 +02:00
darken bddb20c5bb More version plumping 2022-10-27 17:38:50 +02:00
darken f9a97297da Add a release script base.
Based on https://gist.github.com/jv-k/703e79306554c26a65a7cfdb9ca119c6

WIP
2022-10-27 17:38:50 +02:00
darken 5fc2b56635 Update translations 2022-10-27 08:09:49 +02:00
darken 172953ccb3 Fix monitor mode being set to ALWAYS when visiting the reactions settings screen.
Fixes #41
2022-10-27 07:59:21 +02:00
darken d55300f53d Fix bold highlighting of the main pod device not being reset. 2022-10-22 10:49:45 +03:00
darken c3d4500e3a Version bump (v2.2.1-rc0) 2022-09-24 15:50:25 +02:00
darken cf72e28727 Fix app crash if Bluetooth adapter is null and we can't retrieve the list of bonded devices in settings. 2022-09-24 15:49:56 +02:00
darken b488a1be16 Version bump (v2.2.0-rc0) 2022-09-24 12:00:21 +02:00
darken 805d0cb4b6 Update translations 2022-09-24 11:59:58 +02:00
darken fa045c2504 Increase worker scheduling flex interval for WearOS. 2022-09-24 11:59:09 +02:00
Matthias Urhahn 23fd373a9b AirPods Pro 2 (#32)
* Preliminary support for AirPods Pro 2, based on #31, assuming identifier is `11 20`

* Fix AirPods Pro 2 identifier (it's 0x1420)

* Add second AirPods Pro 2 test case from reddit user.

* Update Readme with AirPods Pro 2
2022-09-24 11:51:53 +02:00
darken ab619cb073 Version bump (2.1.1-rc0) 2022-09-19 22:26:43 +02:00
darken 2d96822014 Add request for POST_NOTIFICATIONS on Android 13.
Closes #29
2022-09-19 22:26:43 +02:00
darken 887adc87de Reduce background monitor time to reduce battery usage. 2022-09-16 09:31:18 +02:00
darken 4219bd02c5 Setup fastlane for uploading app and wearos AAB 2022-09-14 21:40:13 +02:00
darken 48544464ae Version bump 2022-09-14 21:19:07 +02:00
darken 9b4f2477e4 Add sponsor link to settings toolbar 2022-09-14 21:15:21 +02:00
darken 26e8840484 On Foss upgrade navigate to gituhb sponsors directly 2022-09-14 21:06:09 +02:00
darken b6ef6d3ce9 Fix upgrade check 2022-09-14 21:00:15 +02:00
darken cdfcd24049 Ensure that BLEScanner only starts when all necessary permissions are granted.
(Or rather that scanner is restarted when permissions change).
+ some refactoring to improve readability
2022-09-14 20:51:39 +02:00
darken 95a8e0b125 Version bump 2022-09-14 19:55:56 +02:00
darken 84b7509f33 Adjust versioning for GPlay wear os bundle upload 2022-09-14 19:39:30 +02:00
darken a53baec9dc Version bump 2022-09-14 18:59:08 +02:00
darken 4b4bc86cf7 Fix Github release for WearOS APK 2022-09-14 18:31:34 +02:00
80 changed files with 1081 additions and 227 deletions
+6 -2
View File
@@ -68,7 +68,9 @@ jobs:
tag_name: ${{ steps.tagger.outputs.tag }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: app/build/outputs/apk/foss/beta/*.apk
files: |
app/build/outputs/apk/foss/beta/*.apk
app-wear/build/outputs/apk/foss/beta/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -80,7 +82,9 @@ jobs:
tag_name: ${{ steps.tagger.outputs.tag }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: app/build/outputs/apk/foss/release/*.apk
files: |
app/build/outputs/apk/foss/release/*.apk
app-wear/build/outputs/apk/foss/release/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -24,7 +24,8 @@ Currently supported models:
* AirPods 1. Generation
* AirPods 2. Generation
* AirPods 3. Generation
* AirPods Pro
* AirPods Pro 1. Generation
* AirPods Pro 2. Generation
* AirPods Max
* Power Beats Pro
* Power Beats 3
+1
View File
@@ -0,0 +1 @@
2.2.1-rc1 20201010
@@ -35,7 +35,7 @@ class BleScanner @Inject constructor(
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
val adapter = bluetoothManager.adapter
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
@@ -45,7 +45,7 @@ class BleScanner @Inject constructor(
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
} && !compatMode
val scanner = bluetoothManager.scanner
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
val callback = object : ScanCallback() {
var lastScanAt = System.currentTimeMillis()
@@ -30,12 +30,11 @@ class BluetoothManager2 @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
) {
val adapter: BluetoothAdapter
val adapter: BluetoothAdapter?
get() = manager.adapter
val scanner: BluetoothLeScanner
get() = adapter.bluetoothLeScanner
?: throw IllegalStateException("Bluetooth is disabled or permissiong missing")
val scanner: BluetoothLeScanner?
get() = adapter?.bluetoothLeScanner
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
send(manager.adapter?.isEnabled ?: false)
@@ -158,7 +157,9 @@ class BluetoothManager2 @Inject constructor(
}
}
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
fun bondedDevices(): Flow<Set<BluetoothDevice>> = flow {
emit(adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable"))
}
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
try {
@@ -71,7 +71,13 @@ enum class Permission(
isGranted = {
android.provider.Settings.canDrawOverlays(it)
},
)
),
POST_NOTIFICATIONS(
minApiLevel = Build.VERSION_CODES.S,
labelRes = R.string.permission_post_notifications_label,
descriptionRes = R.string.permission_post_notifications_description,
permissionId = "android.permission.POST_NOTIFICATIONS",
),
}
fun Permission.isRequired(context: Context): Boolean = when {
@@ -9,6 +9,8 @@ interface UpgradeRepo {
fun launchBillingFlow(activity: Activity)
fun getSponsorUrl(): String? = null
interface Info {
val type: Type
@@ -4,7 +4,6 @@ import android.bluetooth.le.ScanFilter
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BleScanner
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
@@ -14,6 +13,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.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.PodFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
@@ -36,14 +36,103 @@ class PodMonitor @Inject constructor(
private val bluetoothManager: BluetoothManager2,
private val debugSettings: DebugSettings,
private val podDeviceCache: PodDeviceCache,
private val permissionTool: PermissionTool,
) {
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
private val cacheLock = Mutex()
private suspend fun List<BleScanResult>.preFilterAndMap(
scannerMode: ScannerMode
): List<PodFactory.Result> = this
val devices: Flow<List<PodDevice>> = combine(
permissionTool.missingPermissions,
bluetoothManager.isBluetoothEnabled
) { missingPermissions, isBluetoothEnabled ->
log(TAG) { "devices: missingPermissions=$missingPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
// We just want to retrigger if permissions change.
isBluetoothEnabled
}
.flatMapLatest { isReady ->
if (!isReady) {
log(TAG, WARN) { "Bluetooth is not ready" }
flowOf(null)
} else {
createBleScanner()
}
}
.map { newPods ->
val pods = processWithCache(newPods)
val presorted = sortPodsToInterest(pods.values)
val main = determineMainDevice(presorted)
newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let {
podDeviceCache.saveMainDevice(it.scanResult)
}
presorted.sortedByDescending { it == main }
}
.retryWhen { cause, attempt ->
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
delay(3000)
true
}
.onStart { emit(emptyList()) }
.replayingShare(appScope)
val mainDevice: Flow<PodDevice?> = devices
.map { determineMainDevice(it) }
.replayingShare(appScope)
private fun createBleScanner() = combine(
generalSettings.scannerMode.flow,
generalSettings.compatibilityMode.flow,
debugSettings.showUnfiltered.flow
) { scannerMode, compatMode, unfiltered ->
Triple(scannerMode, compatMode, unfiltered)
}
.flatMapLatest { (mode, compat, unfiltered) ->
val filters = when {
unfiltered -> {
log(TAG, WARN) { "Using unfiltered scan mode" }
setOf(getUnfilteredFilter())
}
else -> ProximityPairing.getBleScanFilter()
}
bleScanner.scan(
filters = filters,
scannerMode = mode,
compatMode = compat,
).map { preFilterAndMap(it) }
}
private suspend fun processWithCache(
newPods: List<PodFactory.Result>?
): Map<PodDevice.Id, PodDevice> = cacheLock.withLock {
if (newPods == null) {
log(TAG) { "Null result, Bluetooth is disabled." }
deviceCache.clear()
return emptyMap()
}
val now = Instant.now()
deviceCache.toList().forEach { (key, value) ->
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
deviceCache.remove(key)
}
}
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
pods.putAll(deviceCache)
newPods.map { it.device }.forEach {
deviceCache[it.identifier] = it
pods[it.identifier] = it
}
return pods
}
private suspend fun preFilterAndMap(rawResults: List<BleScanResult>): List<PodFactory.Result> = rawResults
.groupBy { it.address }
.values
.map { sameAdrDevs ->
@@ -56,115 +145,36 @@ class PodMonitor @Inject constructor(
}
.mapNotNull { podFactory.createPod(it) }
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
.flatMapLatest { isBluetoothEnabled ->
if (isBluetoothEnabled) {
log(TAG) { "Bluetooth is enabled" }
combine(
generalSettings.scannerMode.flow,
generalSettings.compatibilityMode.flow,
debugSettings.showUnfiltered.flow
) { scannerMode, compatMode, unfiltered ->
Triple(scannerMode, compatMode, unfiltered)
}.flatMapLatest { (mode, compat, unfiltered) ->
log(TAG, VERBOSE) { "Starting BLEScanner mode=$mode, compat=$compat, unfiltered=$unfiltered" }
val filters = if (unfiltered) {
setOf(getUnfilteredFilter())
} else {
ProximityPairing.getBleScanFilter()
}
bleScanner.scan(
filters = filters,
scannerMode = mode,
compatMode = compat,
).map { it.preFilterAndMap(mode) }
}
} else {
log(TAG, WARN) { "Bluetooth is currently disabled" }
flowOf(null)
}
}
.map { newPods ->
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
cacheLock.withLock {
if (newPods == null) {
log(TAG) { "Null result, Bluetooth is disabled." }
deviceCache.clear()
return@map emptyList()
}
val now = Instant.now()
deviceCache.toList().forEach { (key, value) ->
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
deviceCache.remove(key)
}
}
pods.putAll(deviceCache)
newPods.map { it.device }.forEach {
deviceCache[it.identifier] = it
pods[it.identifier] = it
}
}
val presorted = pods.values.sortPodsToInterest()
val main = presorted.determineMainDevice()
newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let {
podDeviceCache.saveMainDevice(it.scanResult)
}
presorted.sortedByDescending { it == main }
}
.onStart { emit(emptyList()) }
.retryWhen { cause, attempt ->
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
delay(3000)
true
}
.replayingShare(appScope)
val mainDevice: Flow<PodDevice?>
get() = devices
.map { it.determineMainDevice() }
.replayingShare(appScope)
private fun Collection<PodDevice>.sortPodsToInterest(): List<PodDevice> = this.let { devices ->
private fun sortPodsToInterest(pods: Collection<PodDevice>): List<PodDevice> {
val now = Instant.now()
return@let devices.sortedWith(
return pods.sortedWith(
compareByDescending<PodDevice> { true }
.thenBy {
val age = Duration.between(it.seenLastAt, now).seconds
if (age < 5) 0L else (age / 3L).toLong()
if (age < 5) 0L else (age / 3L)
}
.thenByDescending { it.signalQuality }
.thenByDescending { (it.seenCounter / 10) }
)
}
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this
.sortPodsToInterest()
.let { devices ->
val mainDeviceModel = generalSettings.mainDeviceModel.value
private fun determineMainDevice(pods: List<PodDevice>): PodDevice? {
val mainDeviceModel = generalSettings.mainDeviceModel.value
val presorted = devices.sortedByDescending {
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
}
return@let presorted.firstOrNull()?.let { candidate ->
when {
candidate.model == PodDevice.Model.UNKNOWN -> null
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
else -> candidate
}
}
val presorted = sortPodsToInterest(pods).sortedByDescending {
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
}
return presorted.firstOrNull()?.let { candidate ->
when {
candidate.model == PodDevice.Model.UNKNOWN -> null
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
else -> candidate
}
}
}
private fun getUnfilteredFilter(): ScanFilter {
return ScanFilter.Builder().build()
@@ -78,6 +78,10 @@ interface PodDevice {
"AirPods Pro",
R.drawable.ic_device_airpods_gen2
),
@Json(name = "airpods.pro2") AIRPODS_PRO2(
"AirPods Pro 2",
R.drawable.ic_device_airpods_gen2
),
@Json(name = "airpods.max") AIRPODS_MAX(
"AirPods Max",
R.drawable.ic_device_generic_headphones
@@ -19,6 +19,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
@@ -83,7 +83,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
if (definitive.contains(basic.caseLidState)) return null
return history
.filterIsInstance<AirPodsPro>()
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
?.caseLidState
}
@@ -62,9 +62,10 @@ data class AirPodsPro(
)
}
companion object {
private val DEVICE_CODE = 0x0e20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro", "Factory")
}
}
companion object {
private val DEVICE_CODE = 0x0e20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro")
}
}
@@ -0,0 +1,71 @@
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.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
data class AirPodsPro2(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null
) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsPro2(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x1420.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro2")
}
}
@@ -46,7 +46,8 @@ class AutoConnect @Inject constructor(
return@map
}
val bondedDevice = bluetoothManager.bondedDevices().firstOrNull { it.address == mainDeviceAddr }
val bondedDevice = bluetoothManager.bondedDevices().first().firstOrNull { it.address == mainDeviceAddr }
if (bondedDevice == null) {
log(TAG, WARN) { "No bonded device matches $mainDeviceAddr" }
return@map
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Н</string>
<string name="general_error_label">Памылка</string>
<string name="general_grant_permission_action">Даць дазвол</string>
@@ -11,6 +14,7 @@
<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 для злучэння са спалучанымі прыладамі.</string>
<string name="permission_access_fine_location_label">Доступ да дакладнага месцазнаходжання</string>
<string name="permission_access_fine_location_description">CAPod выкарыстоўвае дазвол \"Дакладнае месцазнаходжанне\" для атрымання даных Bluetooth Low Energy. Вашы навушнікі з дапамогай тэхналогіі Bluetooth Low Energy перадаюць інфармацыю пра свой стан. Гэта праграма НЕ будзе карыстацца данымі Bluetooth для вызначэння вашага месцазнаходжання.</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">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">Chyba</string>
<string name="general_grant_permission_action">Udělit oprávnění</string>
<string name="overview_nomaindevice_label">Žádné hlavní zařízení</string>
@@ -10,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Tato aplikace vyžaduje oprávnění „připojení Bluetooth“ pro interakci se spárovanými zařízeními a zahájení připojení.</string>
<string name="permission_bluetooth_scan_label">Skenování Bluetooth</string>
<string name="permission_bluetooth_scan_description">Oprávnění „Skenování Bluetooth“ umožňuje této aplikaci zjišťovat a přijímat data z okolních Bluetooth zařízení, jako jsou například sluchátka AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Tato aplikace vyžaduje oprávněni k „Bluetooth“ pro připojení ke spárovaným zařízením Bluetooth.</string>
<string name="permission_access_fine_location_label">Přístup k přesné poloze</string>
<string name="permission_access_fine_location_description">CAPod používá pro příjem dat Bluetooth Low Energy oprávnění k „přesné poloze“. Sluchátka používají k vysílání svého stavu technologii Bluetooth Low Energy. Tato aplikace NEPOUŽÍVÁ data Bluetooth k určování vaší polohy.</string>
@@ -19,7 +24,7 @@
<string name="permission_ignore_battery_optimizations_description">Optimalizace baterie zabraňuje této aplikaci spolehlivě přijímat data Bluetooth, pokud běží na pozadí.</string>
<string name="permission_required_title">Je vyžadováno následující oprávnění:</string>
<string name="permission_system_alert_window_label">Okno systémových upozornění</string>
<string name="permission_system_alert_window_description">Aby bylo možné použít unkci „Zobrazit vyskakovací okno“, je nutné umožnot aplikaci CAPod vykreslení přes jiné aplikace.</string>
<string name="permission_system_alert_window_description">Aby bylo možné použít unkci „Zobrazit vyskakovací okno“, je nutné umožnit aplikaci CAPod vykreslení přes jiné aplikace.</string>
<string name="settings_scanner_mode_lowpower_label">Nízký výkon</string>
<string name="settings_scanner_mode_balanced_label">Vyvážený</string>
<string name="settings_scanner_mode_lowlatency_label">Nízká latence</string>
@@ -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">k.A.</string>
<string name="general_error_label">Fehler</string>
<string name="general_grant_permission_action">Berechtigung erteilen</string>
@@ -11,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Diese App benötigt die \"Bluetooth verbinden\" Berechtigung um mit bereits verbundenen und neuen Geräten zu interagieren.</string>
<string name="permission_bluetooth_scan_label">Bluetooth Suche</string>
<string name="permission_bluetooth_scan_description">Die \"Bluetooth verbinden\" Berechtigung erlaubt dieser App Bluetooth Daten von Geräten wie AirPods zu finden und zu erhalten.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Diese App benötigt die \"Bluetooth\" Berechtigung um sich mit gekoppelten Geräten zu verbinden.</string>
<string name="permission_access_fine_location_label">Auf genauen Standort zugreifen</string>
<string name="permission_access_fine_location_description">CAPod benutzt die \"genauen Gerätestandort verwenden\" Berechtigung um Bluetooth Low Energy Daten zu erhalten. Ihre Kopfhörer nutzen Bluetooth Low Energy um ihren Batterie Status zu übertragen. Diese App greift nicht auf ihren Standort zu.</string>
@@ -11,6 +11,7 @@
<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. Τα ακουστικά σας χρησιμοποιούν τεχνολογία χαμηλής ενέργειας Bluetooth για να μεταδίδουν την κατάστασή τους. Αυτή η εφαρμογή ΔΕΝ θα χρησιμοποιεί δεδομένα Bluetooth για να προσδιορίσει την τοποθεσία σας.</string>
@@ -1,2 +1,10 @@
<?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/D</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="overview_nomaindevice_label">Ningún dispositivo principal</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 (libre)</string>
<string name="general_value_not_available_label">Non disponible</string>
<string name="general_error_label">Erreur</string>
<string name="general_grant_permission_action">Accorder lautorisation</string>
@@ -11,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Cette appli exige lautorisation « Connexion Bluetooth » pour interagir avec les appareils jumelés et démarrer les connexions.</string>
<string name="permission_bluetooth_scan_label">Analyse Bluetooth</string>
<string name="permission_bluetooth_scan_description">Lautorisation « Analyse Bluetooth » permet à lappli de découvrir et de réceptionner les données Bluetooth des appareils à proximité tels que vos AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Cette appli exige lautorisation « Bluetooth » pour se connecter aux appareils Bluetooth jumelés.</string>
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
<string name="permission_access_fine_location_description">CAPod utilise lautorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli nutilisera PAS les données Bluetooth pour déterminer votre position.</string>
@@ -48,6 +52,7 @@
<string name="pods_none_label_short">Aucun appareil</string>
<string name="pods_charging_label">En charge</string>
<string name="pods_inear_label">Dans loreille</string>
<string name="pods_microphone_label">Microphone</string>
<string name="pods_yours">Les vôtres</string>
<string name="headset_being_worn_label">Porté</string>
<string name="pods_case_unknown_state">État inconnu</string>
@@ -1,4 +1,54 @@
<?xml version="1.0" encoding="utf-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="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">ऐप बंद होने पर \"शो पॉपअप\" और \"ऑटोकनेक्ट\" जैसी सुविधाओं को सक्षम करने के लिए कैपोड \"पृष्ठभूमि स्थान पहुंच\" का उपयोग करता है। बैकग्राउंड लोकेशन एक्सेस ऐप को ब्लूटूथ लो एनर्जी डेटा w . प्राप्त करने की अनुमति देता है</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>
</resources>
@@ -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">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">Errore</string>
<string name="general_grant_permission_action">Concedi permessi</string>
<string name="overview_nomaindevice_label">Nessun device primario</string>
@@ -10,7 +14,9 @@
<string name="permission_bluetooth_connect_description">Questa applicazione richiede il permesso di \"Connesione bluetooth\" per interagire con i dispotivi collegati e iniziare le connesioni.</string>
<string name="permission_bluetooth_scan_label">Scansione bluetooth</string>
<string name="permission_bluetooth_scan_description">Il permesso di \"Scansione bluetooth\" permette a questa app di trovare e ricevere dati Bluetooth da dispositivi vicini come le tue Airpods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Questa app richiede il permesso \"Bluetooth\" per connettersi ai dispoitivi Bluetooth.</string>
<string name="permission_access_fine_location_label">Accedi alla posizione precisa</string>
<string name="permission_access_fine_location_description">CAPod usa i permessi di Geolocalizzazione per ricevere dati Bluetooth a basso consumo. Le tue cuffie suano la tecnologia Bluetooth a basso consumo per inviare al dispositivo al quale sono connesse il loro stato. Questa app non utilizza i dati Bluetooth per geolocalizzarti.</string>
<string name="permission_background_location_label">Accesso in background alla tua posizione</string>
<string name="permission_background_location_description">CAPod usa l\'accesso in background alla tua posizione per far funzionare opzioni come \"Mostra popup\" e \"AutoConnect\" mentre l\'applicazione é chiusa. L\'accesso in background abiliterà l\'applicazione nella ricezione di dati Bluetooth a basso consume mentre l\'app non é aperta. Questa applicazione non utilizza i dati Bluetooth per geolocalizzarti.</string>
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">該当なし</string>
<string name="general_error_label">エラー</string>
<string name="general_grant_permission_action">許可する</string>
@@ -10,16 +13,17 @@
<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 スキャン」権限により、このアプリはAirPodsなどの近くのデバイスからBluetoothのデータを検出して受信することができます。</string>
<string name="permission_bluetooth_scan_description">「Bluetoothスキャン」権限により、このアプリはAirPodsなどの近くのデバイスからBluetoothのデータを検出して受信することができます。</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テクノロジーを使用してデバイスの状態を送信します。このアプリは、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_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="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>
@@ -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">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">오류</string>
<string name="general_grant_permission_action">권한 부여</string>
<string name="overview_nomaindevice_label">내 기기 없음</string>
@@ -4,8 +4,8 @@
<string name="general_grant_permission_action">Berikan kebenaran</string>
<string name="overview_nomaindevice_label">Tiada peranti utama</string>
<string name="overview_nomaindevice_description">Semua peranti yang dikesan tidak mungkin menjadi milik anda. Hidupkan dan sambung peranti anda atau laraskan tetapan.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth dinyahdayakan</string>
<string name="overview_bluetooth_disabled_description">Bluetooth dinyahdayakan, dayakan ia ;)</string>
<string name="overview_bluetooth_disabled_label">Bluetooth dilumpuhkan</string>
<string name="overview_bluetooth_disabled_description">Bluetooth dilumpuhkan, dayakan ia ;)</string>
<string name="permission_bluetooth_connect_label">Sambung bluetooth</string>
<string name="permission_bluetooth_connect_description">Apl ini memerlukan kebenaran \"Bluetooth connect\" untuk berinteraksi dengan pasangan peranti dan memulakan sambungan.</string>
<string name="permission_bluetooth_scan_label">Pengimbasan bluetooth</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">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">Fout</string>
<string name="general_grant_permission_action">Toestemming geven</string>
<string name="overview_nomaindevice_label">Geen primair apparaat</string>
@@ -10,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Deze app heeft de toestemming \"Bluetooth verbinding\" nodig om te communiceren met gekoppelde apparaten en verbindingen tot stand te brengen.</string>
<string name="permission_bluetooth_scan_label">Bluetooth scannen</string>
<string name="permission_bluetooth_scan_description">Met de machtiging \'Bluetooth-scannen\' kan deze app Bluetooth-gegevens van apparaten in de buurt, zoals uw AirPods, ontdekken en ontvangen.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Deze app heeft de toestemming van \"Bluetooth\" nodig om verbinding te maken met gekoppelde Bluetooth-apparaten.</string>
<string name="permission_access_fine_location_label">Toegang prima locatie</string>
<string name="permission_access_fine_location_description">CAPod gebruikt de \"prima locatie\"-toestemming om Bluetooth Low Energy-gegevens te ontvangen. Uw hoofdtelefoon gebruikt Bluetooth Low Energy-technologie om hun status uit te zenden. Deze app gebruikt GEEN Bluetooth-gegevens om uw locatie te bepalen.</string>
@@ -31,7 +36,8 @@
<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>
<string name="pods_case_label">Geval</string>
<string name="pods_case_label">Oplaadcase</string>
<string name="pods_case_status_open_label">Open</string>
<string name="pods_case_status_closed_label">Gesloten</string>
<string name="pods_connection_state_disconnected_label">Niet aangesloten op een apparaat</string>
<string name="pods_connection_state_idle_label">Aangesloten op een apparaat, maar inactief</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">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">Feil</string>
<string name="general_grant_permission_action">Gi tillatelse</string>
<string name="overview_nomaindevice_label">Ingen primær enhet</string>
@@ -10,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Denne appen må ha \"Bluetooth-tilkobling\"-tillatelsen for å kunne snakke med sammenkoblede enheter og starte tilkoblinger.</string>
<string name="permission_bluetooth_scan_label">Bluetooth-skanning</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth-skanning\"-tillatelsen lar denne appen oppdage og motta Bluetooth-data fra enheter i nærheten, som for eksempel AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Denne appen må ha \"Bluetooth\"-tillatelsen for å koble til sammenkoblede Bluetooth-enheter.</string>
<string name="permission_access_fine_location_label">Forbedret posisjonsnøyaktighet</string>
<string name="permission_access_fine_location_description">CAPod bruker tillatelsen \"Forbedret posisjonsnøyaktighet\" for å motta Bluetooth Low Energy-data. Hodetelefonene dine bruker Bluetooth Low Energy-teknologi for å kringkaste statusen. Denne appen bruker IKKE Bluetooth-data til å se posisjonen din.</string>
@@ -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">N/D</string>
<string name="general_error_label">Błąd</string>
<string name="general_grant_permission_action">Udziel uprawnień</string>
@@ -11,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Aplikacja wymaga uprawnień do \"Połączenia Bluetooth\", aby oddziaływać na sparowane urządzenia i inicjować połączenia.</string>
<string name="permission_bluetooth_scan_label">Skanowanie Bluetooth</string>
<string name="permission_bluetooth_scan_description">Uprawnienie \"Skanowanie Bluetooth\" zezwala aplikacji na odkrywanie i odbieranie danych Bluetooth od urządzeń znajdujących się w pobliżu, takich jak AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Aplikacja wymaga uprawnień \"Bluetooth\", aby łączyć się ze sparowanymi urządzeniami bluetooth.</string>
<string name="permission_access_fine_location_label">Dostęp do dokładnej lokalizacji</string>
<string name="permission_access_fine_location_description">CAPod korzysta z uprawnienia do \"dokładnej lokalizacji\", aby odbierać dane Bluetooth Low Energy. Twoje słuchawki korzystają z technologii Bluetooth Low Energy, aby przekazywać informacje o swoim stanie. Aplikacja NIE BĘDZIE korzystać z danych Bluetooth do ustalenia twojego położenia.</string>
@@ -22,7 +26,7 @@
<string name="permission_system_alert_window_label">Okna alertów systemowych</string>
<string name="permission_system_alert_window_description">Zezwól na wyświetlanie CAPod nad innymi aplikacjami, aby aktywować funkcję \"Wyświetlanie informacji\".</string>
<string name="settings_scanner_mode_lowpower_label">Oszczędzanie energii</string>
<string name="settings_scanner_mode_balanced_label">Zabalansowana</string>
<string name="settings_scanner_mode_balanced_label">Zbalansowana</string>
<string name="settings_scanner_mode_lowlatency_label">Małe opóźnienia</string>
<string name="settings_monitor_mode_manual_label">Gdy aplikacja jest uruchomiona</string>
<string name="settings_monitor_mode_automatic_label">Gdy urządzenie jest podłączone</string>
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Недоступно</string>
<string name="general_error_label">Ошибка</string>
<string name="general_grant_permission_action">Предоставить разрешение</string>
@@ -11,6 +14,7 @@
<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\" для подключения к сопряженным устройствам.</string>
<string name="permission_access_fine_location_label">Доступ к точному местоположению</string>
<string name="permission_access_fine_location_description">CAPod необходимо разрешение \"Точное местоположение\" для получения данных Bluetooth Low Energy. Ваши наушники задействуют технологию Bluetooth Low Energy (Bluetooth с низким энергопотреблением) для передачи своего состояния. Это приложение НЕ будет использовать данные Bluetooth для определения Вашего местоположения.</string>
@@ -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">N/D</string>
<string name="general_error_label">Chyba</string>
<string name="general_grant_permission_action">Udeliť povolenie</string>
@@ -11,6 +14,7 @@
<string name="permission_bluetooth_connect_description">Táto aplikácia vyžaduje povolenie „Bluetooth pripojenie“ na interakciu so spárovanými zariadeniami a iniciovanie pripojení.</string>
<string name="permission_bluetooth_scan_label">Bluetooth skenovanie</string>
<string name="permission_bluetooth_scan_description">Povolenie „Bluetooth skenovanie“ umožňuje tejto aplikácii zisťovať a prijímať údaje z okolitých zariadení, ako sú vaše AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Táto aplikácia vyžaduje povolenie „Bluetooth“ na pripojenie k spárovaným bluetooth zariadeniam.</string>
<string name="permission_access_fine_location_label">Prístup k presnej polohe</string>
<string name="permission_access_fine_location_description">CAPod používa oprávnenie „presná poloha“ na prijímanie údajov Bluetooth Low Energy. Vaše slúchadlá využívajú technológiu Bluetooth Low Energy na vysielanie svojho stavu. Táto aplikácia NEBUDE používať údaje Bluetooth na určenie vašej polohy.</string>
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">不可用</string>
<string name="general_error_label">錯誤</string>
<string name="general_grant_permission_action">授予權限</string>
@@ -65,4 +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>
</resources>
@@ -0,0 +1,80 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class AirPodsPro2Test : BaseAirPodsTest() {
/**
* https://github.com/d4rken-org/capod/issues/31#issuecomment-1256791084
*/
@Test
fun `test AirPods Pro 2 - unknown setup from #31`() = runBlockingTest {
create<AirPodsPro2>("07 19 01 14 20 55 88 F9 51 00 04 20 50 03 CA D5 C9 AC 0F FA 84 78 94 5A 4D DF F5") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1420.toUShort()
rawStatus shouldBe 0x55.toUByte()
rawPodsBattery shouldBe 0x88.toUByte()
rawFlags shouldBe 0xF.toUShort()
rawCaseBattery shouldBe 0x9.toUShort()
rawCaseLidState shouldBe 0x51.toUByte()
rawDeviceColor shouldBe 0x0.toUByte()
rawSuffix shouldBe 0x04.toUByte()
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
isLeftPodInEar shouldBe false
isRightPodInEar shouldBe false
batteryLeftPodPercent shouldBe 0.8f
batteryRightPodPercent shouldBe 0.8f
isCaseCharging shouldBe true
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe true
batteryCasePercent shouldBe 0.9f
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
}
}
/**
* https://old.reddit.com/message/messages/1hst12h
*/
@Test
fun `test AirPods Pro 2 - unknown setup from reddit user`() = runBlockingTest {
create<AirPodsPro2>("07 19 01 14 20 2B 9A 8F 01 00 04 0F 26 1A C4 2B FA 2F B9 B6 08 CD 60 CB DF 75 AB") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1420.toUShort()
rawStatus shouldBe 0x2B.toUByte()
rawPodsBattery shouldBe 0x9A.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0.toUByte()
rawSuffix shouldBe 0x04.toUByte()
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe true
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 0.9f
isCaseCharging shouldBe false
isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
}
}
}
+2 -2
View File
@@ -17,7 +17,7 @@ android {
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
versionCode = ProjectConfig.Version.code
versionCode = ProjectConfig.Version.code + 1 // Wear app
versionName = ProjectConfig.Version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
@@ -90,7 +90,7 @@ android {
val variantName: String = variantOutputImpl.name
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
val outputFileName = ProjectConfig.packageName +
val outputFileName = ProjectConfig.packageName + "-WEAROS" +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk"
@@ -43,8 +43,8 @@ open class App : Application(), Configuration.Provider {
private fun setupWorker() {
log(TAG) { "setupWorker()" }
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
Duration.ofMinutes(15),
Duration.ofMinutes(5)
Duration.ofMinutes(30),
Duration.ofMinutes(30)
).apply {
setInputData(Data.Builder().build())
}.build()
@@ -86,12 +86,12 @@ class MonitorWorker @AssistedInject constructor(
val monitorJob = podMonitor.mainDevice
.filterNotNull()
.take(5)
.take(3)
.setupCommonEventHandlers(TAG) { "monitorJob" }
.launchIn(workerScope)
try {
withTimeout(60 * 1000) {
withTimeout(15 * 1000) {
monitorJob.join()
}
log(TAG) { "Monitor job quit after a few takes." }
@@ -29,8 +29,8 @@ class DualPodsCardVH(parent: ViewGroup) :
}
text = sb
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL)
if (device is DualAirPods && item.showDebug) {
append(" [${device.primaryPod.name}]")
@@ -23,8 +23,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL)
}
deviceIcon.setImageResource(device.iconRes)
+1 -1
View File
@@ -18,7 +18,7 @@ android {
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
versionCode = ProjectConfig.Version.code
versionCode = ProjectConfig.Version.code + 0 // Base app
versionName = ProjectConfig.Version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
@@ -40,7 +40,7 @@ class UpgradeControlFoss @Inject constructor(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED
)
webpageTool.open("https://github.com/d4rken-org/capod#support-the-project")
webpageTool.open("https://github.com/sponsors/d4rken")
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
@@ -68,4 +68,6 @@ class UpgradeControlFoss @Inject constructor(
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}
override fun getSponsorUrl(): String? = "https://github.com/sponsors/d4rken"
}
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Derma</string>
<string name="foss_upgrade_alreadydonated_label">Saya sudah menderma</string>
<string name="foss_upgrade_donate_label">Sumbang</string>
<string name="foss_upgrade_alreadydonated_label">Saya telah menyumbang</string>
<string name="foss_upgrade_no_money_label">Saya belanjakan semua wang saya untuk AirPods</string>
</resources>
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="upgrades_gplay_unavailable_error">Los servicios de Google Play no están disponibles.</string>
<string name="upgrades_no_purchases_found_check_account">No se encontraron compras. ¿Está usando la cuenta correcta?</string>
</resources>
+1
View File
@@ -6,6 +6,7 @@
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.BLUETOOTH"
@@ -10,9 +10,7 @@ import androidx.annotation.MenuRes
import androidx.annotation.XmlRes
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.preference.PreferenceFragmentCompat
import androidx.viewbinding.ViewBinding
import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.main.ui.settings.SettingsFragment
@@ -69,17 +67,4 @@ abstract class PreferenceFragment2
}
}
}
inline fun <T> LiveData<T>.observe2(
crossinline callback: (T) -> Unit
) {
observe(viewLifecycleOwner) { callback.invoke(it) }
}
inline fun <T, reified VB : ViewBinding?> LiveData<T>.observe2(
ui: VB,
crossinline callback: VB.(T) -> Unit
) {
observe(viewLifecycleOwner) { callback.invoke(ui, it) }
}
}
@@ -0,0 +1,35 @@
package eu.darken.capod.common.uix
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LiveData
import androidx.viewbinding.ViewBinding
import eu.darken.capod.common.error.asErrorDialogBuilder
abstract class PreferenceFragment3 : PreferenceFragment2() {
abstract val vm: ViewModel3
var onErrorEvent: ((Throwable) -> Boolean)? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vm.errorEvents.observe2 {
val showDialog = onErrorEvent?.invoke(it) ?: true
if (showDialog) it.asErrorDialogBuilder(requireContext()).show()
}
}
inline fun <T> LiveData<T>.observe2(
crossinline callback: (T) -> Unit
) {
observe(viewLifecycleOwner) { callback.invoke(it) }
}
inline fun <T, reified VB : ViewBinding?> LiveData<T>.observe2(
ui: VB,
crossinline callback: VB.(T) -> Unit
) {
observe(viewLifecycleOwner) { callback.invoke(ui, it) }
}
}
@@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.overview.cards
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.ViewGroup
import androidx.core.view.isGone
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.lists.binding
@@ -33,6 +34,12 @@ class PermissionCardVH(parent: ViewGroup) :
val ppText = getString(R.string.settings_privacy_policy_label)
val ppLink = PrivacyPolicy.URL
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
val ppp = setOf(
Permission.ACCESS_FINE_LOCATION,
Permission.ACCESS_BACKGROUND_LOCATION,
Permission.BLUETOOTH_SCAN
)
isGone = !ppp.contains(item.permission)
}
}
@@ -31,8 +31,8 @@ class DualPodsCardVH(parent: ViewGroup) :
}
text = sb
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL)
if (device is DualAirPods && item.showDebug) {
append(" [${device.primaryPod.name}]")
@@ -23,8 +23,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL)
}
deviceIcon.setImageResource(device.iconRes)
@@ -24,8 +24,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
val device = item.device
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL)
}
lastSeen.text = device.lastSeenFormatted(item.now)
@@ -10,6 +10,7 @@ import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.uix.PreferenceFragment2
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.core.GeneralSettings
import javax.inject.Inject
@@ -22,6 +23,7 @@ class SettingsIndexFragment : PreferenceFragment2() {
override val preferenceFile: Int = R.xml.preferences_index
@Inject lateinit var webpageTool: WebpageTool
@Inject lateinit var upgradeRepo: UpgradeRepo
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupMenu(R.menu.menu_settings_index) { item ->
@@ -30,7 +32,13 @@ class SettingsIndexFragment : PreferenceFragment2() {
webpageTool.open("https://twitter.com/d4rken")
}
}
when (item.itemId) {
R.id.menu_item_sponsor -> {
upgradeRepo.getSponsorUrl()?.let { webpageTool.open(it) }
}
}
}
toolbar.menu?.findItem(R.id.menu_item_sponsor)?.isVisible = !upgradeRepo.getSponsorUrl().isNullOrEmpty()
super.onViewCreated(view, savedInstanceState)
}
@@ -10,7 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.preferences.PercentSliderPreference
import eu.darken.capod.common.uix.PreferenceFragment2
import eu.darken.capod.common.uix.PreferenceFragment3
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
@@ -19,9 +19,9 @@ import javax.inject.Inject
@Keep
@AndroidEntryPoint
class GeneralSettingsFragment : PreferenceFragment2() {
class GeneralSettingsFragment : PreferenceFragment3() {
private val vm: GeneralSettingsFragmentVM by viewModels()
override val vm: GeneralSettingsFragmentVM by viewModels()
@Inject lateinit var generalSettings: GeneralSettings
@Inject lateinit var upgradeRepo: UpgradeRepo
@@ -6,7 +6,7 @@ import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.uix.ViewModel3
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@HiltViewModel
@@ -16,9 +16,9 @@ class GeneralSettingsFragmentVM @Inject constructor(
private val bluetoothManager: BluetoothManager2,
) : ViewModel3(dispatcherProvider) {
val bondedDevices = flow {
emit(bluetoothManager.bondedDevices().toList())
}.asLiveData2()
val bondedDevices = bluetoothManager.bondedDevices()
.map { it.toList() }
.asLiveData2()
companion object {
private val TAG = logTag("Settings", "General", "VM")
@@ -9,14 +9,14 @@ import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.observe2
import eu.darken.capod.common.uix.PreferenceFragment2
import eu.darken.capod.common.uix.PreferenceFragment3
import javax.inject.Inject
@Keep
@AndroidEntryPoint
class DebugSettingsFragment : PreferenceFragment2() {
class DebugSettingsFragment : PreferenceFragment3() {
private val vm: DebugSettingsFragmentVM by viewModels()
override val vm: DebugSettingsFragmentVM by viewModels()
@Inject lateinit var debugSettings: DebugSettings
@@ -10,15 +10,15 @@ import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.ClipboardHelper
import eu.darken.capod.common.observe2
import eu.darken.capod.common.uix.PreferenceFragment2
import eu.darken.capod.common.uix.PreferenceFragment3
import eu.darken.capod.main.core.GeneralSettings
import javax.inject.Inject
@Keep
@AndroidEntryPoint
class SupportFragment : PreferenceFragment2() {
class SupportFragment : PreferenceFragment3() {
private val vm: SupportFragmentVM by viewModels()
override val vm: SupportFragmentVM by viewModels()
override val preferenceFile: Int = R.xml.preferences_support
@Inject lateinit var generalSettings: GeneralSettings
@@ -1,5 +1,6 @@
package eu.darken.capod.reaction.ui
import android.bluetooth.BluetoothDevice
import android.os.Bundle
import android.view.View
import androidx.annotation.Keep
@@ -10,7 +11,7 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.uix.PreferenceFragment2
import eu.darken.capod.common.uix.PreferenceFragment3
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
@@ -21,9 +22,9 @@ import javax.inject.Inject
@Keep
@AndroidEntryPoint
class ReactionSettingsFragment : PreferenceFragment2() {
class ReactionSettingsFragment : PreferenceFragment3() {
private val vm: ReactionSettingsFragmentVM by viewModels()
override val vm: ReactionSettingsFragmentVM by viewModels()
@Inject lateinit var generalSettings: GeneralSettings
@Inject lateinit var reactionSettings: ReactionSettings
@@ -35,6 +36,7 @@ class ReactionSettingsFragment : PreferenceFragment2() {
override val preferenceFile: Int = R.xml.preferences_reactions
private var isPro: Boolean = false
private var bondedDevices: List<BluetoothDevice> = emptyList()
private val autoConnectConditionPref by lazy { findPreference<ListPreference>(settings.autoConnectCondition.key)!! }
override fun onPreferencesCreated() {
@@ -66,10 +68,9 @@ class ReactionSettingsFragment : PreferenceFragment2() {
preference.isChecked = false
return true
} else if (generalSettings.mainDeviceAddress.value == null) {
val devices = vm.bondedDevices
DeviceSelectionDialogFactory(requireContext()).create(
devices = devices,
current = devices.firstOrNull { it.address == generalSettings.mainDeviceAddress.value }
devices = bondedDevices,
current = bondedDevices.firstOrNull { it.address == generalSettings.mainDeviceAddress.value }
) { selected ->
generalSettings.mainDeviceAddress.value = selected?.address
if (selected != null) preference.isChecked = true
@@ -87,13 +88,20 @@ class ReactionSettingsFragment : PreferenceFragment2() {
return super.onPreferenceTreeClick(preference)
}
// Some UI interactions shortly turn autoConnect on, then off again
private val previousMonitorMode by lazy { generalSettings.monitorMode.value }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
settings.autoConnect.flow.asLiveData().observe2 {
generalSettings.monitorMode.value = MonitorMode.ALWAYS
autoConnectConditionPref.isEnabled = it
settings.autoConnect.flow.asLiveData().observe2 { isEnabled ->
if (isEnabled) generalSettings.monitorMode.value = MonitorMode.ALWAYS
else generalSettings.monitorMode.value = previousMonitorMode
autoConnectConditionPref.isEnabled = isEnabled
}
vm.isPro.observe2 { isPro = true }
vm.isPro.observe2 { isPro = it }
vm.bondedDevices.observe2 { bondedDevices = it }
super.onViewCreated(view, savedInstanceState)
}
@@ -18,10 +18,12 @@ class ReactionSettingsFragmentVM @Inject constructor(
private val upgradeRepo: UpgradeRepo,
) : ViewModel3(dispatcherProvider) {
val bondedDevices = bluetoothManager.bondedDevices().toList()
val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveData2()
val bondedDevices = bluetoothManager.bondedDevices()
.map { it.toList() }
.asLiveData2()
companion object {
private val TAG = logTag("Settings", "Reaction", "VM")
}
@@ -2,11 +2,19 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.darken.androidstarter.main.ui.MainActivity">
<item
android:id="@+id/menu_item_sponsor"
android:icon="@drawable/ic_heart"
android:orderInCategory="100"
android:title="Sponsor development"
android:tooltipText="Sponsor development"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/menu_item_twitter"
android:icon="@drawable/ic_twitter"
android:orderInCategory="100"
android:tooltipText="Twitter"
android:title="Twitter"
android:tooltipText="Twitter"
app:showAsAction="always" />
</menu>
+1
View File
@@ -53,6 +53,7 @@
<string name="settings_support_description">Калі неабходна дапамога.</string>
<string name="issue_tracker_label">Спіс праблем</string>
<string name="issue_tracker_description">Агульнадаступны спіс для справаздач пра памылкі і запыту новых функцый (толькі англійская мова).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Месца для гутарак і задавання пытанняў.</string>
<string name="changelog_label">Спіс змяненняў</string>
<string name="settings_label">Налады</string>
+1
View File
@@ -53,6 +53,7 @@
<string name="settings_support_description">Potřebujete-li pomoc.</string>
<string name="issue_tracker_label">Sledování problémů</string>
<string name="issue_tracker_description">Veřejné sledování problémů pro hlášení chyb a požadavky na funkce (pouze v angličtině).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Místo, kde můžete psát a klást otázky.</string>
<string name="changelog_label">Seznam změn</string>
<string name="settings_label">Nastavení</string>
+2
View File
@@ -4,6 +4,7 @@
<string name="general_done_action">Fertig</string>
<string name="general_copy_action">Kopieren</string>
<string name="general_thank_you_label">Danke</string>
<string name="general_upgrade_action">Aktualisierung</string>
<string name="general_check_action">Überprüfen</string>
<string name="general_close_action">Schließen</string>
<string name="upgrade_capod_label">Verbesser CAPod</string>
@@ -52,6 +53,7 @@
<string name="settings_support_description">Wenn Sie Hilfe brauchen.</string>
<string name="issue_tracker_label">Issue-Tracker</string>
<string name="issue_tracker_description">Ein öffentlicher Issue-Tracker für Fehlerberichte und Funktionsanfragen (nur auf Englisch).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Ein Ort zum Verweilen und Fragen stellen.</string>
<string name="changelog_label">Änderungsprotokoll</string>
<string name="settings_label">Einstellungen</string>
+1
View File
@@ -53,6 +53,7 @@
<string name="settings_support_description">Si vous avez besoin daide.</string>
<string name="issue_tracker_label">Gestionnaire de problèmes</string>
<string name="issue_tracker_description">Un gestionnaire public de problèmes pour les signalements de bogues et les demandes de fonction (anglais seulement).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Un endroit à fréquenter où poser des questions.</string>
<string name="changelog_label">Journal des changements</string>
<string name="settings_label">Paramètres</string>
@@ -5,4 +5,5 @@
<string name="general_copy_action">प्रतिलिपि</string>
<string name="general_thank_you_label">शुक्रिया</string>
<string name="notification_channel_device_status_label">उपकरण की स्थिति</string>
<string name="settings_support_installid_desc">स्वचालित त्रुटि रिपोर्ट गुमनाम हैं। यदि डेवलपर को आपकी त्रुटि रिपोर्ट ढूंढ़ने की आवश्यकता हो, तो अपना इंस्टॉल आईडी साझा करें.</string>
</resources>
+3
View File
@@ -51,7 +51,9 @@
<string name="settings_support_label">Supporto</string>
<string name="settings_support_description">Se ti serve un aiuto.</string>
<string name="issue_tracker_description">Un issue tracker pubblico per segnalare i problemi e le richieste per aggiungere nuove funzionalità (solo in inglese).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Un posto dove poter fare domande.</string>
<string name="changelog_label">Changelog</string>
<string name="settings_label">Impostazioni</string>
<string name="settings_privacy_policy_label">Politica sulla privacy</string>
<string name="settings_privacy_policy_desc">Gestione dei dati responsabile.</string>
@@ -71,4 +73,5 @@
<string name="help_translate_label">Traduzione</string>
<string name="help_translate_description">Aiuta a tradurre l\'applicazione nella tua lingua preferita.</string>
<string name="translators_thanks_title">Traduttori</string>
<string name="translators_thanks_description">darken</string>
</resources>
+10 -9
View File
@@ -10,27 +10,27 @@
<string name="upgrade_capod_label">CAPodをアップグレード</string>
<string name="upgrade_capod_description">追加機能を取得し、開発者を支援する。</string>
<string name="settings_monitor_mode_label">モニターモード</string>
<string name="settings_monitor_mode_description">いつ、このアプリがBluetoothデータの監視をします</string>
<string name="settings_monitor_mode_description">このアプリがいつ、Bluetoothデータの監視をするのかを選択します。</string>
<string name="settings_scanner_mode_label">スキャンモード</string>
<string name="settings_scanner_mode_description">Bluetooth Low Energyデータスキャンは、パフォーマンスと省電力のどちらを優先すべきでしょうか?</string>
<string name="settings_scanner_mode_description">Bluetooth Low Energyデータスキャンパフォーマンスと省電力のどちらを優先すべきかを選択します。</string>
<string name="settings_autopause_label">自動停止</string>
<string name="settings_autopause_description">耳から外したときに再生を一時停止する。</string>
<string name="settings_showall_label">すべてのデバイスを表示</string>
<string name="settings_showall_description">近くにいる他の人の端末を表示します。</string>
<string name="settings_showall_description">近くにいる他の人のデバイスを表示します。</string>
<string name="settings_autopplay_label">自動再生</string>
<string name="settings_autoplay_description">装着時に再生を開始する。</string>
<string name="settings_fake_data_label">フェイクデータ</string>
<string name="settings_fake_data_description">実在しない機器を模したフェイクデータを表示す</string>
<string name="settings_fake_data_description">実在しない機器を模したフェイクデータを表示します。</string>
<string name="settings_debug_label">デバッグ用の設定</string>
<string name="settings_debug_description">トラブルシューティングに役立つ追加設定。</string>
<string name="settings_signal_minimum_label">最低限必要な信号品質</string>
<string name="settings_signal_minimum_description">デバイスが自分のものとみなすために必要な最低限の信号品質。</string>
<string name="settings_signal_minimum_description">デバイスが自分のものとみなすために必要な最低限の信号品質を調節します</string>
<string name="settings_autoconnect_label">自動接続</string>
<string name="settings_autoconnect_description">Androidが自動的に接続しない場合は、こちらからも問い合わせることができます。これにより、モニターモードの設定が「常に」になります。</string>
<string name="settings_autoconnect_condition_label">自動接続の条件</string>
<string name="settings_autoconnect_condition_description">いつ接続を試みればよいですか</string>
<string name="settings_autoconnect_condition_description">接続をいつ試みるのかを選択します</string>
<string name="settings_reaction_label">アクション</string>
<string name="settings_reaction_description">イベントや行動に対して機能が動作します</string>
<string name="settings_reaction_description">イベントや行動に対して機能が動作します</string>
<string name="settings_category_yourdevice_label">あなたのデバイス</string>
<string name="settings_maindevice_address_label">あなたのデバイスのアドレス</string>
<string name="settings_maindevice_address_description">ペアリングされたデバイスのアドレス。 アプリはこれを使って、携帯電話にいつ接続されたかを判断します。</string>
@@ -53,11 +53,12 @@
<string name="settings_support_description">何か困ったことがあったら</string>
<string name="issue_tracker_label">イシュー・トラッカー</string>
<string name="issue_tracker_description">バグレポートや機能要望のための公開されたイシュー・トラッカー(英語のみ)。</string>
<string name="discord_label">Discord</string>
<string name="discord_description">質問がしやすい場所です。</string>
<string name="changelog_label">更新の内容</string>
<string name="settings_label">設定</string>
<string name="settings_privacy_policy_label">個人情報の取り扱いについて</string>
<string name="settings_privacy_policy_desc">責任を持ってデータを取り扱います</string>
<string name="settings_privacy_policy_desc">責任を持ってデータを取り扱います</string>
<string name="settings_licenses_label">ライセンス情報</string>
<string name="settings_category_other_label">その他</string>
<string name="settings_general_label">全般</string>
@@ -72,7 +73,7 @@
<string name="settings_compatibility_mode_label">互換モード</string>
<string name="settings_compatibility_mode_description">互換性を向上させるために最適化を無効にします。データが表示されない場合は、こちらをお試しください。</string>
<string name="help_translate_label">翻訳</string>
<string name="help_translate_description">このアプリをあなたの好きな言語に翻訳するのを手伝ってください</string>
<string name="help_translate_description">このアプリをあなたの好きな言語に翻訳するのを手伝ってください</string>
<string name="translators_thanks_title">翻訳者</string>
<string name="translators_thanks_description">OrangeOC(translation.love@na-cat.com)</string>
</resources>
+4 -2
View File
@@ -8,9 +8,9 @@
<string name="general_check_action">Semak</string>
<string name="general_close_action">Tutup</string>
<string name="upgrade_capod_label">Naik taraf CAPod</string>
<string name="upgrade_capod_description">Dapatkan ciri tambahan dan sokongi pemaju.</string>
<string name="upgrade_capod_description">Dapatkan ciri tambahan dan dokongi pemaju.</string>
<string name="settings_monitor_mode_label">Mod monitor</string>
<string name="settings_monitor_mode_description">Dalam keadaan yang mana apl ini memantau data Bluetooth.</string>
<string name="settings_monitor_mode_description">Dalam keadaan di mana apl ini memantau data Bluetooth.</string>
<string name="settings_scanner_mode_label">Mod pengimbas</string>
<string name="settings_scanner_mode_description">Apakah sepatutnya pengimbas data Tenaga Rendah Bluetooth mengutamakan prestasi atau penjimatan tenaga?</string>
<string name="settings_autopause_label">Auto jeda</string>
@@ -53,7 +53,9 @@
<string name="settings_support_description">Jika anda perlukan bantuan.</string>
<string name="issue_tracker_label">Penjejak isu</string>
<string name="issue_tracker_description">Penjejak isu awam untuk laporan pepijat dan permintaan ciri (bahasa Inggeris sahaja).</string>
<string name="discord_label">Perselisihan</string>
<string name="discord_description">Tempat melepak dan bertanya soalan.</string>
<string name="changelog_label">Log perubahan</string>
<string name="settings_label">Tetapan</string>
<string name="settings_privacy_policy_label">Polisi privasi</string>
<string name="settings_privacy_policy_desc">Mengendalikan data secara bertanggungjawab.</string>
+1
View File
@@ -75,4 +75,5 @@
<string name="help_translate_label">Vertaling</string>
<string name="help_translate_description">Help deze app te vertalen in uw favoriete taal.</string>
<string name="translators_thanks_title">Vertalers</string>
<string name="translators_thanks_description">darken</string>
</resources>
+1
View File
@@ -53,6 +53,7 @@
<string name="settings_support_description">Hvis du trenger hjelp med noe.</string>
<string name="issue_tracker_label">Problemsporing</string>
<string name="issue_tracker_description">En offentlig problemsporing for feilrapporter og funksjonsforespørsler (kun på engelsk).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Et sted til å henge og for å stille spørsmål.</string>
<string name="changelog_label">Endringslogg</string>
<string name="settings_label">Innstillinger</string>
+2 -1
View File
@@ -12,7 +12,7 @@
<string name="settings_monitor_mode_label">Tryb monitorowania</string>
<string name="settings_monitor_mode_description">W pewnych okolicznościach aplikacja może monitorować dane Bluetooth.</string>
<string name="settings_scanner_mode_label">Tryb skanera</string>
<string name="settings_scanner_mode_description">Czy skaner danych Bluetooth Low Energy ma priorytetowo działać w ramach wydajności czy oszczędzania energii?</string>
<string name="settings_scanner_mode_description">Czy skaner danych Bluetooth Low Energy ma priorytetowo działać w ramach wydajności, czy oszczędzania energii?</string>
<string name="settings_autopause_label">Automatyczna pauza</string>
<string name="settings_autopause_description">Pauzuj odtwarzanie, gdy urządzenia jest wyciągane z ucha.</string>
<string name="settings_showall_label">Wyświetl wszystkie urządzenia</string>
@@ -53,6 +53,7 @@
<string name="settings_support_description">Jeśli potrzebujesz pomocy.</string>
<string name="issue_tracker_label">Śledzenie problemów</string>
<string name="issue_tracker_description">Publiczna lista problemów, dla raportów błędów oraz próśb o nowe funkcjonalności (tylko po angielsku).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Miejsce do rozmów i zadawania pytań.</string>
<string name="changelog_label">Lista zmian</string>
<string name="settings_label">Ustawienia</string>
+2 -1
View File
@@ -75,5 +75,6 @@
<string name="help_translate_label">Перевод</string>
<string name="help_translate_description">Помогите перевести приложение на Ваш язык.</string>
<string name="translators_thanks_title">Переводчики</string>
<string name="translators_thanks_description">AL_Cool_T</string>
<string name="translators_thanks_description">gaich
AL_Cool_T</string>
</resources>
+1
View File
@@ -53,6 +53,7 @@
<string name="settings_support_description">Ak potrebujete pomoc.</string>
<string name="issue_tracker_label">Sledovač problémov</string>
<string name="issue_tracker_description">Verejný nástroj na sledovanie problémov pre hlásenia chýb a požiadavky na funkcie (iba v angličtine).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Miesto, kde sa môžete stretnúť a klásť otázky.</string>
<string name="changelog_label">Zoznam zmien</string>
<string name="settings_label">Nastavenia</string>
@@ -53,6 +53,7 @@
<string name="settings_support_description">如果你需要協助。</string>
<string name="issue_tracker_label">問題追蹤器</string>
<string name="issue_tracker_description">一個用於錯誤回報和功能需求的公用問題追蹤器 (僅英文)。</string>
<string name="discord_label">Discord</string>
<string name="discord_description">一個可以在其中閒逛並提出問題的地方。</string>
<string name="changelog_label">變更記錄</string>
<string name="settings_label">設定</string>
+9 -6
View File
@@ -14,13 +14,16 @@ object ProjectConfig {
const val targetSdk = 33
object Version {
const val major = 2
const val minor = 0
const val patch = 0
const val build = 0
val versionProperties = Properties().apply {
load(FileInputStream(File("version.properties")))
}
val major = versionProperties.getProperty("project.versioning.major").toInt()
val minor = versionProperties.getProperty("project.versioning.minor").toInt()
val patch = versionProperties.getProperty("project.versioning.patch").toInt()
val build = versionProperties.getProperty("project.versioning.build").toInt()
const val name = "${major}.${minor}.${patch}-rc${build}"
const val code = major * 1000000 + minor * 10000 + patch * 100 + build
val name = "${major}.${minor}.${patch}-rc${build}"
val code = major * 10000000 + minor * 100000 + patch * 1000 + build * 10
}
}
+8
View File
@@ -31,6 +31,10 @@ platform :android do
skip_upload_images: 'true',
skip_upload_screenshots: 'true',
skip_upload_metadata: 'true',
aab_paths: [
"app/build/outputs/bundle/gplayRelease/app-gplay-beta.aab",
"app-wear/build/outputs/bundle/gplayRelease/app-wear-gplay-beta.aab",
],
)
end
@@ -45,6 +49,10 @@ platform :android do
skip_upload_images: 'true',
skip_upload_screenshots: 'true',
skip_upload_metadata: 'true',
aab_paths: [
"app/build/outputs/bundle/gplayRelease/app-gplay-release.aab",
"app-wear/build/outputs/bundle/gplayRelease/app-wear-gplay-release.aab",
],
)
end
@@ -1,18 +1,18 @@
CAPod is a companion app for AirPods.
CAPod es una aplicación complementaria para AirPods.
Features:
Funciones:
* 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.
* Nivel de batería para los auriculares y estuches.
* Indicador de carga de auriculares y estuche.
* Información adicional de conexión, micrófono y estuche.
* Puede recibir y mostrar todos los dispositivos cercanos.
* Detección de oído con reproducción/pausa automática.
* Conecta automáticamente el teléfono y los AirPods.
* Muestra un aviso emergente de estuche abierto.
CAPod is ad-free. Some features require an in-app purchase.
CAPod es libre de publicidad. Algunas funciones requieren compras en la aplicación.
Most popular AirPods and Beats devices are supported.
If your device is similar to AirPods but not yet supported, send me a short mail.
Válido para la mayoría de AirPods populares y Beats.
Si su dispositivo es similar a AirPods pero aún no es compatible, envíeme un correo.
Got a cool idea for a new feature? Reach out!
¿Tienes una buena idea para una nueva característica? ¡Háznoslo saber!
@@ -1 +1 @@
CAPod is a companion app for AirPods on Android.
CAPod es una aplicación complementaria para AirPods en Android.
+1 -1
View File
@@ -1 +1 @@
CAPod - Companion for AirPods
CAPod - Asistente para AirPods
@@ -1,18 +1,18 @@
CAPod ialah apl pendamping untuk AirPods.
CAPod ialah aplikasi peneman untuk AirPods.
Ciri-ciri:
* Paras bateri untuk pods dan sarung.
* Paras bateri untuk pod dan sarung.
* Status pengecasan untuk pod dan sarung.
* Maklumat tambahan tentang sambungan, mikrofon dan sarung.
* Boleh menerima dan menunjukkan semua peranti berdekatan.
* Pengesanan telinga dengan main/jeda automatik.
* Sambungkan telefon dan AirPod secara automatik.
* Tunjukkan popup apabila sarung dibuka.
* Pamerkan timbul apabila sarung dibuka.
CAPod bebas iklan. Sesetengah ciri memerlukan pembelian dalam apl.
CAPod adalah bebas iklan. Sesetengah ciri memerlukan pembelian dalam aplikasi.
Kebanyakan peranti AirPods dan Beats yang popular disokong.
Jika peranti anda serupa dengan AirPods tetapi belum disokong, kirimkan mel ringkas kepada saya.
Peroleh idea hebat untuk ciri baharu? Menjangkau!
Ada idea hebat untuk ciri yang baharu? Menjangkau!
+1 -1
View File
@@ -1 +1 @@
CAPod - Rakan untuk AirPods
CAPod - Teman untuk AirPod
Executable
+458
View File
@@ -0,0 +1,458 @@
#!/bin/bash
# Based on
# * https://gist.github.com/jv-k/703e79306554c26a65a7cfdb9ca119c6
# * https://github.com/jv-k/ver-bump
# █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█
# █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█
#
#
# Description:
# - This script automates bumping the git software project's version using automation.
# - It does several things that are typically required for releasing a Git repository, like git tagging,
# automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files.
# - Increments / suggests the current software project's version number
# - Adds a Git tag, named after the chosen version number
# - Updates CHANGELOG.md
# - Updates VERSION file
# - Commits files to a new branch
# - Pushes to remote (optionally)
# - Updates "version" : "x.x.x" tag in JSON files if [-v file1 -v file2...] argument is supplied.
#
# Usage:
# ./bump-version.sh [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-b] [-h]
#
# Options:
# -v <version number> Specify a manual version number
# -m <release message> Custom release message.
# -f <filename.json> Update version number inside JSON files.
# * For multiple files, add a separate -f option for each one,
# * For example: ./bump-version.sh -f src/plugin/package.json -f composer.json
# -p <repository alias> Push commits to remote repository, eg `-p origin`
# -n Don't perform a commit automatically.
# * You may want to do that yourself, for example.
# -b Don't create automatic `release-<version>` branch
# -h Show help message.
#
# Detailed notes:
# The contents of the `VERSION` file which should be a semantic version number such as "1.2.3"
# or even "1.2.3-beta+001.ab"
#
# It pulls a list of changes from git history & prepends to a file called CHANGELOG.md
# under the title of the new version # number, allows the user to review and update the changelist
#
# Creates a Git tag with the version number
#
# - Creates automatic `release-<version>` branch
#
# Commits the new version to the current repository
#
# Optionally pushes the commit to remote repository
#
# Make sure to set execute permissions for the script, eg `$ chmod 755 bump-version.sh`
#
# Credits:
# https://github.com/jv-k/bump-version
#
# - Inspired by the scripts from @pete-otaqui and @mareksuscak
# https://gist.github.com/pete-otaqui/4188238
# https://gist.github.com/mareksuscak/1f206fbc3bb9d97dec9c
#
NOW="$(date +'%B %d, %Y')"
# ANSI/VT100 colours
YELLOW='\033[1;33m'
LIGHTYELLOW='\033[0;33m'
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
WHITE='\033[1;37m'
LIGHTGRAY='\033[0;37m'
DARKGRAY='\033[1;30m'
BOLD="\033[1m"
INVERT="\033[7m"
RESET='\033[0m'
# Default options
FLAG_JSON="false"
FLAG_PUSH="false"
I_OK="✅"
I_STOP="🚫"
I_ERROR="❌"
I_END="👋🏻"
S_NORM="${WHITE}"
S_LIGHT="${LIGHTGRAY}"
S_NOTICE="${GREEN}"
S_QUESTION="${YELLOW}"
S_WARN="${LIGHTRED}"
S_ERROR="${RED}"
V_SUGGEST="0.1.2-rc5" # This is suggested in case VERSION file or user supplied version via -v is missing
V_MAJOR="" # 0
V_MINOR="" # 1
V_PATCH="" # 2
V_BUILD_TYPE="" # rc
V_BUILD_COUNTER="" # 5
V_NAME=""
V_CODE=""
SCRIPT_VER="1.0"
GIT_MSG="Release: "
REL_NOTE=""
REL_PREFIX="release/"
PUSH_DEST="origin"
# Show credits & help
usage() {
echo -e "$GREEN" \
"\n █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ " \
"\n █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ " \
"\n\t\t\t\t\t$LIGHTGRAY v${SCRIPT_VER}"
echo -e " ${S_NORM}${BOLD}Usage:${RESET}" \
"\n $0 [-v <version number>] [-m <release message>] [-n] [-p] [-h]" 1>&2
echo -e "\n ${S_NORM}${BOLD}Options:${RESET}"
echo -e " $S_WARN-v$S_NORM <version number>\tSpecify a manual version number"
echo -e " $S_WARN-m$S_NORM <release message>\tCustom release message."
echo -e " $S_WARN-p$S_NORM \t\t\tPush commits to ORIGIN. "
echo -e " $S_WARN-n$S_NORM \t\t\tDon't perform a commit automatically. " \
"\n\t\t\t* You may want to do that manually after checking everything, for example."
echo -e " $S_WARN-b$S_NORM \t\t\tDon't create automatic \`release-<version>\` branch"
echo -e " $S_WARN-h$S_NORM \t\t\tShow this help message. "
echo -e "\n ${S_NORM}${BOLD}Original author: $S_LIGHT https://github.com/jv-t/bump-version $RESET"
echo -e "\n ${S_NORM}${BOLD}Changes by: $S_LIGHT https://github.com/d4rken $RESET\n"
}
# If there are no commits in repo, quit, because you can't tag with zero commits.
check-commits-exist() {
git rev-parse HEAD &>/dev/null
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2
echo
exit 1
fi
}
exit_abnormal() {
echo -e " ${S_LIGHT}––––––"
usage # Show help
exit 1
}
# Process script options
process-arguments() {
local OPTIONS OPTIND OPTARG
# Get positional parameters
JSON_FILES=()
while getopts ":v:p:m:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs.
case "$OPTIONS" in
h)
# Show help
exit_abnormal
;;
v)
# User has supplied a version number
V_USR_SUPPLIED=$OPTARG
;;
m)
REL_NOTE=$OPTARG
# Custom release note
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'"
;;
p)
FLAG_PUSH=true
PUSH_DEST=${OPTARG} # Replace default with user input
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script."
;;
n)
FLAG_NOCOMMIT=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging."
;;
b)
FLAG_NOBRANCH=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch."
;;
\?)
echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2
echo
exit_abnormal
;;
:)
echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2
echo
exit_abnormal
;;
esac
done
}
# Suggests version from VERSION file, or grabs from user supplied -v <version>.
# If none is set, suggest default from options.
process-version() {
V_RAW=""
V_FILE_REGEX='^([0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z]+[0-9]+) ([0-9]+)$'
V_FILE_RAW="$(cat VERSION)"
if [ -f VERSION ] && [ -s VERSION ] && [[ $V_FILE_RAW =~ $V_FILE_REGEX ]]; then
V_PREV="${BASH_REMATCH[1]}"
V_SUGGEST=$V_PREV
echo -e "\n${S_NOTICE}Current version from <${S_NORM}VERSION${S_NOTICE}> file: ${S_NORM}$V_PREV"
else
echo -ne "\n${S_WARN}The [${S_NORM}VERSION${S_WARN}] "
if [ ! -f VERSION ]; then
echo "VERSION file was not found."
elif [ ! -s VERSION ]; then
echo "VERSION file is empty."
else
echo "could not be parsed."
fi
fi
# If a version number is supplied by the user with [-v <version number>], then use it
if [ -n "$V_USR_SUPPLIED" ]; then
echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}"
V_RAW="${V_USR_SUPPLIED}"
else
echo -ne "\n${S_QUESTION}Enter a new version number [${S_NORM}$V_SUGGEST${S_QUESTION}]: "
echo -ne "$S_WARN"
read -r V_RAW
fi
if [ -z "$V_RAW" ]; then
V_RAW=$V_PREV
fi
if [ -z "$V_RAW" ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: No version was supplied (no file, no CLI)\n"
exit_abnormal
fi
SEMVER_REGEX='^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-zA-Z]+)([0-9]+)$'
echo -e "\n${S_NOTICE}Parsing ${V_RAW}"
if [[ $V_RAW =~ $SEMVER_REGEX ]]; then
echo -e "\n${I_OK} ${S_NOTICE} Successfully parsed ${V_RAW} to ${BASH_REMATCH[0]}"
V_MAJOR="${BASH_REMATCH[1]}"
echo "V_MAJOR=$V_MAJOR"
V_MINOR="${BASH_REMATCH[2]}"
echo "V_MINOR=$V_MINOR"
V_PATCH="${BASH_REMATCH[3]}"
echo "V_PATCH=$V_PATCH"
V_BUILD_TYPE="${BASH_REMATCH[4]}"
echo "V_BUILD_TYPE=$V_BUILD_TYPE"
V_BUILD_COUNTER="${BASH_REMATCH[5]}"
echo "V_BUILD_COUNTER=$V_BUILD_COUNTER"
else
echo -e "\n${I_STOP} ${S_ERROR}Error: Failed to parse $V_RAW\n"
exit_abnormal
fi
# If no version was provided, bump the previous version
if [ -z "$V_USR_SUPPLIED" ]; then
if [ "$V_BUILD_COUNTER" -eq "$V_BUILD_COUNTER" ] 2>/dev/null; then # discard stderr (2) output to black hole (suppress it)
V_BUILD_COUNTER=$((V_BUILD_COUNTER + 1)) # Increment
fi
fi
V_NAME="$V_MAJOR.$V_MINOR.$V_PATCH-$V_BUILD_TYPE$V_BUILD_COUNTER"
V_CODE=$((V_MAJOR * 10000000 + V_MINOR * 100000 + V_PATCH * 1000 + V_BUILD_COUNTER * 10))
echo -e "${S_NOTICE}Setting version to [${S_NORM}${V_NAME} (${V_CODE})${S_NOTICE}] ...."
}
# Only tag if tag doesn't already exist
check-tag-exists() {
TAG_CHECK_EXISTS=$(git tag -l v"$V_NAME")
if [ -n "$TAG_CHECK_EXISTS" ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n"
exit 0
fi
}
# $1 : version
# $2 : release note
create-tag() {
if [ -z "$2" ]; then
# Default release note
git tag -a "v$1" -m "Tag version $1."
else
# Custom release note
git tag -a "v$1" -m "$2"
fi
echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag"
}
# Update version.properties which is used by Gradle to generate the `versionName` and `versionCode`
do-version-properties() {
PROPS_FILE_NAME="version.properties"
echo -e "\n${S_NOTICE}Parsing ${PROPS_FILE_NAME}:\n"
V_MAJOR_REGEX='^([a-zA-Z\.]+major)=([0-9]+)$'
V_MINOR_REGEX='^([a-zA-Z\.]+minor)=([0-9]+)$'
V_PATCH_REGEX='^([a-zA-Z\.]+patch)=([0-9]+)$'
V_BUILD_REGEX='^([a-zA-Z\.]+build)=([0-9]+)$'
PROPS_FILE_NEW=""
LAST_LINE=$(wc -l <$PROPS_FILE_NAME)
CURRENT_LINE=0
while read -r line; do
CURRENT_LINE=$((CURRENT_LINE + 1))
if [[ $line =~ $V_MAJOR_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_MAJOR}"
echo "Found major, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_MINOR_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_MINOR}"
echo "Found minor, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_PATCH_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_PATCH}"
echo "Found patch, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_BUILD_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_BUILD_COUNTER}"
echo "Found build, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
else
PROPS_FILE_NEW+="$line"
fi
if [[ $CURRENT_LINE -ne $LAST_LINE ]]; then
PROPS_FILE_NEW+="\n"
fi
done <"$PROPS_FILE_NAME"
echo -e "$PROPS_FILE_NEW" >"$PROPS_FILE_NAME"
git add "$PROPS_FILE_NAME"
echo -e "\n${I_OK} ${S_NOTICE}Updated [${S_NORM}${PROPS_FILE_NAME}${S_NOTICE}] file"
}
# Update a version file that can be parsed by third-parties, e.g. F-Droid
do-versionfile() {
[ -f VERSION ] && ACTION_MSG="Updated" || ACTION_MSG="Created"
echo "${V_NAME} ${V_CODE}" >VERSION # Create file
echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}VERSION${S_NOTICE}] file"
# Stage file for commit
git add VERSION
}
# Does the release branch already exist?
check-branch-exist() {
[ "$FLAG_NOBRANCH" = true ] && return
BRANCH_MSG=$(git rev-parse --verify "${REL_PREFIX}${V_NAME}" 2>&1)
if [ "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_NAME}${S_ERROR}> already exists!\n"
exit 1
fi
}
# Create release branch if desired
do-branch() {
[ "$FLAG_NOBRANCH" = true ] && return
echo -e "\n${S_NOTICE}Creating new release branch..."
BRANCH_MSG=$(git branch "${REL_PREFIX}${V_NAME}" 2>&1)
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n"
exit 1
else
BRANCH_MSG=$(git checkout "${REL_PREFIX}${V_NAME}" 2>&1)
echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}"
fi
}
# Stage & commit all files modified by this script
do-commit() {
[ "$FLAG_NOCOMMIT" = true ] && return
echo -e "\n${S_NOTICE}Committing..."
COMMIT_MSG=$(git commit -m "${GIT_MSG}" 2>&1)
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n"
exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG"
fi
}
# Pushes files + tags to remote repo. Changes are staged by earlier functions
do-push() {
[ "$FLAG_NOCOMMIT" = true ] && return
if [ "$FLAG_PUSH" = true ]; then
CONFIRM="Y"
else
echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: "
read CONFIRM
fi
case "$CONFIRM" in
[yY][eE][sS] | [yY])
echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..."
PUSH_MSG=$(git push "${PUSH_DEST}" v"$V_NAME" 2>&1) # Push new tag
PUSH_MSG+="\n"
PUSH_MSG+=$(git push 2>&1) # Push new tag
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG"
# exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG"
fi
;;
esac
}
#### Initiate Script ###########################
check-commits-exist
# Process and prepare
process-arguments "$@"
process-version
GIT_MSG+="${V_NAME}"
check-branch-exist
check-tag-exists
echo -e "\n${S_LIGHT}––––––"
# Update steps
do-version-properties
do-versionfile
do-branch
do-commit
create-tag "${V_NAME}" "${REL_NOTE}"
do-push
echo -e "\n${S_LIGHT}––––––"
echo -e "\n${I_OK} ${S_NOTICE}"Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} >" || echo "to ") "$V_NAME"
echo -e "\n${GREEN}Done ${I_END}\n"
+6
View File
@@ -0,0 +1,6 @@
### Updated by release.sh ###
project.versioning.major=2
project.versioning.minor=2
project.versioning.patch=1
project.versioning.build=1
#############################