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 getServerApiLevelSync() = getValueOfKeySync(ConfigurationItemType.ServerApiLevel).let { it?.toInt() ?: 0 }

View file

@ -22,4 +22,5 @@ interface DeviceOwnerApi {
fun setDelegations(packageName: String, scopes: 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
abstract fun setForceNetworkTime(enable: Boolean)
abstract fun canSetOrganizationName(): Boolean
abstract fun setOrganizationName(name: String): Boolean
abstract fun restartApp()
abstract fun getCurrentNetworkId(): NetworkId

View file

@ -94,4 +94,12 @@ class AndroidDeviceOwnerApi(
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
* 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
}
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(
activity: FragmentActivity,
permission: SystemPermission,

View file

@ -181,9 +181,6 @@ class DummyIntegration(
override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected
override fun canSetOrganizationName(): Boolean = false
override fun setOrganizationName(name: String): Boolean = false
override fun openSystemPermissionScren(
activity: FragmentActivity,
permission: SystemPermission,
@ -202,5 +199,7 @@ class DummyIntegration(
}
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 {
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
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@ -29,6 +31,7 @@ import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.TextOverflow
import androidx.compose.ui.unit.dp
@ -58,7 +61,22 @@ fun DeviceOwnerScreen(
modifier = modifier.verticalScroll(rememberScrollState()),
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(
stringResource(

View file

@ -16,8 +16,12 @@
package io.timelimit.android.ui.model.diagnose
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.compose.material.SnackbarHostState
import io.timelimit.android.BuildConfig
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.model.App
import io.timelimit.android.extensions.whileTrue
@ -33,21 +37,33 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.io.Serializable
object DeviceOwnerHandling {
private const val LOG_TAG = "DeviceOwnerHandling"
data class OwnerState(
val appListDialog: AppListDialog? = null,
val apps: List<String> = emptyList()
): java.io.Serializable {
val apps: List<String> = emptyList(),
val organizationName: OrganizationName = OrganizationName.Original
): Serializable {
data class AppListDialog(
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 {
object Error: OwnerScreen()
data class Normal(
val isParentAuthenticated: Boolean,
val organizationName: String,
val appListDialog: AppListDialog?,
val scopes: List<DeviceOwnerApi.DelegationScope>,
val apps: List<AppInfo>,
@ -66,6 +82,7 @@ object DeviceOwnerHandling {
)
data class Actions(
val updateOrganizationName: ((String) -> Unit)?,
val showAppListDialog: () -> Unit,
val dismissAppListDialog: () -> Unit,
val addApp: (String) -> Unit,
@ -132,7 +149,40 @@ object DeviceOwnerHandling {
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(
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 ->
launch {
if (authentication.authenticatedParentOnly.first() != null) updateState { state ->
@ -182,16 +232,30 @@ object DeviceOwnerHandling {
)
emitAll(
appsLive.combine(dialogLive) { apps, dialog ->
combine(
appsLive, dialogLive, hadUpdateOrganizationNameErrorLive, isParentAuthenticatedLive, organizationNameLive
) { apps, dialog, hadUpdateOrganizationNameError, isParentAuthenticated, organizationName ->
OwnerScreen.Normal(
isParentAuthenticated = isParentAuthenticated,
organizationName = organizationName,
appListDialog = dialog,
scopes = scopes,
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(
integration: PlatformIntegration,

View file

@ -87,13 +87,6 @@
android:layout_width="match_parent"
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
style="?materialButtonOutlinedStyle"
android:id="@+id/diagnose_dom"

View file

@ -561,8 +561,7 @@
<string name="diagnose_bat_level">Akkustand: %d\%%</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_not_supported_toast">Ihr System ermöglicht es nicht, einen Organisationsnamen zu hinterlegen</string>
<string name="diagnose_don_auth">Melden Sie sich an, um den Namen zu ändern</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>

View file

@ -614,8 +614,7 @@
<string name="diagnose_bat_level">Level: %d\%%</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_not_supported_toast">Your system does not support setting the organization name</string>
<string name="diagnose_don_auth">Sign in to change the name</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>