mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-06 03:50:23 +02:00
Add option to transfer the device owner role
This commit is contained in:
parent
a53883e87a
commit
b1a3c10f96
9 changed files with 164 additions and 30 deletions
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -17,4 +17,6 @@
|
||||||
<uses-policies>
|
<uses-policies>
|
||||||
<!-- nothing -->
|
<!-- nothing -->
|
||||||
</uses-policies>
|
</uses-policies>
|
||||||
|
|
||||||
|
<support-transfer-ownership />
|
||||||
</device-admin>
|
</device-admin>
|
||||||
|
|
|
@ -23,4 +23,6 @@ interface DeviceOwnerApi {
|
||||||
fun setDelegations(packageName: String, scopes: List<DelegationScope>)
|
fun setDelegations(packageName: String, scopes: List<DelegationScope>)
|
||||||
fun getDelegations(): Map<String, List<DelegationScope>>
|
fun getDelegations(): Map<String, List<DelegationScope>>
|
||||||
fun setOrganizationName(name: String)
|
fun setOrganizationName(name: String)
|
||||||
|
|
||||||
|
fun transferOwnership(packageName: String, dryRun: Boolean = false)
|
||||||
}
|
}
|
|
@ -15,8 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.integration.platform.android
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.admin.DeviceAdminReceiver
|
||||||
import android.app.admin.DevicePolicyManager
|
import android.app.admin.DevicePolicyManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build.VERSION
|
import android.os.Build.VERSION
|
||||||
import android.os.Build.VERSION_CODES
|
import android.os.Build.VERSION_CODES
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -25,7 +28,8 @@ import io.timelimit.android.integration.platform.DeviceOwnerApi
|
||||||
|
|
||||||
class AndroidDeviceOwnerApi(
|
class AndroidDeviceOwnerApi(
|
||||||
private val componentName: ComponentName,
|
private val componentName: ComponentName,
|
||||||
private val devicePolicyManager: DevicePolicyManager
|
private val devicePolicyManager: DevicePolicyManager,
|
||||||
|
private val packageManager: PackageManager
|
||||||
): DeviceOwnerApi {
|
): DeviceOwnerApi {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "AndroidDeviceOwnerApi"
|
private const val LOG_TAG = "AndroidDeviceOwnerApi"
|
||||||
|
@ -102,4 +106,22 @@ class AndroidDeviceOwnerApi(
|
||||||
} else throw SecurityException()
|
} else throw SecurityException()
|
||||||
} else throw SecurityException()
|
} else throw SecurityException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun transferOwnership(packageName: String, dryRun: Boolean) {
|
||||||
|
if (VERSION.SDK_INT < VERSION_CODES.P) throw IllegalStateException()
|
||||||
|
if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) throw SecurityException()
|
||||||
|
|
||||||
|
val targetComponentName = packageManager.queryBroadcastReceivers(
|
||||||
|
Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED)
|
||||||
|
.setPackage(packageName),
|
||||||
|
0
|
||||||
|
).singleOrNull()?.activityInfo?.let { ComponentName(it.packageName, it.name) }
|
||||||
|
|
||||||
|
if (targetComponentName == null) throw RuntimeException("no device admin in the target App found")
|
||||||
|
|
||||||
|
if (dryRun) return
|
||||||
|
|
||||||
|
devicePolicyManager.setDelegatedScopes(componentName, packageName, emptyList())
|
||||||
|
devicePolicyManager.transferOwnership(componentName, targetComponentName, null)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -823,5 +823,5 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val deviceOwner: DeviceOwnerApi = AndroidDeviceOwnerApi(deviceAdmin, policyManager)
|
override val deviceOwner: DeviceOwnerApi = AndroidDeviceOwnerApi(deviceAdmin, policyManager, context.packageManager)
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,5 +201,7 @@ class DummyIntegration(
|
||||||
override fun getDelegations(): Map<String, List<DeviceOwnerApi.DelegationScope>> = emptyMap()
|
override fun getDelegations(): Map<String, List<DeviceOwnerApi.DelegationScope>> = emptyMap()
|
||||||
|
|
||||||
override fun setOrganizationName(name: String) = throw SecurityException()
|
override fun setOrganizationName(name: String) = throw SecurityException()
|
||||||
|
|
||||||
|
override fun transferOwnership(packageName: String, dryRun: Boolean) = throw IllegalStateException("unsupported operation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.diagnose.exception.SimpleErrorDialog
|
||||||
import io.timelimit.android.ui.model.managedevice.DeviceOwnerHandling
|
import io.timelimit.android.ui.model.managedevice.DeviceOwnerHandling
|
||||||
import io.timelimit.android.ui.overview.overview.ListCardCommon
|
import io.timelimit.android.ui.overview.overview.ListCardCommon
|
||||||
import io.timelimit.android.ui.overview.overview.ListCommon
|
import io.timelimit.android.ui.overview.overview.ListCommon
|
||||||
|
@ -126,6 +127,12 @@ fun DeviceOwnerScreen(
|
||||||
Text(scope.label)
|
Text(scope.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { screen.actions.transferOwnership(app.packageName) }
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.device_owner_transfer_title))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +145,29 @@ fun DeviceOwnerScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
if (screen.appListDialog != null) AddAppDialog(screen.appListDialog, screen.actions)
|
when (val dialog = screen.dialog) {
|
||||||
|
is DeviceOwnerHandling.OwnerScreen.Normal.AppListDialog -> AddAppDialog(dialog, screen.actions)
|
||||||
|
is DeviceOwnerHandling.OwnerScreen.Normal.TransferOwnershipDialog -> AlertDialog(
|
||||||
|
title = { Text(stringResource(R.string.device_owner_transfer_title)) },
|
||||||
|
text = { Text(stringResource(R.string.device_owner_transfer_text, dialog.packageName)) },
|
||||||
|
buttons = {
|
||||||
|
TextButton(onClick = dialog.cancel) {
|
||||||
|
Text(stringResource(R.string.generic_cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = dialog.confirm) {
|
||||||
|
Text(stringResource(R.string.device_owner_transfer_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = dialog.cancel
|
||||||
|
)
|
||||||
|
is DeviceOwnerHandling.OwnerScreen.Normal.ErrorDialog -> SimpleErrorDialog(
|
||||||
|
title = null,
|
||||||
|
message = dialog.message,
|
||||||
|
close = dialog.close
|
||||||
|
)
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@ package io.timelimit.android.ui.model.managedevice
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.material.SnackbarDuration
|
||||||
import androidx.compose.material.SnackbarHostState
|
import androidx.compose.material.SnackbarHostState
|
||||||
|
import androidx.compose.material.SnackbarResult
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
@ -28,13 +30,16 @@ import io.timelimit.android.data.model.App
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.integration.platform.DeviceOwnerApi
|
import io.timelimit.android.integration.platform.DeviceOwnerApi
|
||||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.ui.diagnose.exception.ExceptionUtil
|
||||||
import io.timelimit.android.ui.model.AuthenticationModelApi
|
import io.timelimit.android.ui.model.AuthenticationModelApi
|
||||||
import io.timelimit.android.ui.model.BackStackItem
|
import io.timelimit.android.ui.model.BackStackItem
|
||||||
import io.timelimit.android.ui.model.Screen
|
import io.timelimit.android.ui.model.Screen
|
||||||
import io.timelimit.android.ui.model.State
|
import io.timelimit.android.ui.model.State
|
||||||
|
import io.timelimit.android.ui.model.flow.Case
|
||||||
|
import io.timelimit.android.ui.model.flow.splitConflated
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
@ -45,13 +50,23 @@ object DeviceOwnerHandling {
|
||||||
private const val LOG_TAG = "DeviceOwnerHandling"
|
private const val LOG_TAG = "DeviceOwnerHandling"
|
||||||
|
|
||||||
data class OwnerState(
|
data class OwnerState(
|
||||||
val appListDialog: AppListDialog? = null,
|
|
||||||
val apps: List<String> = emptyList(),
|
val apps: List<String> = emptyList(),
|
||||||
|
val dialog: Dialog? = null,
|
||||||
val organizationName: OrganizationName = OrganizationName.Original
|
val organizationName: OrganizationName = OrganizationName.Original
|
||||||
): Serializable {
|
): Serializable {
|
||||||
|
sealed class Dialog: Serializable
|
||||||
|
|
||||||
data class AppListDialog(
|
data class AppListDialog(
|
||||||
val filter: String = ""
|
val filter: String = ""
|
||||||
): Serializable
|
): Dialog()
|
||||||
|
|
||||||
|
data class TransferOwnershipDialog(
|
||||||
|
val packageName: String
|
||||||
|
): Dialog()
|
||||||
|
|
||||||
|
data class ErrorDialog(
|
||||||
|
val message: String
|
||||||
|
): Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class OrganizationName: Serializable {
|
sealed class OrganizationName: Serializable {
|
||||||
|
@ -66,7 +81,7 @@ object DeviceOwnerHandling {
|
||||||
data class Normal(
|
data class Normal(
|
||||||
val isParentAuthenticated: Boolean,
|
val isParentAuthenticated: Boolean,
|
||||||
val organizationName: String,
|
val organizationName: String,
|
||||||
val appListDialog: AppListDialog?,
|
val dialog: Dialog?,
|
||||||
val scopes: List<DeviceOwnerApi.DelegationScope>,
|
val scopes: List<DeviceOwnerApi.DelegationScope>,
|
||||||
val apps: List<AppInfo>,
|
val apps: List<AppInfo>,
|
||||||
val actions: Actions
|
val actions: Actions
|
||||||
|
@ -78,10 +93,23 @@ object DeviceOwnerHandling {
|
||||||
val scopes: Set<DeviceOwnerApi.DelegationScope>
|
val scopes: Set<DeviceOwnerApi.DelegationScope>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed class Dialog
|
||||||
|
|
||||||
data class AppListDialog(
|
data class AppListDialog(
|
||||||
val filter: String,
|
val filter: String,
|
||||||
val apps: List<App>
|
val apps: List<App>
|
||||||
)
|
): Dialog()
|
||||||
|
|
||||||
|
data class TransferOwnershipDialog(
|
||||||
|
val packageName: String,
|
||||||
|
val confirm: () -> Unit,
|
||||||
|
val cancel: () -> Unit
|
||||||
|
): Dialog()
|
||||||
|
|
||||||
|
data class ErrorDialog(
|
||||||
|
val message: String,
|
||||||
|
val close: () -> Unit
|
||||||
|
): Dialog()
|
||||||
|
|
||||||
data class Actions(
|
data class Actions(
|
||||||
val updateOrganizationName: ((String) -> Unit)?,
|
val updateOrganizationName: ((String) -> Unit)?,
|
||||||
|
@ -89,6 +117,7 @@ object DeviceOwnerHandling {
|
||||||
val dismissAppListDialog: () -> Unit,
|
val dismissAppListDialog: () -> Unit,
|
||||||
val addApp: (String) -> Unit,
|
val addApp: (String) -> Unit,
|
||||||
val updateScopeEnabled: (String, DeviceOwnerApi.DelegationScope, Boolean) -> Unit,
|
val updateScopeEnabled: (String, DeviceOwnerApi.DelegationScope, Boolean) -> Unit,
|
||||||
|
val transferOwnership: (String) -> Unit,
|
||||||
val updateDialogSearch: (String) -> Unit
|
val updateDialogSearch: (String) -> Unit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -106,7 +135,7 @@ object DeviceOwnerHandling {
|
||||||
val snackbarHostState = SnackbarHostState()
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
|
||||||
val isMatchingDeviceLive = combine(deviceLive, logic.deviceId.asFlow()) { device, id ->
|
val isMatchingDeviceLive = combine(deviceLive, logic.deviceId.asFlow()) { device, id ->
|
||||||
device.id == id
|
device.id == id && device.currentProtectionLevel == ProtectionLevel.DeviceOwner
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
val screenLive = getScreen(
|
val screenLive = getScreen(
|
||||||
|
@ -124,7 +153,7 @@ object DeviceOwnerHandling {
|
||||||
)
|
)
|
||||||
|
|
||||||
return combine(ownerStateLive, screenLive, backStackLive) { state, screen, backStack ->
|
return combine(ownerStateLive, screenLive, backStackLive) { state, screen, backStack ->
|
||||||
Screen.DeviceOwnerScreen(state, screen, backStack, snackbarHostState) as Screen
|
Screen.DeviceOwnerScreen(state, screen, backStack, snackbarHostState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +174,15 @@ object DeviceOwnerHandling {
|
||||||
try {
|
try {
|
||||||
action()
|
action()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
snackbarHostState.showSnackbar(logic.context.getString(R.string.error_general))
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
logic.context.getString(R.string.error_general),
|
||||||
|
logic.context.getString(R.string.generic_show_details),
|
||||||
|
SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result == SnackbarResult.ActionPerformed) updateState {
|
||||||
|
it.copy(dialog = OwnerState.ErrorDialog(ExceptionUtil.format(ex)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,17 +226,17 @@ object DeviceOwnerHandling {
|
||||||
addApp = { packageName ->
|
addApp = { packageName ->
|
||||||
launch {
|
launch {
|
||||||
if (authentication.authenticatedParentOnly.first() != null) updateState { state ->
|
if (authentication.authenticatedParentOnly.first() != null) updateState { state ->
|
||||||
state.copy(apps = state.apps + packageName, appListDialog = null)
|
state.copy(apps = state.apps + packageName, dialog = null)
|
||||||
} else updateState { it.copy(appListDialog = null) }
|
} else updateState { it.copy(dialog = null) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissAppListDialog = {
|
dismissAppListDialog = {
|
||||||
updateState { it.copy(appListDialog = null) }
|
updateState { it.copy(dialog = null) }
|
||||||
},
|
},
|
||||||
showAppListDialog = {
|
showAppListDialog = {
|
||||||
launch {
|
launch {
|
||||||
authentication.doParentAuthentication()?.also {
|
authentication.doParentAuthentication()?.also {
|
||||||
updateState { it.copy(appListDialog = OwnerState.AppListDialog()) }
|
updateState { it.copy(dialog = OwnerState.AppListDialog()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -215,9 +252,19 @@ object DeviceOwnerHandling {
|
||||||
} else authentication.doParentAuthentication()
|
} else authentication.doParentAuthentication()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
transferOwnership = { packageName ->
|
||||||
|
launch {
|
||||||
|
owner.transferOwnership(packageName, dryRun = true)
|
||||||
|
|
||||||
|
authentication.doParentAuthentication()
|
||||||
|
|
||||||
|
updateState { it.copy(dialog = OwnerState.TransferOwnershipDialog(packageName)) }
|
||||||
|
}
|
||||||
|
},
|
||||||
updateDialogSearch = { filter ->
|
updateDialogSearch = { filter ->
|
||||||
updateState {
|
updateState {
|
||||||
it.copy(appListDialog = it.appListDialog?.copy(filter = filter))
|
if (it.dialog is OwnerState.AppListDialog) it.copy(dialog = it.dialog.copy(filter = filter))
|
||||||
|
else it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -231,7 +278,9 @@ object DeviceOwnerHandling {
|
||||||
|
|
||||||
val dialogLive = getNullableDialog(
|
val dialogLive = getNullableDialog(
|
||||||
logic.platformIntegration,
|
logic.platformIntegration,
|
||||||
state.map { it.appListDialog }
|
state.map { it.dialog },
|
||||||
|
updateState,
|
||||||
|
::launch
|
||||||
)
|
)
|
||||||
|
|
||||||
emitAll(
|
emitAll(
|
||||||
|
@ -245,7 +294,7 @@ object DeviceOwnerHandling {
|
||||||
if (isMatchingDevice) OwnerScreen.Normal(
|
if (isMatchingDevice) OwnerScreen.Normal(
|
||||||
isParentAuthenticated = isParentAuthenticated,
|
isParentAuthenticated = isParentAuthenticated,
|
||||||
organizationName = organizationName,
|
organizationName = organizationName,
|
||||||
appListDialog = dialog,
|
dialog = dialog,
|
||||||
scopes = scopes,
|
scopes = scopes,
|
||||||
apps = apps,
|
apps = apps,
|
||||||
actions = actions.copy(
|
actions = actions.copy(
|
||||||
|
@ -311,20 +360,36 @@ object DeviceOwnerHandling {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private fun getNullableDialog(
|
private fun getNullableDialog(
|
||||||
integration: PlatformIntegration,
|
integration: PlatformIntegration,
|
||||||
state: Flow<OwnerState.AppListDialog?>
|
state: Flow<OwnerState.Dialog?>,
|
||||||
): Flow<OwnerScreen.Normal.AppListDialog?> {
|
updateState: ((OwnerState) -> OwnerState) -> Unit,
|
||||||
val hasDialog = state.map { it != null }.distinctUntilChanged()
|
launch: (suspend () -> Unit) -> Unit
|
||||||
|
): Flow<OwnerScreen.Normal.Dialog?> = state.splitConflated(
|
||||||
|
Case.simple<_, _, OwnerState.AppListDialog> { getAppListDialog(integration, it) },
|
||||||
|
Case.simple<_, _, OwnerState.TransferOwnershipDialog> { stateLive ->
|
||||||
|
stateLive.map { OwnerScreen.Normal.TransferOwnershipDialog(
|
||||||
|
packageName = it.packageName,
|
||||||
|
confirm = { launch {
|
||||||
|
try {
|
||||||
|
integration.deviceOwner.transferOwnership(it.packageName)
|
||||||
|
} finally {
|
||||||
|
updateState { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
} },
|
||||||
|
cancel = { updateState { it.copy(dialog = null) } }
|
||||||
|
) }
|
||||||
|
},
|
||||||
|
Case.simple<_, _, OwnerState.ErrorDialog> { stateLive ->
|
||||||
|
stateLive.map { OwnerScreen.Normal.ErrorDialog(
|
||||||
|
message = it.message,
|
||||||
|
close = { updateState { it.copy(dialog = null) } }
|
||||||
|
) }
|
||||||
|
},
|
||||||
|
Case.nil { flowOf(null) }
|
||||||
|
)
|
||||||
|
|
||||||
return hasDialog.transformLatest {
|
private fun getAppListDialog(
|
||||||
if (it) emitAll(getDialog(integration, state.filterNotNull()))
|
|
||||||
else emit(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDialog(
|
|
||||||
integration: PlatformIntegration,
|
integration: PlatformIntegration,
|
||||||
state: Flow<OwnerState.AppListDialog>
|
state: Flow<OwnerState.AppListDialog>
|
||||||
): Flow<OwnerScreen.Normal.AppListDialog> {
|
): Flow<OwnerScreen.Normal.AppListDialog> {
|
||||||
|
|
|
@ -1779,4 +1779,10 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="account_deletion_confirmation_premium_toggle">Ich verzichte ersatzlos auf eine evtl. vorhandene Restlaufzeit meiner Vollversion</string>
|
<string name="account_deletion_confirmation_premium_toggle">Ich verzichte ersatzlos auf eine evtl. vorhandene Restlaufzeit meiner Vollversion</string>
|
||||||
<string name="account_deletion_confirmation_button">Löschung anfordern</string>
|
<string name="account_deletion_confirmation_button">Löschung anfordern</string>
|
||||||
|
|
||||||
|
<string name="device_owner_transfer_title">Gerätebesitzer übertragen</string>
|
||||||
|
<string name="device_owner_transfer_text">Soll TimeLimit die Gerätebesitzerrolle abgeben und an %s übergeben?
|
||||||
|
Das wird zu einer Manipulationswarnung führen.
|
||||||
|
</string>
|
||||||
|
<string name="device_owner_transfer_confirm">Berechtigung übertragen</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1831,4 +1831,10 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="account_deletion_confirmation_premium_toggle">I accept that my premium version (if it exists) will be deleted without replacement</string>
|
<string name="account_deletion_confirmation_premium_toggle">I accept that my premium version (if it exists) will be deleted without replacement</string>
|
||||||
<string name="account_deletion_confirmation_button">Request Deletion</string>
|
<string name="account_deletion_confirmation_button">Request Deletion</string>
|
||||||
|
|
||||||
|
<string name="device_owner_transfer_title">Transfer device ownership</string>
|
||||||
|
<string name="device_owner_transfer_text">Do you want that TimeLimit revokes this permission for itself and grants it to %s?
|
||||||
|
This will be interpreted as manipulation.
|
||||||
|
</string>
|
||||||
|
<string name="device_owner_transfer_confirm">Transfer permission</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue