mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
13
Commits
v4.0.0-rc0
...
v4.0.1-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac477113cf | ||
|
|
5e21941765 | ||
|
|
e5032ee35a | ||
|
|
1ceac349ab | ||
|
|
2db01b410b | ||
|
|
193a1030fb | ||
|
|
12e35631d6 | ||
|
|
67474a30da | ||
|
|
44cd0eec2a | ||
|
|
a79f6a0535 | ||
|
|
8675d73a98 | ||
|
|
3423786f06 | ||
|
|
00083707c6 |
@@ -0,0 +1 @@
|
||||
worktrees/
|
||||
@@ -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
|
||||
|
||||
@@ -177,8 +177,6 @@ dependencies {
|
||||
|
||||
addDagger()
|
||||
|
||||
addMoshi()
|
||||
|
||||
addOkio()
|
||||
|
||||
addBaseAndroid()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Faire un don</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">J’ai déjà fait un don</string>
|
||||
<string name="foss_upgrade_no_money_label">J’ai 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">J’ai 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
+12
-10
@@ -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)
|
||||
}
|
||||
+23
@@ -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)
|
||||
}
|
||||
}
|
||||
-116
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 l’option.</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 l’appli.</string>
|
||||
<string name="upgrade_benefit_themes">Personnalisation du thème</string>
|
||||
<string name="upgrade_benefit_autoplay">Lecture automatique & 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 & 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 d’accueil</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. L’abonnement comprend un essai gratuit et peut être annulé n’importe quand.</string>
|
||||
<string name="upgrade_screen_subscription_trial_action">Commencer l’essai 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 l’achat</string>
|
||||
<string name="upgrade_screen_restore_purchase_message">Aucun achat n’a é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 n’apparaît pas.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Assurez-vous d’être connecté avec le même compte Google utilisé pour l’achat.</string>
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Si l’achat est récent, Google Play peut mettre du temps à synchroniser.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Si votre achat n’apparaît pas, réessayez dans quelques minutes.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Le même compte Google doit être utilisé pour l’achat 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 l’appli.</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 d’aide.</string>
|
||||
<string name="settings_wiki_label">Wiki</string>
|
||||
<string name="settings_wiki_description">FAQ & 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 d’appareil 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 l’inté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 l’enregistrement, reproduisez le problème, puis arrêtez l’enregistrement.</string>
|
||||
<string name="debug_debuglog_short_recording_continue">Continuer l’enregistrement</string>
|
||||
<string name="debug_debuglog_short_recording_title">L’enregistrement 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 l’enregistrement, reproduisez le problème, puis arrêtez l’enregistrement.</string>
|
||||
<string name="debug_debuglog_short_recording_continue">Poursuivre l’enregistrement</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 j’assure 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 l’appareil 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 s’est 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 s’est 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 qu’il 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 l’appli de courriel</string>
|
||||
<string name="support_contact_no_email_app">Aucune appli de courriel n’a é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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user