Move device owner organization name to the device owner screen

This commit is contained in:
Jonas Lochmann 2023-06-12 02:00:00 +02:00
parent 8fb102390f
commit 34f7bab46d
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
13 changed files with 104 additions and 151 deletions

View file

@ -333,7 +333,7 @@ abstract class ConfigDao {
} }
) )
suspend fun getCustomOrganizationName(): String = getValueOfKeyCoroutine(ConfigurationItemType.CustomOrganizationName) ?: "" suspend fun getCustomOrganizationName(): String? = getValueOfKeyCoroutine(ConfigurationItemType.CustomOrganizationName)
fun setCustomOrganizationName(value: String) = updateValueSync(ConfigurationItemType.CustomOrganizationName, value) fun setCustomOrganizationName(value: String) = updateValueSync(ConfigurationItemType.CustomOrganizationName, value)
fun getServerApiLevelSync() = getValueOfKeySync(ConfigurationItemType.ServerApiLevel).let { it?.toInt() ?: 0 } fun getServerApiLevelSync() = getValueOfKeySync(ConfigurationItemType.ServerApiLevel).let { it?.toInt() ?: 0 }

View file

@ -22,4 +22,5 @@ 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)
} }

View file

@ -75,9 +75,6 @@ abstract class PlatformIntegration(
// this function requires the device owner permission and a recent android version // this function requires the device owner permission and a recent android version
abstract fun setForceNetworkTime(enable: Boolean) abstract fun setForceNetworkTime(enable: Boolean)
abstract fun canSetOrganizationName(): Boolean
abstract fun setOrganizationName(name: String): Boolean
abstract fun restartApp() abstract fun restartApp()
abstract fun getCurrentNetworkId(): NetworkId abstract fun getCurrentNetworkId(): NetworkId

View file

@ -94,4 +94,12 @@ class AndroidDeviceOwnerApi(
entry.value.map { it.second } entry.value.map { it.second }
} }
} }
override fun setOrganizationName(name: String) {
if (VERSION.SDK_INT >= VERSION_CODES.O && !BuildConfig.storeCompilant) {
if (devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) {
devicePolicyManager.setOrganizationName(componentName, name)
} else throw SecurityException()
} else throw SecurityException()
}
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 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
@ -629,28 +629,6 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
session.playbackState?.state == PlaybackState.STATE_REWINDING session.playbackState?.state == PlaybackState.STATE_REWINDING
} }
override fun canSetOrganizationName(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !BuildConfig.storeCompilant) {
policyManager.isDeviceOwnerApp(context.packageName)
} else false
}
override fun setOrganizationName(name: String): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !BuildConfig.storeCompilant) {
if (policyManager.isDeviceOwnerApp(context.packageName)) {
return try {
policyManager.setOrganizationName(deviceAdmin, name)
true
} catch (ex: SecurityException) {
false
}
}
}
return false
}
override fun openSystemPermissionScren( override fun openSystemPermissionScren(
activity: FragmentActivity, activity: FragmentActivity,
permission: SystemPermission, permission: SystemPermission,

View file

@ -181,9 +181,6 @@ class DummyIntegration(
override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected
override fun canSetOrganizationName(): Boolean = false
override fun setOrganizationName(name: String): Boolean = false
override fun openSystemPermissionScren( override fun openSystemPermissionScren(
activity: FragmentActivity, activity: FragmentActivity,
permission: SystemPermission, permission: SystemPermission,
@ -202,5 +199,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()
} }
} }

View file

@ -80,12 +80,6 @@ class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
} }
}) })
binding.diagnoseOrganizationNameButton.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
DiagnoseOrganizationNameDialogFragment.newInstance().show(parentFragmentManager)
}
}
binding.diagnoseDom.setOnClickListener { binding.diagnoseDom.setOnClickListener {
requireActivity().execute(UpdateStateCommand.Diagnose.DeviceOwner) requireActivity().execute(UpdateStateCommand.Diagnose.DeviceOwner)
} }

View file

@ -1,97 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
*
* 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
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.diagnose
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.util.EditTextBottomSheetDialog
class DiagnoseOrganizationNameDialogFragment: EditTextBottomSheetDialog() {
companion object {
private const val DIALOG_TAG = "DiagnoseOrganizationNameDialogFragment"
fun newInstance() = DiagnoseOrganizationNameDialogFragment()
}
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
auth.authenticatedUser.observe(this) {
if (it?.second?.type != UserType.Parent) {
dismissAllowingStateLoss()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title = getString(R.string.diagnose_don_title)
if (appLogic.platformIntegration.getCurrentProtectionLevel() != ProtectionLevel.DeviceOwner) {
Toast.makeText(
requireContext(),
R.string.diagnose_don_no_owner_toast,
Toast.LENGTH_SHORT
).show()
dismissAllowingStateLoss()
} else if (!appLogic.platformIntegration.canSetOrganizationName()) {
Toast.makeText(requireContext(), R.string.diagnose_don_not_supported_toast, Toast.LENGTH_SHORT).show()
dismissAllowingStateLoss()
} else {
if (savedInstanceState == null) {
runAsync {
val name = appLogic.database.config().getCustomOrganizationName()
binding.editText.setText(name)
}
}
}
}
override fun go() {
val value = binding.editText.text.toString()
if (appLogic.platformIntegration.setOrganizationName(value)) {
val database = appLogic.database
Threads.database.execute { database.config().setCustomOrganizationName(value) }
} else {
Toast.makeText(requireContext(), R.string.diagnose_don_not_supported_toast, Toast.LENGTH_SHORT).show()
}
dismissAllowingStateLoss()
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -15,11 +15,13 @@
*/ */
package io.timelimit.android.ui.diagnose.deviceowner package io.timelimit.android.ui.diagnose.deviceowner
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -29,6 +31,7 @@ import androidx.compose.runtime.key
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign 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
@ -58,7 +61,22 @@ fun DeviceOwnerScreen(
modifier = modifier.verticalScroll(rememberScrollState()), modifier = modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Spacer(modifier = Modifier.height(8.dp)) TextField(
value = screen.organizationName,
onValueChange = screen.actions.updateOrganizationName ?: {},
enabled = screen.actions.updateOrganizationName != null,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
singleLine = true,
label = { Text(stringResource(R.string.diagnose_don_title)) },
modifier = Modifier.fillMaxWidth()
)
AnimatedVisibility(visible = !screen.isParentAuthenticated) {
Text(
stringResource(R.string.diagnose_don_auth),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
Text( Text(
stringResource( stringResource(

View file

@ -16,8 +16,12 @@
package io.timelimit.android.ui.model.diagnose package io.timelimit.android.ui.model.diagnose
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
import androidx.compose.material.SnackbarHostState import androidx.compose.material.SnackbarHostState
import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.App import io.timelimit.android.data.model.App
import io.timelimit.android.extensions.whileTrue import io.timelimit.android.extensions.whileTrue
@ -33,21 +37,33 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.Serializable
object DeviceOwnerHandling { object DeviceOwnerHandling {
private const val LOG_TAG = "DeviceOwnerHandling"
data class OwnerState( data class OwnerState(
val appListDialog: AppListDialog? = null, val appListDialog: AppListDialog? = null,
val apps: List<String> = emptyList() val apps: List<String> = emptyList(),
): java.io.Serializable { val organizationName: OrganizationName = OrganizationName.Original
): Serializable {
data class AppListDialog( data class AppListDialog(
val filter: String = "" val filter: String = ""
): java.io.Serializable ): Serializable
}
sealed class OrganizationName: Serializable {
object Original: OrganizationName()
object Error: OrganizationName()
class Modified(val value: String): OrganizationName()
} }
sealed class OwnerScreen { sealed class OwnerScreen {
object Error: OwnerScreen() object Error: OwnerScreen()
data class Normal( data class Normal(
val isParentAuthenticated: Boolean,
val organizationName: String,
val appListDialog: AppListDialog?, val appListDialog: AppListDialog?,
val scopes: List<DeviceOwnerApi.DelegationScope>, val scopes: List<DeviceOwnerApi.DelegationScope>,
val apps: List<AppInfo>, val apps: List<AppInfo>,
@ -66,6 +82,7 @@ object DeviceOwnerHandling {
) )
data class Actions( data class Actions(
val updateOrganizationName: ((String) -> Unit)?,
val showAppListDialog: () -> Unit, val showAppListDialog: () -> Unit,
val dismissAppListDialog: () -> Unit, val dismissAppListDialog: () -> Unit,
val addApp: (String) -> Unit, val addApp: (String) -> Unit,
@ -132,7 +149,40 @@ object DeviceOwnerHandling {
val refreshSignal = Channel<Unit>(Channel.CONFLATED) val refreshSignal = Channel<Unit>(Channel.CONFLATED)
val hadUpdateOrganizationNameErrorLive = state.map { it.organizationName == OrganizationName.Error }.distinctUntilChanged()
val isParentAuthenticatedLive = authentication.authenticatedParentOnly.map { it != null }.distinctUntilChanged()
val organizationNameLive = state.map { it.organizationName }.distinctUntilChanged().map {
when (it) {
is OrganizationName.Modified -> it.value
is OrganizationName.Error, OrganizationName.Original -> logic.database.config().getCustomOrganizationName() ?: ""
}
}
val actions = OwnerScreen.Normal.Actions( val actions = OwnerScreen.Normal.Actions(
updateOrganizationName = { organizationName ->
updateState { state ->
if (state.organizationName == OrganizationName.Error) state
else state.copy(organizationName = OrganizationName.Modified(organizationName))
}
launch {
if (isParentAuthenticatedLive.first()) try {
owner.setOrganizationName(organizationName)
Threads.database.executeAndWait {
logic.database.config().setCustomOrganizationName(organizationName)
}
} catch (ex: Exception) {
updateState { it.copy(organizationName = OrganizationName.Error) }
throw ex
} else updateState { state ->
if (state.organizationName == OrganizationName.Error) state
else state.copy(organizationName = OrganizationName.Original)
}
}
},
addApp = { packageName -> addApp = { packageName ->
launch { launch {
if (authentication.authenticatedParentOnly.first() != null) updateState { state -> if (authentication.authenticatedParentOnly.first() != null) updateState { state ->
@ -182,16 +232,30 @@ object DeviceOwnerHandling {
) )
emitAll( emitAll(
appsLive.combine(dialogLive) { apps, dialog -> combine(
appsLive, dialogLive, hadUpdateOrganizationNameErrorLive, isParentAuthenticatedLive, organizationNameLive
) { apps, dialog, hadUpdateOrganizationNameError, isParentAuthenticated, organizationName ->
OwnerScreen.Normal( OwnerScreen.Normal(
isParentAuthenticated = isParentAuthenticated,
organizationName = organizationName,
appListDialog = dialog, appListDialog = dialog,
scopes = scopes, scopes = scopes,
apps = apps, apps = apps,
actions = actions actions = actions.copy(
updateOrganizationName =
if (hadUpdateOrganizationNameError || !isParentAuthenticated) null
else actions.updateOrganizationName
)
) )
} }
) )
}.catch { emit(OwnerScreen.Error) } }.catch {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "error during generating screen", it)
}
emit(OwnerScreen.Error)
}
private fun getApps( private fun getApps(
integration: PlatformIntegration, integration: PlatformIntegration,

View file

@ -87,13 +87,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:id="@+id/diagnose_organization_name_button"
android:text="@string/diagnose_don_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button <Button
style="?materialButtonOutlinedStyle" style="?materialButtonOutlinedStyle"
android:id="@+id/diagnose_dom" android:id="@+id/diagnose_dom"

View file

@ -561,8 +561,7 @@
<string name="diagnose_bat_level">Akkustand: %d\%%</string> <string name="diagnose_bat_level">Akkustand: %d\%%</string>
<string name="diagnose_don_title">Gerätebesitzerorganisationsname</string> <string name="diagnose_don_title">Gerätebesitzerorganisationsname</string>
<string name="diagnose_don_no_owner_toast">TimeLimit ist KEIN Geräte-Besitzer</string> <string name="diagnose_don_auth">Melden Sie sich an, um den Namen zu ändern</string>
<string name="diagnose_don_not_supported_toast">Ihr System ermöglicht es nicht, einen Organisationsnamen zu hinterlegen</string>
<string name="diagnose_dom_title">Geräte-Besitzer</string> <string name="diagnose_dom_title">Geräte-Besitzer</string>
<string name="diagnose_dom_error">Diese Funktion erfordert eine ausreichend neue Android-Version und die Geräte-Besitzer-Berechtigung</string> <string name="diagnose_dom_error">Diese Funktion erfordert eine ausreichend neue Android-Version und die Geräte-Besitzer-Berechtigung</string>

View file

@ -614,8 +614,7 @@
<string name="diagnose_bat_level">Level: %d\%%</string> <string name="diagnose_bat_level">Level: %d\%%</string>
<string name="diagnose_don_title">Device Owner Organization Name</string> <string name="diagnose_don_title">Device Owner Organization Name</string>
<string name="diagnose_don_no_owner_toast">TimeLimit is NOT set as device owner</string> <string name="diagnose_don_auth">Sign in to change the name</string>
<string name="diagnose_don_not_supported_toast">Your system does not support setting the organization name</string>
<string name="diagnose_dom_title">Device Owner</string> <string name="diagnose_dom_title">Device Owner</string>
<string name="diagnose_dom_error">You need a recent Android version and the device owner permission to use this</string> <string name="diagnose_dom_error">You need a recent Android version and the device owner permission to use this</string>