Compare commits

..
13 Commits
Author SHA1 Message Date
darken ac477113cf Release: 4.0.1-rc0 2026-03-12 13:42:29 +01:00
darken 5e21941765 General: Update app translations from Crowdin 2026-03-12 12:12:40 +00:00
darken e5032ee35a General: Update fastlane translations from Crowdin 2026-03-12 12:12:40 +00:00
darken 1ceac349ab chore: Add .claude/.gitignore for worktrees 2026-03-12 11:59:07 +00:00
darken 2db01b410b fix(billing): Prevent crash when billing service disconnects 2026-03-12 11:59:07 +00:00
darken 193a1030fb feat: Start BLE scanning with only core permissions granted
Allow the app to scan for AirPods when only BLE scan permissions are granted, without waiting for all optional permissions (notifications, overlay, etc.).

Add isScanBlocking flag to Permission enum. Gate monitor service and pod scanning on scan permissions only. Show scan-blocking permission cards with error color and sorted first. Wrap BLUETOOTH_CONNECT-dependent calls in try-catch for graceful degradation. Fix POST_NOTIFICATIONS minApiLevel from S (31) to TIRAMISU (33).
2026-03-10 16:37:43 +00:00
darken 12e35631d6 fix: Scope ViewModels to navigation entries via NavDisplay decorator 2026-03-10 15:11:10 +00:00
darken 67474a30da fix(debug): Rename recorder screen button from Close to Keep
The keep button's onClick handler was already named onKeep but the label said Close. Renamed to Keep for clarity alongside the Delete/Share buttons. Added dedicated string resource with translations for all 75 locales.
2026-03-10 07:50:36 +00:00
darken 44cd0eec2a test: Add missing AutoConnectCondition round-trip and Model encode tests 2026-03-09 20:29:19 +00:00
darken a79f6a0535 refactor: Replace Moshi with kotlinx.serialization
Remove Moshi dependency entirely, completing the migration to kotlinx.serialization. All JSON serialization now uses kotlinx with explicit @SerialName annotations for wire format stability.

- Migrate PodDeviceCache from Moshi to kotlinx Json injection

- Add MapIntByteArrayBase64Serializer for BleScanResult cache compat

- Strip @JsonClass/@Json annotations from all dual-annotated classes

- Delete Moshi adapters, ProGuard rules, and build config

- Convert compat tests to pure kotlinx round-trip tests
2026-03-09 20:29:19 +00:00
darken 8675d73a98 fix(debug): Persist recording session across app restarts
The debug recording trigger file now stores the session directory path and start timestamp. On app restart, the recorder resumes into the same session directory and log file instead of creating a new one, preserving the original start time so the short-recording guard doesn't reset.
2026-03-09 17:34:29 +00:00
darken 3423786f06 chore: Remove redundant package name from manifest 2026-03-09 17:41:08 +01:00
darken 00083707c6 fix(debug): Harden debug session recording and sharing 2026-03-09 16:39:56 +00:00
127 changed files with 816 additions and 522 deletions
+1
View File
@@ -0,0 +1 @@
worktrees/
+1 -1
View File
@@ -80,5 +80,5 @@ Code shared between phone and Wear OS apps is placed in `app-common`. When modif
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **Moshi**: JSON serialization for configuration and debugging
- **kotlinx.serialization**: JSON serialization for configuration and caching
- **Material Design**: UI components following Material Design guidelines
+1 -1
View File
@@ -1 +1 @@
4.0.0-rc0 40000000
4.0.1-rc0 40001000
-2
View File
@@ -177,8 +177,6 @@ dependencies {
addDagger()
addMoshi()
addOkio()
addBaseAndroid()
-3
View File
@@ -1,3 +0,0 @@
-keepclassmembernames @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
}
@@ -1,23 +1,19 @@
package eu.darken.capod.common.upgrade.core
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.serialization.InstantEpochMillisSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
@Serializable
@JsonClass(generateAdapter = true)
data class FossUpgrade(
@Serializable(with = InstantEpochMillisSerializer::class) val upgradedAt: Instant,
val reason: Reason
@SerialName("upgradedAt") @Serializable(with = InstantEpochMillisSerializer::class) val upgradedAt: Instant,
@SerialName("reason") val reason: Reason
) {
@Serializable
@JsonClass(generateAdapter = false)
enum class Reason {
@SerialName("foss.upgrade.reason.donated") @Json(name = "foss.upgrade.reason.donated") DONATED,
@SerialName("foss.upgrade.reason.alreadydonated") @Json(name = "foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@SerialName("foss.upgrade.reason.nomoney") @Json(name = "foss.upgrade.reason.nomoney") NO_MONEY;
@SerialName("foss.upgrade.reason.donated") DONATED,
@SerialName("foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@SerialName("foss.upgrade.reason.nomoney") NO_MONEY;
}
}
+4 -4
View File
@@ -2,9 +2,9 @@
<resources>
<string name="foss_upgrade_donate_label">Faire un don</string>
<string name="foss_upgrade_alreadydonated_label">Jai déjà fait un don</string>
<string name="foss_upgrade_no_money_label">Jai dépensé tout mon argent sur les AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS est gratuit et open source. Si vous le trouvez utile, pensez à sponsoriser le développement pour aider à maintenir le projet en vie.</string>
<string name="foss_upgrade_no_money_label">Jai dépensé tout mon argent sur des AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS est gratuite et à code source ouvert. Si vous la trouvez utile, pensez à soutenir le développement pour contribuer à la pérennité du projet.</string>
<string name="upgrade_foss_sponsor_action">Soutenir le développement</string>
<string name="upgrade_foss_sponsor_subtitle">Pas de publicités. Pas de suivi. Pas de dépendance à Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Déjà de retour ? Votre soutien maintient CAPod en vie.</string>
<string name="upgrade_foss_sponsor_subtitle">Pas de publicités. Pas de suivi à la trace. Pas de dépendance à Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Déjà de retour? Votre soutien maintient CAPod en vie.</string>
</resources>
@@ -24,9 +24,11 @@ class BillingDataRepo @Inject constructor(
) {
private val connectionProvider = billingClientConnectionProvider.connection
.catch {
log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" }
throw it
.retryWhen { cause, attempt ->
if (cause is CancellationException) return@retryWhen false
log(TAG, ERROR) { "Unable to provide client connection (attempt=$attempt):\n${cause.asLog()}" }
delay(60_000) // 60s between retries (upstream already did 5 quick retries)
true
}
.replayingShare(scope)
+10 -5
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.darken.capod">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
@@ -9,10 +9,14 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission
android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
tools:ignore="BackgroundLocationPolicy" />
<uses-permission
android:name="android.permission.BLUETOOTH"
@@ -32,7 +36,8 @@
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
android:usesPermissionFlags="neverForLocation"
tools:ignore="UnusedAttribute" />
<uses-feature
android:name="android.hardware.bluetooth_le"
@@ -3,19 +3,21 @@ package eu.darken.capod.common.bluetooth
import android.bluetooth.le.ScanResult
import android.os.Parcelable
import androidx.core.util.forEach
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.serialization.InstantEpochMillisSerializer
import eu.darken.capod.common.serialization.MapIntByteArrayBase64Serializer
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
@Parcelize
@JsonClass(generateAdapter = true)
@Serializable
data class BleScanResult(
@Json(name = "receivedAt") val receivedAt: Instant,
@Json(name = "address") val address: String,
@Json(name = "rssi") val rssi: Int,
@Json(name = "generatedAtNanos") val generatedAtNanos: Long,
@Json(name = "manufacturerSpecificData") val manufacturerSpecificData: Map<Int, ByteArray>
@SerialName("receivedAt") @Serializable(with = InstantEpochMillisSerializer::class) val receivedAt: Instant,
@SerialName("address") val address: String,
@SerialName("rssi") val rssi: Int,
@SerialName("generatedAtNanos") val generatedAtNanos: Long,
@SerialName("manufacturerSpecificData") @Serializable(with = MapIntByteArrayBase64Serializer::class) val manufacturerSpecificData: Map<Int, ByteArray>
) : Parcelable {
fun getManufacturerSpecificData(id: Int): ByteArray? = manufacturerSpecificData[id]
@@ -1,27 +1,24 @@
package eu.darken.capod.common.bluetooth
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class ScannerMode(
val identifier: String,
@StringRes val labelRes: Int
) {
@SerialName("scanner.mode.lowpower") @Json(name = "scanner.mode.lowpower") LOW_POWER(
@SerialName("scanner.mode.lowpower") LOW_POWER(
"scanner.mode.lowpower",
R.string.settings_scanner_mode_lowpower_label
),
@SerialName("scanner.mode.balanced") @Json(name = "scanner.mode.balanced") BALANCED(
@SerialName("scanner.mode.balanced") BALANCED(
"scanner.mode.balanced",
R.string.settings_scanner_mode_balanced_label
),
@SerialName("scanner.mode.lowlatency") @Json(name = "scanner.mode.lowlatency") LOW_LATENCY(
@SerialName("scanner.mode.lowlatency") LOW_LATENCY(
"scanner.mode.lowlatency",
R.string.settings_scanner_mode_lowlatency_label
),
@@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.CancellationException
@@ -57,21 +59,20 @@ class DebugSessionManager @Inject constructor(
activeDir = recorderState.currentLogDir,
recordingStartedAt = recorderState.recordingStartedAt,
)
val overlaid = applyOverlays(raw, zipping, failedZips)
applyOverlays(raw, zipping, failedZips)
}.replayingShare(appScope)
val orphans = findOrphans(overlaid, zipping)
if (orphans.isNotEmpty()) {
orphans.forEach { (id, _) -> pendingAutoZips.add(id) }
appScope.launch {
orphans.forEach { (id, dir) ->
init {
sessions.onEach { allSessions ->
val orphans = findOrphans(allSessions, zippingIds.value)
orphans.forEach { (id, dir) ->
if (pendingAutoZips.add(id)) {
log(TAG, INFO) { "Orphan session detected, auto-zipping: $id" }
zipSessionAsync(id, dir)
}
}
}
overlaid
}.replayingShare(appScope)
}.launchIn(appScope)
}
private fun applyOverlays(
sessions: List<DebugSession>,
@@ -226,6 +227,7 @@ class DebugSessionManager @Inject constructor(
}
}
failedZipIds.update { emptySet() }
pendingAutoZips.clear()
log(TAG) { "All stored logs deleted" }
refresh()
}
@@ -3,6 +3,7 @@ package eu.darken.capod.common.debug.recording.core
import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.annotation.VisibleForTesting
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId
@@ -48,7 +49,12 @@ class RecorderModule @Inject constructor(
private val internalState = DynamicStateFlow(TAG, appScope + dispatcherProvider.IO) {
val triggerFileExists = triggerFile.exists()
State(shouldRecord = triggerFileExists)
val persistedInfo = if (triggerFileExists) readTriggerFile() else null
State(
shouldRecord = triggerFileExists,
persistedLogDir = persistedInfo?.first,
recordingStartedAt = persistedInfo?.second ?: 0L,
)
}
val state: Flow<State> = internalState.flow
@@ -59,24 +65,46 @@ class RecorderModule @Inject constructor(
internalState.updateBlocking {
if (!isRecording && shouldRecord) {
val sessionDir = createSessionDir()
val isResume = persistedLogDir != null && persistedLogDir.exists()
val sessionDir = if (isResume) {
log(TAG, INFO) { "Resuming recording into existing session: $persistedLogDir" }
persistedLogDir
} else {
createSessionDir()
}
val logFile = File(sessionDir, "core.log")
val newRecorder = Recorder()
newRecorder.start(logFile)
triggerFile.createNewFile()
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
if (!isResume) {
val startTime = System.currentTimeMillis()
writeTriggerFile(sessionDir, startTime)
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
this@RecorderModule.currentLogDir = sessionDir
this@RecorderModule.currentLogDir = sessionDir
copy(
recorder = newRecorder,
currentLogDir = sessionDir,
recordingStartedAt = System.currentTimeMillis(),
)
copy(
recorder = newRecorder,
currentLogDir = sessionDir,
recordingStartedAt = startTime,
persistedLogDir = null,
)
} else {
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
this@RecorderModule.currentLogDir = sessionDir
copy(
recorder = newRecorder,
currentLogDir = sessionDir,
recordingStartedAt = if (recordingStartedAt > 0L) recordingStartedAt else System.currentTimeMillis(),
persistedLogDir = null,
)
}
} else if (!shouldRecord && isRecording) {
recorder!!.stop()
requireNotNull(recorder) { "Recorder is null despite isRecording" }.stop()
if (triggerFile.exists() && !triggerFile.delete()) {
log(TAG, ERROR) { "Failed to delete trigger file" }
@@ -133,7 +161,8 @@ class RecorderModule @Inject constructor(
internalState.updateBlocking {
copy(shouldRecord = true)
}
return internalState.flow.filter { it.isRecording }.first().currentLogDir!!
val state = internalState.flow.filter { it.isRecording }.first()
return requireNotNull(state.currentLogDir) { "Recording state has no logDir" }
}
suspend fun stopRecorder(): File? {
@@ -169,6 +198,7 @@ class RecorderModule @Inject constructor(
internal val recorder: Recorder? = null,
val currentLogDir: File? = null,
val recordingStartedAt: Long = 0L,
internal val persistedLogDir: File? = null,
) {
val isRecording: Boolean
get() = recorder != null
@@ -177,9 +207,59 @@ class RecorderModule @Inject constructor(
get() = recorder?.path
}
internal fun readTriggerFile(): Pair<File, Long>? = try {
parseTriggerContent(triggerFile.readText())
} catch (e: Exception) {
log(TAG, WARN) { "Failed to read trigger file: $e" }
null
}
private fun writeTriggerFile(sessionDir: File, startTime: Long) {
try {
triggerFile.writeText("${sessionDir.absolutePath}\n$startTime")
} catch (e: Exception) {
log(TAG, WARN) { "Failed to write trigger file: $e" }
try {
triggerFile.createNewFile()
} catch (e2: Exception) {
log(TAG, ERROR) { "Failed to create trigger file fallback: $e2" }
}
}
}
companion object {
internal val TAG = logTag("Debug", "Log", "Recorder", "Module")
private const val FORCE_FILE = "capod_force_debug_run"
private const val MIN_RECORDING_MS = 5_000L
@VisibleForTesting
internal fun parseTriggerContent(
content: String,
now: Long = System.currentTimeMillis(),
): Pair<File, Long>? {
val trimmed = content.trim()
if (trimmed.isEmpty()) return null
val lines = trimmed.lines()
if (lines.size < 2) {
log(TAG, WARN) { "Trigger file has unexpected format: $trimmed" }
return null
}
val dir = File(lines[0])
val timestamp = lines[1].toLongOrNull()
if (timestamp == null || timestamp !in 1..(now + 60_000L)) {
log(TAG, WARN) { "Trigger file has invalid timestamp: ts=$timestamp" }
return null
}
if (!dir.exists()) {
log(TAG, WARN) { "Trigger file references non-existent dir: ${lines[0]}" }
return null
}
return dir to timestamp
}
}
}
@@ -9,6 +9,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.debug.recording.core.DebugSession
@@ -132,8 +133,6 @@ class RecorderActivityVM @Inject constructor(
}
fun share() = launch {
val currentState = stater.flow.first()
val dir = currentState.logDir ?: return@launch
val sid = sessionId ?: return@launch
stater.updateBlocking { copy(isWorking = true) }
@@ -141,17 +140,21 @@ class RecorderActivityVM @Inject constructor(
try {
val uri = sessionManager.getZipUri(sid)
val displayName = stater.flow.first().logDir?.name ?: sid
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "application/zip"
addCategory(Intent.CATEGORY_DEFAULT)
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${dir.name}")
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - $displayName")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val chooserIntent = Intent.createChooser(intent, context.getString(R.string.support_debuglog_label))
events.tryEmit(Event.ShareIntent(chooserIntent))
} catch (e: Exception) {
log(TAG, WARN) { "Failed to share session $sid: ${e.message}" }
} finally {
stater.updateBlocking { copy(isWorking = false) }
}
@@ -390,7 +390,7 @@ private fun BottomActionBar(
onClick = onKeep,
modifier = Modifier.weight(1f),
) {
Text(text = stringResource(R.string.general_close_action))
Text(text = stringResource(R.string.debug_debuglog_screen_keep_action))
}
androidx.compose.material3.Button(
onClick = onShare,
@@ -16,6 +16,7 @@ enum class Permission(
@StringRes val labelRes: Int,
@StringRes val descriptionRes: Int,
val permissionId: String,
val isScanBlocking: Boolean = false,
val isGranted: (Context) -> Boolean = {
ContextCompat.checkSelfPermission(it, permissionId) == PackageManager.PERMISSION_GRANTED
},
@@ -26,6 +27,7 @@ enum class Permission(
labelRes = R.string.permission_bluetooth_label,
descriptionRes = R.string.permission_bluetooth_description,
permissionId = "android.permission.BLUETOOTH",
isScanBlocking = true,
),
BLUETOOTH_CONNECT(
minApiLevel = Build.VERSION_CODES.S,
@@ -38,6 +40,7 @@ enum class Permission(
labelRes = R.string.permission_bluetooth_scan_label,
descriptionRes = R.string.permission_bluetooth_scan_description,
permissionId = "android.permission.BLUETOOTH_SCAN",
isScanBlocking = true,
),
ACCESS_FINE_LOCATION(
minApiLevel = Build.VERSION_CODES.BASE,
@@ -45,6 +48,7 @@ enum class Permission(
labelRes = R.string.permission_access_fine_location_label,
descriptionRes = R.string.permission_access_fine_location_description,
permissionId = "android.permission.ACCESS_FINE_LOCATION",
isScanBlocking = true,
),
ACCESS_BACKGROUND_LOCATION(
minApiLevel = Build.VERSION_CODES.Q,
@@ -73,7 +77,7 @@ enum class Permission(
},
),
POST_NOTIFICATIONS(
minApiLevel = Build.VERSION_CODES.S,
minApiLevel = Build.VERSION_CODES.TIRAMISU,
labelRes = R.string.permission_post_notifications_label,
descriptionRes = R.string.permission_post_notifications_description,
permissionId = "android.permission.POST_NOTIFICATIONS",
@@ -1,16 +0,0 @@
package eu.darken.capod.common.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@Suppress("unused")
@OptIn(ExperimentalEncodingApi::class)
class ByteArrayAdapter {
@ToJson
fun toJson(obj: ByteArray): String = Base64.encode(obj)
@FromJson
fun fromJson(base64: String): ByteArray? = Base64.decode(base64)
}
@@ -1,13 +0,0 @@
package eu.darken.capod.common.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.time.Instant
class JavaInstantAdapter {
@ToJson
fun toJson(obj: Instant): Long = obj.toEpochMilli()
@FromJson
fun fromJson(epochMillis: Long): Instant = Instant.ofEpochMilli(epochMillis)
}
@@ -0,0 +1,23 @@
package eu.darken.capod.common.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object MapIntByteArrayBase64Serializer : KSerializer<Map<Int, ByteArray>> {
private val delegate = MapSerializer(Int.serializer(), ByteArrayBase64Serializer)
override val descriptor: SerialDescriptor = delegate.descriptor
override fun serialize(encoder: Encoder, value: Map<Int, ByteArray>) {
delegate.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): Map<Int, ByteArray> {
return delegate.deserialize(decoder)
}
}
@@ -1,116 +0,0 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package eu.darken.capod.common.serialization
import com.squareup.moshi.*
import java.lang.reflect.Type
import java.util.*
import javax.annotation.CheckReturnValue
import kotlin.apply
import kotlin.collections.isNotEmpty
import kotlin.collections.plus
import kotlin.collections.toTypedArray
import kotlin.io.use
import kotlin.jvm.javaClass
class NameBasedPolyJsonAdapterFactory<T> internal constructor(
val baseType: Class<T>,
val keyLabels: List<String> = emptyList(),
val subtypes: List<Type> = emptyList(),
) : JsonAdapter.Factory {
fun withSubtype(subtype: Class<out T>, label: String): NameBasedPolyJsonAdapterFactory<T> {
require(!keyLabels.contains(label)) { "Labels must be unique." }
return NameBasedPolyJsonAdapterFactory(
baseType = baseType,
keyLabels = keyLabels + label,
subtypes = subtypes + subtype,
)
}
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (Types.getRawType(type) != baseType || annotations.isNotEmpty()) {
return null
}
val jsonAdapters = ArrayList<JsonAdapter<Any>>(subtypes.size)
var i = 0
val size = subtypes.size
while (i < size) {
jsonAdapters.add(moshi.adapter(subtypes[i]))
i++
}
return PolymorphicJsonAdapter(
nameLabels = keyLabels,
subTypes = subtypes,
jsonAdapters = jsonAdapters,
).nullSafe()
}
internal class PolymorphicJsonAdapter(
val nameLabels: List<String>,
val subTypes: List<Type>,
val jsonAdapters: List<JsonAdapter<Any>>,
) : JsonAdapter<Any>() {
private val nameOptions: JsonReader.Options = JsonReader.Options.of(*nameLabels.toTypedArray())
override fun fromJson(reader: JsonReader): Any? {
val peeked = reader.peekJson().apply {
setFailOnUnknown(false)
}
val labelIndex = peeked.use(::labelIndex)
if (labelIndex == -1) {
throw JsonDataException("No matching Field names for $nameLabels")
}
return jsonAdapters[labelIndex].fromJson(reader)
}
private fun labelIndex(reader: JsonReader): Int {
reader.beginObject()
while (reader.hasNext()) {
val labelIndex = reader.selectName(nameOptions)
if (labelIndex == -1) {
reader.skipName()
reader.skipValue()
continue
}
return labelIndex
}
return -1
// throw JsonDataException("Missing label for $labelKey")
}
override fun toJson(writer: JsonWriter, value: Any?) {
val type = value!!.javaClass
val typeIndex = subTypes.indexOf(type)
if (typeIndex == -1) {
throw JsonDataException("No matching name label for $value. Valid labels are $nameLabels")
}
val adapter = jsonAdapters[typeIndex]
writer.beginObject()
val flattenToken = writer.beginFlatten()
adapter.toJson(writer, value)
writer.endFlatten(flattenToken)
writer.endObject()
}
override fun toString(): String = "KeyBasedPolyJsonAdapterFactory($nameLabels)"
}
companion object {
@CheckReturnValue
fun <T> of(baseType: Class<T>): NameBasedPolyJsonAdapterFactory<T> = NameBasedPolyJsonAdapterFactory(
baseType
)
}
}
@@ -1,11 +1,9 @@
package eu.darken.capod.common.serialization
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.profiles.core.DeviceProfile
import kotlinx.serialization.json.Json
import javax.inject.Qualifier
import javax.inject.Singleton
@@ -14,14 +12,6 @@ import javax.inject.Singleton
@Module
class SerializationModule {
@Provides
@Singleton
fun moshi(): Moshi = Moshi.Builder().apply {
add(JavaInstantAdapter())
add(ByteArrayAdapter())
add(DeviceProfile.MOSHI_FACTORY)
}.build()
@Provides
@Singleton
@SerializationCapod
@@ -29,6 +19,7 @@ class SerializationModule {
ignoreUnknownKeys = true
encodeDefaults = true
explicitNulls = false
classDiscriminator = "type"
}
}
@@ -1,18 +1,15 @@
package eu.darken.capod.common.theming
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class ThemeColor(
@StringRes val labelRes: Int
) {
@SerialName("theme.color.blue") @Json(name = "theme.color.blue") BLUE(R.string.ui_theme_color_blue_label),
@SerialName("theme.color.green") @Json(name = "theme.color.green") GREEN(R.string.ui_theme_color_green_label),
@SerialName("theme.color.amber") @Json(name = "theme.color.amber") AMBER(R.string.ui_theme_color_amber_label),
@SerialName("theme.color.blue") BLUE(R.string.ui_theme_color_blue_label),
@SerialName("theme.color.green") GREEN(R.string.ui_theme_color_green_label),
@SerialName("theme.color.amber") AMBER(R.string.ui_theme_color_amber_label),
}
@@ -1,18 +1,15 @@
package eu.darken.capod.common.theming
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class ThemeMode(
@StringRes val labelRes: Int
) {
@SerialName("theme.mode.system") @Json(name = "theme.mode.system") SYSTEM(R.string.ui_theme_mode_system_label),
@SerialName("theme.mode.dark") @Json(name = "theme.mode.dark") DARK(R.string.ui_theme_mode_dark_label),
@SerialName("theme.mode.light") @Json(name = "theme.mode.light") LIGHT(R.string.ui_theme_mode_light_label),
@SerialName("theme.mode.system") SYSTEM(R.string.ui_theme_mode_system_label),
@SerialName("theme.mode.dark") DARK(R.string.ui_theme_mode_dark_label),
@SerialName("theme.mode.light") LIGHT(R.string.ui_theme_mode_light_label),
}
@@ -1,19 +1,16 @@
package eu.darken.capod.common.theming
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class ThemeStyle(
@StringRes val labelRes: Int
) {
@SerialName("theme.style.default") @Json(name = "theme.style.default") DEFAULT(R.string.ui_theme_style_default_label),
@SerialName("theme.style.materialyou") @Json(name = "theme.style.materialyou") MATERIAL_YOU(R.string.ui_theme_style_materialyou_label),
@SerialName("theme.style.mediumcontrast") @Json(name = "theme.style.mediumcontrast") MEDIUM_CONTRAST(R.string.ui_theme_style_medium_contrast_label),
@SerialName("theme.style.highcontrast") @Json(name = "theme.style.highcontrast") HIGH_CONTRAST(R.string.ui_theme_style_high_contrast_label),
@SerialName("theme.style.default") DEFAULT(R.string.ui_theme_style_default_label),
@SerialName("theme.style.materialyou") MATERIAL_YOU(R.string.ui_theme_style_materialyou_label),
@SerialName("theme.style.mediumcontrast") MEDIUM_CONTRAST(R.string.ui_theme_style_medium_contrast_label),
@SerialName("theme.style.highcontrast") HIGH_CONTRAST(R.string.ui_theme_style_high_contrast_label),
}
@@ -1,24 +1,21 @@
package eu.darken.capod.main.core
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class MonitorMode(
@StringRes val labelRes: Int
) {
@SerialName("monitor.mode.manual") @Json(name = "monitor.mode.manual") MANUAL(
@SerialName("monitor.mode.manual") MANUAL(
R.string.settings_monitor_mode_manual_label
),
@SerialName("monitor.mode.automatic") @Json(name = "monitor.mode.automatic") AUTOMATIC(
@SerialName("monitor.mode.automatic") AUTOMATIC(
R.string.settings_monitor_mode_automatic_label
),
@SerialName("monitor.mode.always") @Json(name = "monitor.mode.always") ALWAYS(
@SerialName("monitor.mode.always") ALWAYS(
R.string.settings_monitor_mode_always_label
),
}
@@ -10,6 +10,7 @@ import eu.darken.capod.reaction.core.ReactionSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.util.UUID
import javax.inject.Inject
@@ -42,6 +43,9 @@ class PermissionTool @Inject constructor(
}
.onEach { log(TAG) { "Missing permission: $it" } }
val missingScanPermissions: Flow<Set<Permission>> = missingPermissions
.map { perms -> perms.filter { it.isScanBlocking }.toSet() }
companion object {
private val TAG = logTag("PermissionTool")
}
@@ -14,9 +14,11 @@ import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.common.debug.logging.log
@@ -78,6 +80,10 @@ class MainActivity : Activity2() {
finish()
}
},
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider {
navigationEntries.forEach { entry ->
entry.apply {
@@ -195,7 +195,7 @@ fun OverviewScreen(
) {
// 1. Permission cards
items(
items = state.permissions.toList(),
items = state.permissions.sortedByDescending { it.isScanBlocking },
key = { it.permissionId },
) { permission ->
PermissionCard(
@@ -205,21 +205,21 @@ fun OverviewScreen(
}
// 2. Bluetooth disabled card
if (!state.isBluetoothEnabled && state.permissions.isEmpty()) {
if (!state.isBluetoothEnabled && !state.isScanBlocked) {
item(key = "bluetooth_disabled") {
BluetoothDisabledCard()
}
}
// 3. No profiles card
if (state.profiles.isEmpty() && state.permissions.isEmpty() && state.isBluetoothEnabled) {
if (state.profiles.isEmpty() && !state.isScanBlocked && state.isBluetoothEnabled) {
item(key = "no_profiles") {
NoProfilesCard(onManageDevices = onManageDevices)
}
}
// 4. Profiled device cards
if (state.permissions.isEmpty() && state.isBluetoothEnabled) {
if (!state.isScanBlocked && state.isBluetoothEnabled) {
items(
items = state.profiledDevices,
key = { it.identifier.hashCode() },
@@ -53,18 +53,23 @@ class OverviewViewModel @Inject constructor(
private val showUnmatchedDevices = MutableStateFlow(false)
val workerAutolaunch = permissionTool.missingPermissions
val workerAutolaunch = permissionTool.missingScanPermissions
.onEach { permissions ->
if (permissions.isNotEmpty()) {
log(TAG) { "Missing permissions: $permissions" }
log(TAG) { "Missing scan permissions: $permissions" }
return@onEach
}
val shouldStart = when (generalSettings.monitorMode.valueBlocking) {
MonitorMode.MANUAL -> false
MonitorMode.AUTOMATIC -> {
val devices = withTimeoutOrNull(5_000) { bluetoothManager.connectedDevices.first() }
devices?.isNotEmpty() == true
try {
val devices = withTimeoutOrNull(5_000) { bluetoothManager.connectedDevices.first() }
devices?.isNotEmpty() == true
} catch (e: SecurityException) {
log(TAG) { "Can't check connected devices without BLUETOOTH_CONNECT: ${e.message}" }
false
}
}
MonitorMode.ALWAYS -> true
}
@@ -82,7 +87,7 @@ class OverviewViewModel @Inject constructor(
}
}
private val pods = permissionTool.missingPermissions
private val pods = permissionTool.missingScanPermissions
.flatMapLatest { permissions ->
if (permissions.isNotEmpty()) {
return@flatMapLatest flowOf(emptyList())
@@ -124,6 +129,7 @@ class OverviewViewModel @Inject constructor(
val upgradeInfo: UpgradeRepo.Info,
val showUnmatchedDevices: Boolean,
) {
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
val profiledDevices: List<PodDevice> get() = devices.filter { it.meta.profile != null }
val unmatchedDevices: List<PodDevice> get() = devices.filter { it.meta.profile == null }
}
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -24,10 +25,17 @@ fun PermissionCard(
permission: Permission,
onRequest: (Permission) -> Unit,
) {
val colors = if (permission.isScanBlocking) {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
} else {
CardDefaults.cardColors()
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = colors,
) {
Column(
modifier = Modifier.padding(16.dp),
@@ -138,13 +138,6 @@ fun AcknowledgementsScreen(
onClick = { onOpenUrl("https://github.com/google/dagger") },
)
}
item {
SettingsBaseItem(
title = "Moshi",
subtitle = "A modern JSON library for Kotlin and Java. (APACHE 2.0)",
onClick = { onOpenUrl("https://github.com/square/moshi") },
)
}
item {
SettingsBaseItem(
title = "Android",
@@ -1,8 +1,6 @@
package eu.darken.capod.monitor.core
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.coroutine.DispatcherProvider
@@ -11,10 +9,12 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.serialization.SerializationCapod
import eu.darken.capod.profiles.core.ProfileId
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@@ -23,12 +23,11 @@ import javax.inject.Singleton
class PodDeviceCache @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatcherProvider: DispatcherProvider,
moshi: Moshi,
@SerializationCapod private val json: Json,
) {
private val cacheDir by lazy {
File(context.cacheDir, "device_cache").apply { mkdirs() }
}
private val jsonAdapter = moshi.adapter<BleScanResult>()
private val lock = Mutex()
private fun ProfileId.toCacheFile(): File = File(cacheDir, "profile_${this}.json")
@@ -40,7 +39,7 @@ class PodDeviceCache @Inject constructor(
if (!cacheFile.exists()) return@withLock null
try {
val raw = cacheFile.readText()
jsonAdapter.fromJson(raw)
json.decodeFromString<BleScanResult>(raw)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to read profile $id device: ${e.asLog()}, deleting corrupted cache file" }
cacheFile.delete()
@@ -56,8 +55,8 @@ class PodDeviceCache @Inject constructor(
log(TAG, VERBOSE) { "save(id=$id, device=$device)" }
val cacheFile = id.toCacheFile()
try {
val json = jsonAdapter.toJson(device)
cacheFile.writeText(json)
val encoded = json.encodeToString(BleScanResult.serializer(), device)
cacheFile.writeText(encoded)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to save profile $id device $device: ${e.asLog()}" }
cacheFile.delete()
@@ -55,11 +55,11 @@ class PodMonitor @Inject constructor(
private val cacheLock = Mutex()
val devices: Flow<List<PodDevice>> = combine(
permissionTool.missingPermissions,
permissionTool.missingScanPermissions,
bluetoothManager.isBluetoothEnabled
) { missingPermissions, isBluetoothEnabled ->
log(TAG) { "devices: missingPermissions=$missingPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
missingPermissions.isEmpty() && isBluetoothEnabled
) { missingScanPermissions, isBluetoothEnabled ->
log(TAG) { "devices: missingScanPermissions=$missingScanPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
missingScanPermissions.isEmpty() && isBluetoothEnabled
}
.flatMapLatest { isReady ->
if (!isReady) {
@@ -34,7 +34,12 @@ class BluetoothEventReceiver : BroadcastReceiver() {
} else {
log { "Event related to $bluetoothDevice" }
}
val supportedFeatures = ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) }
val supportedFeatures = try {
ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) }
} catch (e: SecurityException) {
log(TAG, WARN) { "Missing BLUETOOTH_CONNECT, can't check device features: ${e.message}" }
return
}
if (supportedFeatures.isEmpty()) {
log(TAG) { "Device has no features we support." }
@@ -22,7 +22,7 @@ class MonitorControl @Inject constructor(
log(TAG, VERBOSE) { "startMonitor(forceStart=$forceStart)" }
val hasBluetoothPermission =
Permission.BLUETOOTH.isGranted(context) || Permission.BLUETOOTH_CONNECT.isGranted(context)
Permission.BLUETOOTH.isGranted(context) || Permission.BLUETOOTH_SCAN.isGranted(context)
if (!hasBluetoothPermission) {
log(TAG, WARN) { "Missing Bluetooth permission, not starting monitor service." }
return
@@ -162,9 +162,9 @@ class MonitorService : Service() {
}
private suspend fun doMonitor() {
val permissionsMissingOnStart = permissionTool.missingPermissions.first()
val permissionsMissingOnStart = permissionTool.missingScanPermissions.first()
if (permissionsMissingOnStart.isNotEmpty()) {
log(TAG, WARN) { "Aborting, missing permissions: $permissionsMissingOnStart" }
log(TAG, WARN) { "Aborting, missing scan permissions: $permissionsMissingOnStart" }
return
}
@@ -190,10 +190,10 @@ class MonitorService : Service() {
}
.launchIn(monitorScope)
permissionTool.missingPermissions
permissionTool.missingScanPermissions
.flatMapLatest { missingPermsFlow ->
if (missingPermsFlow.isNotEmpty()) {
log(TAG, WARN) { "Aborting, permissions are missing: $missingPermsFlow" }
log(TAG, WARN) { "Aborting, scan permissions are missing: $missingPermsFlow" }
monitorScope.coroutineContext.cancelChildren()
emptyFlow()
} else {
@@ -2,8 +2,6 @@ package eu.darken.capod.pods.core
import android.content.Context
import androidx.annotation.DrawableRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BluetoothAddress
@@ -77,112 +75,111 @@ interface PodDevice {
value class Id(private val id: UUID = UUID.randomUUID())
@Serializable
@JsonClass(generateAdapter = false)
enum class Model(
val label: String,
@DrawableRes val iconRes: Int = R.drawable.device_earbuds_generic_both,
) {
@SerialName("airpods.gen1") @Json(name = "airpods.gen1") AIRPODS_GEN1(
@SerialName("airpods.gen1") AIRPODS_GEN1(
label = "AirPods (Gen 1)",
iconRes = R.drawable.device_airpods_gen1_both,
),
@SerialName("airpods.gen2") @Json(name = "airpods.gen2") AIRPODS_GEN2(
@SerialName("airpods.gen2") AIRPODS_GEN2(
"AirPods (Gen 2)",
R.drawable.device_airpods_gen1_both,
),
@SerialName("airpods.gen3") @Json(name = "airpods.gen3") AIRPODS_GEN3(
@SerialName("airpods.gen3") AIRPODS_GEN3(
"AirPods (Gen 3)",
R.drawable.device_airpods_gen3_both,
),
@SerialName("airpods.gen4") @Json(name = "airpods.gen4") AIRPODS_GEN4(
@SerialName("airpods.gen4") AIRPODS_GEN4(
"AirPods (Gen 4)",
R.drawable.device_airpods_gen3_both,
),
@SerialName("airpods.gen4.anc") @Json(name = "airpods.gen4.anc") AIRPODS_GEN4_ANC(
@SerialName("airpods.gen4.anc") AIRPODS_GEN4_ANC(
"AirPods (Gen 4 ANC)",
R.drawable.device_airpods_gen4anc_both,
),
@SerialName("airpods.pro") @Json(name = "airpods.pro") AIRPODS_PRO(
@SerialName("airpods.pro") AIRPODS_PRO(
"AirPods Pro",
R.drawable.device_airpods_pro2_both
),
@SerialName("airpods.pro2") @Json(name = "airpods.pro2") AIRPODS_PRO2(
@SerialName("airpods.pro2") AIRPODS_PRO2(
"AirPods Pro 2",
R.drawable.device_airpods_pro2_both
),
@SerialName("airpods.pro2.usbc") @Json(name = "airpods.pro2.usbc") AIRPODS_PRO2_USBC(
@SerialName("airpods.pro2.usbc") AIRPODS_PRO2_USBC(
"AirPods Pro 2 USB-C",
R.drawable.device_airpods_pro2_both
),
@SerialName("airpods.pro3") @Json(name = "airpods.pro3") AIRPODS_PRO3(
@SerialName("airpods.pro3") AIRPODS_PRO3(
"AirPods Pro 3",
R.drawable.device_airpods_pro2_both
),
@SerialName("airpods.max") @Json(name = "airpods.max") AIRPODS_MAX(
@SerialName("airpods.max") AIRPODS_MAX(
"AirPods Max",
R.drawable.device_airpods_max
),
@SerialName("airpods.max.usbc") @Json(name = "airpods.max.usbc") AIRPODS_MAX_USBC(
@SerialName("airpods.max.usbc") AIRPODS_MAX_USBC(
"AirPods Max USB-C",
R.drawable.device_airpods_max
),
@SerialName("beats.flex") @Json(name = "beats.flex") BEATS_FLEX(
@SerialName("beats.flex") BEATS_FLEX(
"Beats Flex",
R.drawable.device_beats_earbuds,
),
@SerialName("beats.solo.3") @Json(name = "beats.solo.3") BEATS_SOLO_3(
@SerialName("beats.solo.3") BEATS_SOLO_3(
"Beats Solo 3",
R.drawable.device_beats_headphones,
),
@SerialName("beats.studio.3") @Json(name = "beats.studio.3") BEATS_STUDIO_3(
@SerialName("beats.studio.3") BEATS_STUDIO_3(
"Beats Studio 3",
R.drawable.device_beats_studio3,
),
@SerialName("beats.x") @Json(name = "beats.x") BEATS_X(
@SerialName("beats.x") BEATS_X(
"Beats X",
R.drawable.device_beats_x,
),
@SerialName("beats.powerbeats.3") @Json(name = "beats.powerbeats.3") POWERBEATS_3(
@SerialName("beats.powerbeats.3") POWERBEATS_3(
"Power Beats 3",
R.drawable.device_powerbeats_3,
),
@SerialName("beats.powerbeats.4") @Json(name = "beats.powerbeats.4") POWERBEATS_4(
@SerialName("beats.powerbeats.4") POWERBEATS_4(
"Power Beats 4",
R.drawable.device_powerbeats_4,
),
@SerialName("beats.powerbeats.pro") @Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
@SerialName("beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro",
R.drawable.device_powerbeats_pro_both,
),
@SerialName("beats.powerbeats.pro2") @Json(name = "beats.powerbeats.pro2") POWERBEATS_PRO2(
@SerialName("beats.powerbeats.pro2") POWERBEATS_PRO2(
"Power Beats Pro 2",
R.drawable.device_powerbeats_pro2_both,
),
@SerialName("beats.fit.pro") @Json(name = "beats.fit.pro") BEATS_FIT_PRO(
@SerialName("beats.fit.pro") BEATS_FIT_PRO(
"Beats Fit Pro",
R.drawable.device_beats_fitpro_both,
),
@SerialName("fakes.tws.i99999") @Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
@SerialName("fakes.tws.i99999") FAKE_AIRPODS_GEN1(
"AirPods (Gen 1)? \uD83C\uDFAD",
R.drawable.device_airpods_gen1_both,
),
@SerialName("fakes.generic.airpods.gen2") @Json(name = "fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
@SerialName("fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
"AirPods (Gen 2)? \uD83C\uDFAD",
R.drawable.device_airpods_gen1_both,
),
@SerialName("fakes.generic.airpods.gen3") @Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
@SerialName("fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
"AirPods (Gen 3)? \uD83C\uDFAD",
R.drawable.device_airpods_gen3_both,
),
@SerialName("fakes.varunr.airpodspro") @Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
@SerialName("fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
"AirPods Pro? \uD83C\uDFAD",
R.drawable.device_airpods_pro2_both,
),
@SerialName("fakes.generic.airpods.pro2") @Json(name = "fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
@SerialName("fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
"AirPods Pro2? \uD83C\uDFAD",
R.drawable.device_airpods_pro2_both,
),
@SerialName("unknown") @Json(name = "unknown") UNKNOWN(
@SerialName("unknown") UNKNOWN(
"Unknown"
);
}
@@ -1,7 +1,5 @@
package eu.darken.capod.profiles.core
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.serialization.ByteArrayBase64Serializer
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey
@@ -14,14 +12,13 @@ import java.util.UUID
@Parcelize
@Serializable
@SerialName("apple")
@JsonClass(generateAdapter = true)
data class AppleDeviceProfile(
@Json(name = "id") override val id: ProfileId = UUID.randomUUID().toString(),
@Json(name = "label") override val label: String,
@Json(name = "priority") override val priority: Int = 0,
@Json(name = "model") override val model: PodDevice.Model = PodDevice.Model.UNKNOWN,
@Json(name = "minimumSignalQuality") override val minimumSignalQuality: Float = DeviceProfile.DEFAULT_MINIMUM_SIGNAL_QUALITY,
@Serializable(with = ByteArrayBase64Serializer::class) @Json(name = "identityKey") val identityKey: IdentityResolvingKey? = null,
@Serializable(with = ByteArrayBase64Serializer::class) @Json(name = "encryptionKey") val encryptionKey: ProximityEncryptionKey? = null,
@Json(name = "address") override val address: String? = null,
@SerialName("id") override val id: ProfileId = UUID.randomUUID().toString(),
@SerialName("label") override val label: String,
@SerialName("priority") override val priority: Int = 0,
@SerialName("model") override val model: PodDevice.Model = PodDevice.Model.UNKNOWN,
@SerialName("minimumSignalQuality") override val minimumSignalQuality: Float = DeviceProfile.DEFAULT_MINIMUM_SIGNAL_QUALITY,
@SerialName("identityKey") @Serializable(with = ByteArrayBase64Serializer::class) val identityKey: IdentityResolvingKey? = null,
@SerialName("encryptionKey") @Serializable(with = ByteArrayBase64Serializer::class) val encryptionKey: ProximityEncryptionKey? = null,
@SerialName("address") override val address: String? = null,
) : DeviceProfile
@@ -1,9 +1,7 @@
package eu.darken.capod.profiles.core
import android.os.Parcelable
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import eu.darken.capod.pods.core.PodDevice
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@@ -17,8 +15,5 @@ sealed interface DeviceProfile : Parcelable {
companion object {
const val DEFAULT_MINIMUM_SIGNAL_QUALITY = 0.15f
val MOSHI_FACTORY = PolymorphicJsonAdapterFactory.of(DeviceProfile::class.java, "type")
.withSubtype(AppleDeviceProfile::class.java, "apple")
}
}
@@ -1,11 +1,9 @@
package eu.darken.capod.profiles.core
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class DeviceProfilesContainer(
@Json(name = "profiles") val profiles: List<DeviceProfile> = emptyList()
@SerialName("profiles") val profiles: List<DeviceProfile> = emptyList()
)
@@ -1,27 +1,24 @@
package eu.darken.capod.reaction.core.autoconnect
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.R
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = false)
enum class AutoConnectCondition(
val identifier: String,
@StringRes val labelRes: Int
) {
@SerialName("autoconnect.condition.seen") @Json(name = "autoconnect.condition.seen") WHEN_SEEN(
@SerialName("autoconnect.condition.seen") WHEN_SEEN(
"monitor.mode.manual",
R.string.settings_reaction_autoconnect_whenseen_label
),
@SerialName("autoconnect.condition.case") @Json(name = "autoconnect.condition.case") CASE_OPEN(
@SerialName("autoconnect.condition.case") CASE_OPEN(
"autoconnect.condition.case",
R.string.settings_reaction_autoconnect_caseopen_label
),
@SerialName("autoconnect.condition.inear") @Json(name = "autoconnect.condition.inear") IN_EAR(
@SerialName("autoconnect.condition.inear") IN_EAR(
"autoconnect.condition.inear",
R.string.settings_reaction_autoconnect_inear_label
),
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Is die ontfoutrekord suksesvol gestuur? Die rekord sal uitgevee word.</string>
<string name="support_contact_sent_title">E-pos gestuur?</string>
<string name="support_contact_sent_message">Is die e-pos suksesvol gestuur? Die aangehegte ontfoutrekord sal uitgevee word.</string>
<string name="debug_debuglog_screen_keep_action">Hou</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">የዲባግ ምዝግብ ማስታወሻ በተሳካ ሁኔታ ተልኳል? ምዝግብ ማስታወሻው ይሰረዛል።</string>
<string name="support_contact_sent_title">ኢሜይል ተልኳል?</string>
<string name="support_contact_sent_message">ኢሜይሉ በተሳካ ሁኔታ ተልኳል? ተያይዞ የቀረበው የዲባግ ምዝግብ ማስታወሻ ይሰረዛል።</string>
<string name="debug_debuglog_screen_keep_action">ያቆዩ</string>
</resources>
+1
View File
@@ -377,4 +377,5 @@
<string name="support_debuglog_sent_message">هل تم إرسال سجل التصحيح بنجاح؟ سيتم حذف السجل.</string>
<string name="support_contact_sent_title">هل تم إرسال البريد الإلكتروني؟</string>
<string name="support_contact_sent_message">هل تم إرسال البريد الإلكتروني بنجاح؟ سيتم حذف سجل التصحيح المرفق.</string>
<string name="debug_debuglog_screen_keep_action">احتفظ</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Sazlama jurnalı uğurla göndərildi? Jurnal silinəcək.</string>
<string name="support_contact_sent_title">E-poçt göndərildi?</string>
<string name="support_contact_sent_message">E-poçt uğurla göndərildi? Əlavə edilmiş sazlama jurnalı silinəcək.</string>
<string name="debug_debuglog_screen_keep_action">Saxla</string>
</resources>
+1
View File
@@ -369,4 +369,5 @@
<string name="support_debuglog_sent_message">Адладачны журнал быў паспяхова адпраўлены? Журнал будзе выдалены.</string>
<string name="support_contact_sent_title">Электронны ліст адпраўлены?</string>
<string name="support_contact_sent_message">Электронны ліст быў паспяхова адпраўлены? Прыкладзены адладачны журнал будзе выдалены.</string>
<string name="debug_debuglog_screen_keep_action">Захаваць</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Изпратен ли е успешно дневникът за отстраняване на грешки? Логът ще бъде изтрит.</string>
<string name="support_contact_sent_title">Имейлът изпратен ли е?</string>
<string name="support_contact_sent_message">Изпратен ли е успешно имейлът? Прикаченият дневник за отстраняване на грешки ще бъде изтрит.</string>
<string name="debug_debuglog_screen_keep_action">Запази</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">ডিবাগ লগটি সফলভাবে পাঠানো হয়েছে? লগটি মুছে ফেলা হবে।</string>
<string name="support_contact_sent_title">ইমেইল পাঠানো হয়েছে?</string>
<string name="support_contact_sent_message">ইমেইলটি সফলভাবে পাঠানো হয়েছে? সংযুক্ত ডিবাগ লগটি মুছে ফেলা হবে।</string>
<string name="debug_debuglog_screen_keep_action">রাখুন</string>
</resources>
+3 -2
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d fitxers preparats (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Elimina</string>
<string name="debug_debuglog_screen_keep_action">Conserva</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d registre de depuració (%2$s)</item>
@@ -358,7 +359,7 @@
<string name="support_contact_debuglog_delete_message">Aquest registre de depuració se suprimrirà permanentment.</string>
<!-- Post-share confirmation -->
<string name="support_debuglog_sent_title">S\'ha enviat el registre?</string>
<string name="support_debuglog_sent_message">S\'ha enviat correctament el registre de depuració? El registre s\'eliminarà.</string>
<string name="support_debuglog_sent_message">El registre de depuració s\'ha enviat correctament? El registre s\'eliminarà.</string>
<string name="support_contact_sent_title">S\'ha enviat el correu electrònic?</string>
<string name="support_contact_sent_message">S\'ha enviat correctament el correu electrònic? El registre de depuració adjunt s\'eliminarà.</string>
<string name="support_contact_sent_message">El correu electrònic s\'ha enviat correctament? El registre de depuració adjunt s\'eliminarà.</string>
</resources>
+3 -2
View File
@@ -315,6 +315,7 @@
<item quantity="other">%1$d souborů připraveno (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Smazat</string>
<string name="debug_debuglog_screen_keep_action">Ponechat</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d log ladění (%2$s)</item>
@@ -366,7 +367,7 @@
<string name="support_contact_debuglog_delete_message">Tento log ladění bude trvale smazán.</string>
<!-- Post-share confirmation -->
<string name="support_debuglog_sent_title">Log odeslán?</string>
<string name="support_debuglog_sent_message">Byl protokol ladění úspěšně odeslán? Protokol bude smazán.</string>
<string name="support_debuglog_sent_message">Byl log ladění úspěšně odeslán? Log bude smazán.</string>
<string name="support_contact_sent_title">E-mail odeslán?</string>
<string name="support_contact_sent_message">Byl e-mail úspěšně odeslán? Přiložený protokol ladění bude smazán.</string>
<string name="support_contact_sent_message">Byl e-mail úspěšně odeslán? Přiložený log ladění bude smazán.</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Blev fejlloggen sendt korrekt? Loggen vil blive slettet.</string>
<string name="support_contact_sent_title">E-mail sendt?</string>
<string name="support_contact_sent_message">Blev e-mailen sendt korrekt? Den vedhæftede fejllog vil blive slettet.</string>
<string name="debug_debuglog_screen_keep_action">Behold</string>
</resources>
+2 -1
View File
@@ -31,7 +31,7 @@
<string name="upgrade_screen_subscription_action_hint">%s/Jahr</string>
<string name="upgrade_screen_iap_action">Einmalig kaufen</string>
<string name="upgrade_screen_iap_action_hint">Einmaliger Kauf: %s</string>
<string name="upgrade_screen_restore_purchase_action">Auswahl wiederherstellen</string>
<string name="upgrade_screen_restore_purchase_action">Kauf wiederherstellen</string>
<string name="upgrade_screen_restore_purchase_message">Keine Käufe gefunden. Verwendest Du das richtige Konto?</string>
<string name="upgrade_screen_restore_troubleshooting_msg">Wenn du kürzlich gekauft hast, kann es einen Moment dauern, bis Google Play synchronisiert.</string>
<string name="upgrade_screen_restore_sync_patience_hint">Versuche es in wenigen Minuten erneut, falls dein Kauf nicht erscheint.</string>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Wurde das Debug-Protokoll erfolgreich gesendet? Das Protokoll wird gelöscht.</string>
<string name="support_contact_sent_title">E-Mail gesendet?</string>
<string name="support_contact_sent_message">Wurde die E-Mail erfolgreich gesendet? Das angehängte Debug-Protokoll wird gelöscht.</string>
<string name="debug_debuglog_screen_keep_action">Behalten</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Στάλθηκε επιτυχώς το αρχείο καταγραφής εντοπισμού σφαλμάτων; Το αρχείο καταγραφής θα διαγραφεί.</string>
<string name="support_contact_sent_title">Email εστάλη;</string>
<string name="support_contact_sent_message">Στάλθηκε επιτυχώς το email; Το συνημμένο αρχείο καταγραφής εντοπισμού σφαλμάτων θα διαγραφεί.</string>
<string name="debug_debuglog_screen_keep_action">Διατήρηση</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">¿Se envió el log de depuración correctamente? El log será eliminado.</string>
<string name="support_contact_sent_title">¿Email enviado?</string>
<string name="support_contact_sent_message">¿Se envió el email correctamente? El log de depuración adjunto será eliminado.</string>
<string name="debug_debuglog_screen_keep_action">Conservar</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">¿El registro de depuración se envió correctamente? El registro será eliminado.</string>
<string name="support_contact_sent_title">¿Correo enviado?</string>
<string name="support_contact_sent_message">¿El correo se envió correctamente? El registro de depuración adjunto será eliminado.</string>
<string name="debug_debuglog_screen_keep_action">Conservar</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">¿Se envió el registro de depuración correctamente? El registro se eliminará.</string>
<string name="support_contact_sent_title">¿Correo enviado?</string>
<string name="support_contact_sent_message">¿Se envió el correo electrónico correctamente? El registro de depuración adjunto se eliminará.</string>
<string name="debug_debuglog_screen_keep_action">Conservar</string>
</resources>
+6 -5
View File
@@ -251,7 +251,7 @@
<string name="profiles_drag_handle_description">Järjekorra muutmiseks lohista</string>
<string name="profiles_delete_title">Eemalda Profiil</string>
<string name="profiles_delete_message">Kas soovid kindlasti seda profiili kustutada? Seda toimingut ei saa tagasi võtta.</string>
<string name="profiles_delete_action">Delete</string>
<string name="profiles_delete_action">Kustuta</string>
<string name="profiles_basic_info_title">Seadme teave</string>
<string name="profiles_basic_info_description">Seadistage seadme nime, mudelit ja valikulist Bluetooth-sidumist.</string>
<string name="profiles_signal_quality_title">Väikseim signaali kvaliteet</string>
@@ -310,7 +310,7 @@
<item quantity="one">%1$d fail valmis (%2$s)</item>
<item quantity="other">%1$d faili valmis (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Delete</string>
<string name="debug_debuglog_screen_discard_action">Kustuta</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d silumislogi (%2$s)</item>
@@ -357,8 +357,9 @@
<string name="support_contact_debuglog_delete_title">Kustuta silumislogi?</string>
<string name="support_contact_debuglog_delete_message">See silumislogi kustutatakse jäädavalt.</string>
<!-- Post-share confirmation -->
<string name="support_debuglog_sent_title">Logi saadeti?</string>
<string name="support_debuglog_sent_title">Logi saadetud?</string>
<string name="support_debuglog_sent_message">Kas silumislogi saadeti edukalt? Logi kustutatakse.</string>
<string name="support_contact_sent_title">E-kiri saadeti?</string>
<string name="support_contact_sent_message">Kas e-kiri saadeti edukalt? Manustatud silumislogi kustutatakse.</string>
<string name="support_contact_sent_title">Meil saadetud?</string>
<string name="support_contact_sent_message">Kas meil saadeti edukalt? Manustatud silumislogi kustutatakse.</string>
<string name="debug_debuglog_screen_keep_action">Hoia alles</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Arazketa-loga behar bezala bidali al da? Loga ezabatuko da.</string>
<string name="support_contact_sent_title">Mezu elektronikoa bidali da?</string>
<string name="support_contact_sent_message">Mezu elektronikoa behar bezala bidali al da? Erantsitako arazketa-loga ezabatuko da.</string>
<string name="debug_debuglog_screen_keep_action">Gorde</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">آیا لاگ اشکال‌زدایی با موفقیت ارسال شد؟ لاگ حذف خواهد شد.</string>
<string name="support_contact_sent_title">ایمیل ارسال شد؟</string>
<string name="support_contact_sent_message">آیا ایمیل با موفقیت ارسال شد؟ لاگ اشکال‌زدایی پیوست‌شده حذف خواهد شد.</string>
<string name="debug_debuglog_screen_keep_action">نگه داشتن</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Lähetettiin virheenkorjausloki onnistuneesti? Loki poistetaan.</string>
<string name="support_contact_sent_title">Sähköposti lähetetty?</string>
<string name="support_contact_sent_message">Lähetettiin sähköposti onnistuneesti? Liitetty virheenkorjausloki poistetaan.</string>
<string name="debug_debuglog_screen_keep_action">Säilytä</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Matagumpay bang naipadala ang debug log? Ang log ay matatanggal.</string>
<string name="support_contact_sent_title">Naipadala na ang email?</string>
<string name="support_contact_sent_message">Matagumpay bang naipadala ang email? Ang kalakip na debug log ay matatanggal.</string>
<string name="debug_debuglog_screen_keep_action">Itago</string>
</resources>
+34 -34
View File
@@ -6,7 +6,7 @@
<string name="general_copy_action">Copier</string>
<string name="general_thank_you_label">Merci</string>
<string name="general_upgrade_action">Passer Pro</string>
<string name="common_feature_requires_pro_msg">Mettez à niveau pour déverrouiller.</string>
<string name="common_feature_requires_pro_msg">Passez à la version Pro pour débloquer loption.</string>
<string name="general_donate_action">Faire un don</string>
<string name="general_check_action">Vérifier</string>
<string name="general_close_action">Fermer</string>
@@ -18,24 +18,24 @@
<string name="general_example_label">Exemple : %s</string>
<string name="upgrade_capod_label">Surclasser CAPod</string>
<string name="upgrade_capod_description">Obtenez des fonctions supplémentaires et soutenez le développeur.</string>
<string name="upgrade_preamble">CAPod est développé par une seule personne. La mise à niveau déverrouille des fonctionnalités supplémentaires et aide à maintenir l\'appli en vie.</string>
<string name="upgrade_preamble">CAPod est développée par une seule personne. Passer à la version Pro déverrouille des fonctions supplémentaires et contribue à la pérennité de lappli.</string>
<string name="upgrade_benefit_themes">Personnalisation du thème</string>
<string name="upgrade_benefit_autoplay">Lecture automatique &amp; pause automatique</string>
<string name="upgrade_benefit_autoplay">Lecture et pause automatiques</string>
<string name="upgrade_benefit_autoconnect">Connexion automatique</string>
<string name="upgrade_benefit_popups">Popup à l\'ouverture de l\'étui &amp; connexion</string>
<string name="upgrade_benefit_widgets">Widgets de l\'écran d\'accueil</string>
<string name="upgrade_benefit_popups">Fenêtre de notification à l\'ouverture de l\'étui et à la connexion</string>
<string name="upgrade_benefit_widgets">Widgets sur lécran daccueil</string>
<string name="upgrade_benefit_support">Soutenir le développeur</string>
<string name="upgrade_screen_options_description">Mêmes fonctionnalités, tarification différente. L\'abonnement comprend un essai gratuit et peut être annulé à tout moment.</string>
<string name="upgrade_screen_subscription_trial_action">Commencez votre essai gratuit</string>
<string name="upgrade_screen_subscription_action">S\'abonner annuellement</string>
<string name="upgrade_screen_options_description">Mêmes fonctions, tarification différente. Labonnement comprend un essai gratuit et peut être annulé nimporte quand.</string>
<string name="upgrade_screen_subscription_trial_action">Commencer lessai gratuit</string>
<string name="upgrade_screen_subscription_action">Abonnement annuel</string>
<string name="upgrade_screen_subscription_action_hint">%s/an</string>
<string name="upgrade_screen_iap_action">Acheter une fois</string>
<string name="upgrade_screen_iap_action_hint">Achat unique : %s</string>
<string name="upgrade_screen_restore_purchase_action">Rétablir lachat</string>
<string name="upgrade_screen_restore_purchase_message">Aucun achat na été trouvé. Utilisez-vous le bon compte?</string>
<string name="upgrade_screen_restore_troubleshooting_msg">Si vous avez récemment effectué un achat, Google Play peut mettre quelques instants à synchroniser.</string>
<string name="upgrade_screen_restore_sync_patience_hint">Réessayez dans quelques minutes si votre achat napparaît pas.</string>
<string name="upgrade_screen_restore_multiaccount_hint">Assurez-vous d’être connecté avec le même compte Google utilisé pour lachat.</string>
<string name="upgrade_screen_restore_troubleshooting_msg">Si lachat est récent, Google Play peut mettre du temps à synchroniser.</string>
<string name="upgrade_screen_restore_sync_patience_hint">Si votre achat napparaît pas, réessayez dans quelques minutes.</string>
<string name="upgrade_screen_restore_multiaccount_hint">Le même compte Google doit être utilisé pour lachat et la connexion.</string>
<string name="settings_monitor_mode_label">Mode surveillance</string>
<string name="settings_monitor_mode_description">Les circonstances de surveillance des données Bluetooth par lappli.</string>
<string name="settings_monitor_connected_notification_label">Notification supplémentaire</string>
@@ -96,7 +96,7 @@
<string name="settings_support_label">Assistance</string>
<string name="settings_support_description">Si vous avez besoin daide.</string>
<string name="settings_wiki_label">Wiki</string>
<string name="settings_wiki_description">FAQ &amp; Guides</string>
<string name="settings_wiki_description">FAQ et guides</string>
<string name="issue_tracker_label">Suivi des problèmes</string>
<string name="issue_tracker_description">Un système public de suivi des problèmes et des demandes de fonctions (anglais seulement).</string>
<string name="discord_label">Discord</string>
@@ -129,7 +129,7 @@
<string name="widget_configuration_title">Choisir un appareil</string>
<string name="widget_configuration_description">Choisissez le profil dappareil que ce widget doit afficher.</string>
<string name="widget_no_data_label">Aucune donnée</string>
<string name="widget_error_loading_label">Erreur lors du chargement du widget</string>
<string name="widget_error_loading_label">Erreur de chargement du widget</string>
<string name="widget_config_appearance_label">Apparence</string>
<string name="widget_config_preset_label">Préréglages</string>
<string name="widget_config_background_color_label">Couleur d\'arrière-plan</string>
@@ -264,8 +264,8 @@
<string name="profiles_maindevice_encryptionkey_explanation">Les AirPods envoient un message d’état, dont une partie est chiffrée. La clé de chiffrement permet à CAPod de déchiffrer lintégralité du message. Un accès ponctuel à un MacBook est nécessaire.</string>
<string name="profiles_key_invalid_format">Le format de la clé est invalide</string>
<string name="profiles_key_expected_format">Format attendu : %1$s</string>
<string name="profiles_key_status_configured">Configuré</string>
<string name="profiles_key_status_not_set">Non défini</string>
<string name="profiles_key_status_configured">Configurée</string>
<string name="profiles_key_status_not_set">Non définie</string>
<string name="profiles_signal_quality_min_label">Désactivé</string>
<string name="profiles_signal_quality_max_label">Strict</string>
<string name="profiles_priority_hint">L\'ordre de profil détermine la priorité. Faites glisser les profils pour les réorganiser - les profils plus élevés dans la liste ont la priorité lorsque plusieurs appareils correspondent.</string>
@@ -296,14 +296,14 @@
<string name="general_discard_action">Abandonner</string>
<string name="general_keep_editing_action">Continuer de modifier</string>
<!-- Short recording warning -->
<string name="debug_debuglog_short_recording_title">Enregistrement trop court</string>
<string name="debug_debuglog_short_recording_message">Un journal de débogage doit capturer le problème au moment où il se produit. Continuez lenregistrement, reproduisez le problème, puis arrêtez lenregistrement.</string>
<string name="debug_debuglog_short_recording_continue">Continuer lenregistrement</string>
<string name="debug_debuglog_short_recording_title">Lenregistrement est trop court</string>
<string name="debug_debuglog_short_recording_message">Un journal de débogage doit capturer le problème au moment où il se produit. Poursuivez lenregistrement, reproduisez le problème, puis arrêtez lenregistrement.</string>
<string name="debug_debuglog_short_recording_continue">Poursuivre lenregistrement</string>
<string name="debug_debuglog_short_recording_stop">Arrêter quand même</string>
<!-- Recorder screen -->
<string name="debug_debuglog_screen_title">Journal de débogage</string>
<string name="debug_debuglog_screen_subtitle">Exporter les informations de débogage pour le dépannage</string>
<string name="debug_debuglog_screen_sensitive_title">Informations sensibles</string>
<string name="debug_debuglog_screen_subtitle">Exporter les renseignements de débogage pour le dépannage</string>
<string name="debug_debuglog_screen_sensitive_title">Information sensible</string>
<string name="debug_debuglog_screen_session_path_label">Chemin de la session</string>
<string name="debug_debuglog_screen_log_files_label">Fichiers journaux</string>
<plurals name="debug_debuglog_screen_log_files_ready">
@@ -316,34 +316,34 @@
<item quantity="one">%1$d journal de débogage (%2$s)</item>
<item quantity="other">%1$d journaux de débogage (%2$s)</item>
</plurals>
<string name="support_debuglog_clear_action">Effacer les journaux de débogage enregistrés</string>
<string name="support_debuglog_clear_action">Supprimer les journaux de débogage enregistrés</string>
<string name="support_debuglog_sessions_label">Sessions de débogage</string>
<string name="support_debuglog_sessions_desc">Gérer les sessions de journal de débogage enregistrées</string>
<string name="support_debuglog_sessions_empty">Aucune session de débogage</string>
<string name="support_debuglog_session_recording">Enregistrement…</string>
<string name="support_debuglog_session_compressing">Compression…</string>
<string name="support_debuglog_session_delete_title">Supprimer la session ?</string>
<string name="support_debuglog_session_delete_message">Cette session de débogage sera définitivement supprimée.</string>
<string name="support_debuglog_clear_all_message">Toutes les sessions de débogage seront définitivement supprimées.</string>
<string name="support_debuglog_session_delete_message">Cette session de débogage sera supprimée irrémédiablement.</string>
<string name="support_debuglog_clear_all_message">Les sessions de débogage seront supprimées irrémédiablement.</string>
<string name="support_debuglog_failed_empty_log">Échec : le fichier journal est vide</string>
<string name="support_debuglog_failed_missing_log">Échec : le fichier journal est manquant</string>
<string name="support_debuglog_failed_missing_log">Échec : le fichier journal manque</string>
<string name="support_debuglog_failed_corrupt_zip">Échec : le fichier zip est corrompu</string>
<string name="support_debuglog_failed_zip_failed">Échec : la compression a échoué</string>
<!-- Contact form -->
<string name="support_contact_label">Contacter le développeur</string>
<string name="support_contact_desc">Remplissez un formulaire pour envoyer un courriel au développeur.</string>
<string name="support_contact_welcome">Je lis chaque message moi-même et fais de mon mieux pour répondre, mais comme je travaille seul sur ce projet, cela peut prendre un peu de temps. Merci pour votre patience.</string>
<string name="support_contact_welcome">Je lis chaque message et fais de mon mieux pour répondre. Comme jassure seul le développement, cela peut prendre un peu de temps. Merci pour votre patience.</string>
<string name="support_contact_footer">Votre message sera envoyé par courriel. Les renseignements sur lappareil et la configuration sont joints automatiquement. Vous pouvez joindre des captures d’écran ou une vidéo au courriel. Certaines parties du courriel seront en anglais afin que le développeur puisse facilement les comprendre.</string>
<string name="support_contact_category_label">Catégorie</string>
<string name="support_contact_category_question_label">Question</string>
<string name="support_contact_category_feature_label">Demande de fonction</string>
<string name="support_contact_category_bug_label">Relevé de bogue</string>
<string name="support_contact_description_label">Description</string>
<string name="support_contact_description_question_hint">Décrivez votre question en détail. Veuillez être précis.</string>
<string name="support_contact_description_feature_hint">Décrivez la fonctionnalité que vous souhaitez voir ajoutée. Veuillez être précis.</string>
<string name="support_contact_description_bug_hint">Décrivez ce qui sest passé et comment reproduire le problème. Veuillez être précis.</string>
<string name="support_contact_description_question_hint">Rédigez précisément votre question.</string>
<string name="support_contact_description_feature_hint">Décrivez précisément la fonction que vous souhaitez voir ajoutée .</string>
<string name="support_contact_description_bug_hint">Décrivez précisément ce qui sest passé et comment reproduire le problème.</string>
<string name="support_contact_expected_label">Comportement attendu</string>
<string name="support_contact_expected_hint">Décrivez ce que vous attendiez quil se passe.</string>
<string name="support_contact_expected_hint">Décrivez ce à quoi vous vous attendiez.</string>
<plurals name="support_contact_word_count">
<item quantity="one">%1$d / %2$d mot</item>
<item quantity="other">%1$d / %2$d mots</item>
@@ -355,10 +355,10 @@
<string name="support_contact_send_action">Ouvrir lappli de courriel</string>
<string name="support_contact_no_email_app">Aucune appli de courriel na été trouvée sur cet appareil.</string>
<string name="support_contact_debuglog_delete_title">Supprimer le journal de débogage ?</string>
<string name="support_contact_debuglog_delete_message">Ce journal de débogage sera définitivement supprimé.</string>
<string name="support_contact_debuglog_delete_message">Ce journal de débogage sera supprimé irrémédiablement.</string>
<!-- Post-share confirmation -->
<string name="support_debuglog_sent_title">Journal envoyé ?</string>
<string name="support_debuglog_sent_message">Le journal de débogage a-t-il été envoyé avec succès ? Le journal sera supprimé.</string>
<string name="support_contact_sent_title">E-mail envoyé ?</string>
<string name="support_contact_sent_message">L\'e-mail a-t-il été envoyé avec succès ? Le journal de débogage joint sera supprimé.</string>
<string name="support_debuglog_sent_title">Le journal a-t-il été envoyé?</string>
<string name="support_debuglog_sent_message">Le journal de débogage a-t-il été envoyé? Il sera supprimé.</string>
<string name="support_contact_sent_title">Le courriel a été envoyé?</string>
<string name="support_contact_sent_message">Le courriel a-t-il été envoyé? Le journal de débogage joint sera supprimé.</string>
</resources>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d ficheiros listos (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Eliminar</string>
<string name="debug_debuglog_screen_keep_action">Gardar</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d rexistro de depuración (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d फ़ाइलें तैयार (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">मिटाएं</string>
<string name="debug_debuglog_screen_keep_action">रखें</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d डीबग लॉग (%2$s)</item>
+1
View File
@@ -313,6 +313,7 @@
<item quantity="other">%1$d datoteka spremno (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Izbriši</string>
<string name="debug_debuglog_screen_keep_action">Zadrži</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d debug zapisnik (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d fájl kész (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Törlés</string>
<string name="debug_debuglog_screen_keep_action">Megőrzés</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d hibakeresési napló (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d Ֆայլեր պատրաստուաց են (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Ջնջել</string>
<string name="debug_debuglog_screen_keep_action">Պահպանել</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d Կարգաբերման մատյան (%2$s)</item>
+1
View File
@@ -309,6 +309,7 @@
<item quantity="other">%1$d file siap (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Hapus</string>
<string name="debug_debuglog_screen_keep_action">Simpan</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="other">%1$d log debug (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d skráar tilbúnar (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Eyða</string>
<string name="debug_debuglog_screen_keep_action">Geyma</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d kembingarskrá (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d file pronti (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Elimina</string>
<string name="debug_debuglog_screen_keep_action">Conserva</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d log di debug (%2$s)</item>
+1
View File
@@ -315,6 +315,7 @@
<item quantity="other">%1$d קבצים מוכנים (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">מחיקה</string>
<string name="debug_debuglog_screen_keep_action">שמור</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">יומן ניפוי באגים %1$d (%2$s)</item>
+1
View File
@@ -309,6 +309,7 @@
<item quantity="other">%1$d つのファイルが準備完了 (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">削除</string>
<string name="debug_debuglog_screen_keep_action">保持</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="other">%1$d件のデバッグログ (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d ფაილი მზადაა (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">წაშლა</string>
<string name="debug_debuglog_screen_keep_action">შენახვა</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d გამართვის ჟურნალი (%2$s)</item>
@@ -309,6 +309,7 @@
<item quantity="other">%1$d ឯកសាររួចរាល់ (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">លុប</string>
<string name="debug_debuglog_screen_keep_action">រក្សាទុក</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="other">%1$d កំណត់ហេតុបំបាត់កំហុស (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d pel amade (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Jêbirin</string>
<string name="debug_debuglog_screen_keep_action">Bihêle</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d tomara çareserkirinê (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d ಫೈಲ್‌ಗಳು ಸಿದ್ಧವಾಗಿವೆ (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">ಅಳಿಸಿ</string>
<string name="debug_debuglog_screen_keep_action">ಇರಿಸಿ</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d ಡೀಬಗ್ ಲಾಗ್ (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d개 파일 준비됨 (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">삭제</string>
<string name="debug_debuglog_screen_keep_action">유지</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="other">%1$d개 디버그 로그 (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d файл даяр (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Жойуу</string>
<string name="debug_debuglog_screen_keep_action">Сактоо</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d мүчүлүштүк жазылмасы (%2$s)</item>
+2 -1
View File
@@ -86,7 +86,7 @@
<string name="debug_debuglog_size_compressed_label">ຂະໜາດທີ່ບີບອັດ</string>
<string name="debug_notification_channel_label">ການແຈ້ງເຕືອນດີບັກ</string>
<string name="debug_debuglog_file_label">ໄຟລ໌ບັນທຶກທີ່ບັນທຶກໄວ້</string>
<string name="debug_debuglog_recording_progress">ລັງບັນທຶກບັນທຶກດີບັກ</string>
<string name="debug_debuglog_recording_progress">ໍາລັງບັນທຶກບັນທຶກດີບັກ</string>
<string name="debug_debuglog_record_action">ບັນທຶກບັນທຶກດີບັກ</string>
<string name="debug_debuglog_stop_action">ຢຸດການບັນທຶກ</string>
<string name="debug_debuglog_sensitive_information_message">ໄຟລ໌ທີ່ສ້າງຂຶ້ນມີຂໍ້ມູນທີ່ละเอียดອ່ອນ (ຕົວຢ່າງ: ລາຍລະອຽດອຸປະກອນ Bluetooth). ແບ່ງປັນມັນກັບຝ່າຍທີ່ເຊື່ອຖືໄດ້ເທົ່ານັ້ນ.</string>
@@ -309,6 +309,7 @@
<item quantity="other">%1$d ໄຟລ່ພ້ອມ (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">ລຶບ</string>
<string name="debug_debuglog_screen_keep_action">ເກັບຮັກສາ</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="other">%1$d ບັນທຶກດີບັກ (%2$s)</item>
+1
View File
@@ -315,6 +315,7 @@
<item quantity="other">%1$d failų paruošta (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Trinti</string>
<string name="debug_debuglog_screen_keep_action">Palikti</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d derinimo žurnalas (%2$s)</item>
+1
View File
@@ -313,6 +313,7 @@
<item quantity="other">%1$d faili gatavi (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Dzēst</string>
<string name="debug_debuglog_screen_keep_action">Paturēt</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="zero">%1$d atkļūdošanas žurnālu (%2$s)</item>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Дали логот за дебагирање е успешно испратен? Логот ќе биде избришан.</string>
<string name="support_contact_sent_title">Е-поштата е испратена?</string>
<string name="support_contact_sent_message">Дали е-поштата е успешно испратена? Приложениот лог за дебагирање ќе биде избришан.</string>
<string name="debug_debuglog_screen_keep_action">Задржи</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">ഡീബഗ് ലോഗ് വിജയകരമായി അയച്ചോ? ലോഗ് ഇല്ലാതാക്കപ്പെടും.</string>
<string name="support_contact_sent_title">ഇമെയിൽ അയച്ചോ?</string>
<string name="support_contact_sent_message">ഇമെയിൽ വിജയകരമായി അയച്ചോ? അറ്റാച്ച് ചെയ്ത ഡീബഗ് ലോഗ് ഇല്ലാതാക്കപ്പെടും.</string>
<string name="debug_debuglog_screen_keep_action">സൂക്ഷിക്കുക</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Дебаг лог амжилттай илгээгдсэн үү? Лог устгагдах болно.</string>
<string name="support_contact_sent_title">Имэйл илгээсэн үү?</string>
<string name="support_contact_sent_message">Имэйл амжилттай илгээгдсэн үү? Хавсаргасан дебаг лог устгагдах болно.</string>
<string name="debug_debuglog_screen_keep_action">Хадгалах</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">डीबग लॉग यशस्वीरित्या पाठवला का? लॉग हटवला जाईल.</string>
<string name="support_contact_sent_title">ईमेल पाठवला?</string>
<string name="support_contact_sent_message">ईमेल यशस्वीरित्या पाठवला का? संलग्न डीबग लॉग हटवला जाईल.</string>
<string name="debug_debuglog_screen_keep_action">ठेवा</string>
</resources>
+1
View File
@@ -357,4 +357,5 @@
<string name="support_debuglog_sent_message">Adakah log debug berjaya dihantar? Log akan dipadamkan.</string>
<string name="support_contact_sent_title">E-mel dihantar?</string>
<string name="support_contact_sent_message">Adakah e-mel berjaya dihantar? Log debug yang dilampirkan akan dipadamkan.</string>
<string name="debug_debuglog_screen_keep_action">Simpan</string>
</resources>
@@ -357,4 +357,5 @@
<string name="support_debuglog_sent_message">Debug log ကို အောင်မြင်စွာ ပို့ပြီးပြီလား? Log ကို ဖျက်ပစ်မည်။</string>
<string name="support_contact_sent_title">အီးမေးလ် ပို့ပြီးပြီလား?</string>
<string name="support_contact_sent_message">အီးမေးလ်ကို အောင်မြင်စွာ ပို့ပြီးပြီလား? ပူးတွဲပါသော debug log ကို ဖျက်ပစ်မည်။</string>
<string name="debug_debuglog_screen_keep_action">သိမ်းထားရန်</string>
</resources>
+1
View File
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Ble feilsøkingsloggen sendt? Loggen vil bli slettet.</string>
<string name="support_contact_sent_title">E-post sendt?</string>
<string name="support_contact_sent_message">Ble e-posten sendt? Den vedlagte feilsøkingsloggen vil bli slettet.</string>
<string name="debug_debuglog_screen_keep_action">Behold</string>
</resources>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">डिबग लग सफलतापूर्वक पठाइयो? लग मेटिनेछ।</string>
<string name="support_contact_sent_title">इमेल पठाइयो?</string>
<string name="support_contact_sent_message">इमेल सफलतापूर्वक पठाइयो? संलग्न डिबग लग मेटिनेछ।</string>
<string name="debug_debuglog_screen_keep_action">राख्नुहोस्</string>
</resources>
+3 -2
View File
@@ -32,8 +32,8 @@
<string name="upgrade_screen_iap_action">Eenmalig kopen</string>
<string name="upgrade_screen_iap_action_hint">Eenmalige aankoop: %s</string>
<string name="upgrade_screen_restore_purchase_action">Herstel aankoop</string>
<string name="upgrade_screen_restore_purchase_message">Geen aankopen gevonden. Gebruikt u de juiste rekening?</string>
<string name="upgrade_screen_restore_troubleshooting_msg">Als je recent hebt gekocht, kan het even duren voordat Google Play synchroniseert.</string>
<string name="upgrade_screen_restore_purchase_message">Geen aankopen gevonden. Gebruikt je wel het juiste account?</string>
<string name="upgrade_screen_restore_troubleshooting_msg">Als je de app onlangs hebt gekocht, kan het even duren voordat Google Play de gegevens synchroniseert.</string>
<string name="upgrade_screen_restore_sync_patience_hint">Probeer het over een paar minuten opnieuw als je aankoop niet verschijnt.</string>
<string name="upgrade_screen_restore_multiaccount_hint">Zorg ervoor dat je bent aangemeld met hetzelfde Google-account dat is gebruikt voor de aankoop.</string>
<string name="settings_monitor_mode_label">Monitor modus</string>
@@ -361,4 +361,5 @@
<string name="support_debuglog_sent_message">Is het foutopsporingslogboek succesvol verzonden? Het logboek wordt verwijderd.</string>
<string name="support_contact_sent_title">E-mail verzonden?</string>
<string name="support_contact_sent_message">Is de e-mail succesvol verzonden? Het bijgevoegde foutopsporingslogboek wordt verwijderd.</string>
<string name="debug_debuglog_screen_keep_action">Bewaren</string>
</resources>
+1
View File
@@ -371,4 +371,5 @@ K4r0lSz;</string>
<string name="support_debuglog_sent_message">Czy dziennik debugowania został wysłany pomyślnie? Dziennik zostanie usunięty.</string>
<string name="support_contact_sent_title">E-mail wysłany?</string>
<string name="support_contact_sent_message">Czy e-mail został wysłany pomyślnie? Załączony dziennik debugowania zostanie usunięty.</string>
<string name="debug_debuglog_screen_keep_action">Zachowaj</string>
</resources>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d arquivos prontos (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Apagar</string>
<string name="debug_debuglog_screen_keep_action">Manter</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d log de depuração (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d arquivos prontos (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Apagar</string>
<string name="debug_debuglog_screen_keep_action">Manter</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d registo de depuração (%2$s)</item>
+1
View File
@@ -311,6 +311,7 @@
<item quantity="other">%1$d datotecas prontas (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Stritgar</string>
<string name="debug_debuglog_screen_keep_action">Mantegnair</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d protocol da debug (%2$s)</item>
+1
View File
@@ -313,6 +313,7 @@
<item quantity="other">%1$d fișiere gata (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Șterge</string>
<string name="debug_debuglog_screen_keep_action">Păstrați</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d jurnal de depanare (%2$s)</item>
+1
View File
@@ -316,6 +316,7 @@ AL_Cool_T</string>
<item quantity="other">Файлов готово: %1$d (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Удалить</string>
<string name="debug_debuglog_screen_keep_action">Сохранить</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d отладочный журнал (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d archivos prontos (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Iscancellare</string>
<string name="debug_debuglog_screen_keep_action">Mantene</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d log de debug (%2$s)</item>
@@ -311,6 +311,7 @@
<item quantity="other">%1$d ගොනු සූදානම් (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">මකන්න</string>
<string name="debug_debuglog_screen_keep_action">තබා ගන්න</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d දෝෂහරණ ලොගය (%2$s)</item>
+13
View File
@@ -308,7 +308,14 @@
<string name="debug_debuglog_screen_sensitive_title">Citlivé informácie</string>
<string name="debug_debuglog_screen_session_path_label">Cesta relácie</string>
<string name="debug_debuglog_screen_log_files_label">Log súbory</string>
<plurals name="debug_debuglog_screen_log_files_ready">
<item quantity="one">%1$d súbor pripravený (%2$s)</item>
<item quantity="few">%1$d súbory pripravené (%2$s)</item>
<item quantity="many">%1$d súboru pripravených (%2$s)</item>
<item quantity="other">%1$d súborov pripravených (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Vymazať</string>
<string name="debug_debuglog_screen_keep_action">Ponechať</string>
<!-- Log management -->
<string name="support_debuglog_clear_action">Vymazať uložené ladíace záznamy</string>
<string name="support_debuglog_sessions_label">Ladíace relacie</string>
@@ -338,6 +345,12 @@
<string name="support_contact_description_bug_hint">Popíšte, čo sa stalo a ako replikovať problém. Prosím, buďte špecifický.</string>
<string name="support_contact_expected_label">Očakávané správanie</string>
<string name="support_contact_expected_hint">Popíšte, čo ste očakávali, že sa stane.</string>
<plurals name="support_contact_word_count">
<item quantity="one">%1$d / %2$d slovo</item>
<item quantity="few">%1$d / %2$d slová</item>
<item quantity="many">%1$d / %2$d slova</item>
<item quantity="other">%1$d / %2$d slov</item>
</plurals>
<string name="support_contact_debuglog_label">Ladiaci log</string>
<string name="support_contact_debuglog_picker_hint">Priložte existujúci ladíaci záznam alebo nahrajte nový.</string>
<string name="support_contact_debuglog_picker_empty">Žiadne ladíace záznamy sa nenašli. Najskôr nahraje jeden.</string>
+1
View File
@@ -315,6 +315,7 @@
<item quantity="other">%1$d datotek je pripravljenih (%2$s).</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Izbriši</string>
<string name="debug_debuglog_screen_keep_action">Ohrani</string>
<!-- Log management -->
<plurals name="support_debuglog_folder_summary">
<item quantity="one">%1$d dnevnik razhroščevanja (%2$s)</item>

Some files were not shown because too many files have changed in this diff Show More