ViewModel refactoring: Settings part

User restriction shortcuts
Optimize ShortcutUtils
Fix Private DNS bug
This commit is contained in:
BinTianqi
2025-10-12 13:16:50 +08:00
parent 44aad18814
commit 9e1d18b8e7
22 changed files with 420 additions and 266 deletions

View File

@@ -9,10 +9,9 @@ import android.util.Log
class ApiReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val requestKey = intent.getStringExtra("key")
var log = "OwnDroid API request received. action: ${intent.action}\nkey: $requestKey"
if(!SP.isApiEnabled) return
val key = SP.apiKey
if(!key.isNullOrEmpty() && key == requestKey) {
var log = "OwnDroid API request received. action: ${intent.action}"
val key = SP.apiKeyHash
if(!key.isNullOrEmpty() && key == requestKey?.hash()) {
val app = intent.getStringExtra("package")
val permission = intent.getStringExtra("permission")
val restriction = intent.getStringExtra("restriction")

View File

@@ -1,9 +1,11 @@
package com.bintianqi.owndroid
import android.Manifest
import android.os.Build.VERSION
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -240,6 +242,10 @@ class MainActivity : FragmentActivity() {
val locale = context.resources?.configuration?.locale
zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA
val vm by viewModels<MyViewModel>()
if (VERSION.SDK_INT >= 33) {
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
setContent {
var appLockDialog by rememberSaveable { mutableStateOf(false) }
val theme by vm.theme.collectAsStateWithLifecycle()
@@ -576,7 +582,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
composable<UserRestrictionOptions> {
UserRestrictionOptionsScreen(it.toRoute(), vm.userRestrictions,
vm::setUserRestriction, ::navigateUp)
vm::setUserRestriction, vm::createUserRestrictionShortcut, ::navigateUp)
}
composable<Users> { UsersScreen(vm, ::navigateUp, ::navigate) }
@@ -619,14 +625,22 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<RequiredPasswordQuality> { RequiredPasswordQualityScreen(::navigateUp) }
composable<Settings> { SettingsScreen(::navigateUp, ::navigate) }
composable<SettingsOptions> { SettingsOptionsScreen(::navigateUp) }
composable<Appearance> {
val theme by vm.theme.collectAsStateWithLifecycle()
AppearanceScreen(::navigateUp, theme, vm::changeTheme)
composable<SettingsOptions> {
SettingsOptionsScreen(vm::getDisplayDangerousFeatures, vm::getShortcutsEnabled,
vm::setDisplayDangerousFeatures, vm::setShortcutsEnabled, ::navigateUp)
}
composable<Appearance> {
AppearanceScreen(::navigateUp, vm.theme, vm::changeTheme)
}
composable<AppLockSettings> {
AppLockSettingsScreen(vm::getAppLockConfig, vm::setAppLockConfig, ::navigateUp)
}
composable<ApiSettings> {
ApiSettings(vm::getApiEnabled, vm::setApiKey, ::navigateUp)
}
composable<Notifications> {
NotificationsScreen(vm::getEnabledNotifications, vm::setNotificationEnabled, ::navigateUp)
}
composable<AppLockSettings> { AppLockSettingsScreen(::navigateUp) }
composable<ApiSettings> { ApiSettings(::navigateUp) }
composable<Notifications> { NotificationsScreen(::navigateUp) }
composable<About> { AboutScreen(::navigateUp) }
}
DisposableEffect(lifecycleOwner) {

View File

@@ -13,6 +13,7 @@ class MyApplication : Application() {
val dbHelper = MyDbHelper(this)
myRepo = MyRepository(dbHelper)
Privilege.initialize(applicationContext)
NotificationUtils.createChannels(this)
}
}

View File

@@ -122,6 +122,7 @@ import kotlin.reflect.jvm.jvmErasure
class MyViewModel(application: Application): AndroidViewModel(application) {
val myRepo = getApplication<MyApplication>().myRepo
val PM = application.packageManager
val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme))
fun changeTheme(newTheme: ThemeSettings) {
theme.value = newTheme
@@ -129,6 +130,54 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
SP.darkTheme = newTheme.darkTheme
SP.blackTheme = newTheme.blackTheme
}
fun getDisplayDangerousFeatures(): Boolean {
return SP.displayDangerousFeatures
}
fun getShortcutsEnabled(): Boolean {
return SP.shortcuts
}
fun setDisplayDangerousFeatures(state: Boolean) {
SP.displayDangerousFeatures = state
}
fun setShortcutsEnabled(enabled: Boolean) {
SP.shortcuts = enabled
ShortcutUtils.setAllShortcuts(application, enabled)
}
fun getAppLockConfig(): AppLockConfig {
val passwordHash = SP.lockPasswordHash
return AppLockConfig(passwordHash?.ifEmpty { null }, SP.biometricsUnlock, SP.lockWhenLeaving)
}
fun setAppLockConfig(config: AppLockConfig) {
SP.lockPasswordHash = if (config.password == null) {
""
} else {
config.password.hash()
}
SP.biometricsUnlock = config.biometrics
SP.lockWhenLeaving = config.whenLeaving
}
fun getApiEnabled(): Boolean {
return SP.apiKeyHash?.isNotEmpty() ?: false
}
fun setApiKey(key: String) {
SP.apiKeyHash = if (key.isEmpty()) "" else key.hash()
}
fun getEnabledNotifications(): List<NotificationType> {
val list = SP.notifications?.split(',')?.mapNotNull { it.toIntOrNull() }
return if (list == null) {
NotificationType.entries
} else {
NotificationType.entries.filter { it.id in list }
}
}
fun setNotificationEnabled(type: NotificationType, enabled: Boolean) {
val list = SP.notifications?.split(',')?.mapNotNull { it.toIntOrNull() }
SP.notifications = if (list == null) {
NotificationType.entries.minus(type).map { it.id }
} else {
list.run { if (enabled) plus(type.id) else minus(type.id) }
}.joinToString { it.toString() }
}
val chosenPackage = Channel<String>(1, BufferOverflow.DROP_LATEST)
@@ -1083,11 +1132,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.clearUserRestriction(DAR, name)
}
userRestrictions.update { it.plus(name to state) }
ShortcutUtils.updateUserRestrictionShortcut(application, name, !state, true)
true
} catch (_: SecurityException) {
false
}
}
fun createUserRestrictionShortcut(id: String): Boolean {
return ShortcutUtils.setUserRestrictionShortcut(
application, id, userRestrictions.value[id] ?: true
)
}
fun createWorkProfile(options: CreateWorkProfileOptions): Intent {
val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
if (VERSION.SDK_INT >= 23) {
@@ -1478,7 +1533,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getPrivateDns(): PrivateDnsConfiguration {
val mode = DPM.getGlobalPrivateDnsMode(DAR)
return PrivateDnsConfiguration(
PrivateDnsMode.entries.find { it.id == mode }!!, DPM.getGlobalPrivateDnsHost(DAR) ?: ""
PrivateDnsMode.entries.find { it.id == mode }, DPM.getGlobalPrivateDnsHost(DAR) ?: ""
)
}
@Suppress("PrivateApi")
@@ -1489,7 +1544,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
field.isAccessible = true
val dpm = field.get(DPM) as IDevicePolicyManager
val host = if (conf.mode == PrivateDnsMode.Host) conf.host else null
val result = dpm.setGlobalPrivateDns(DAR, conf.mode.id, host)
val result = dpm.setGlobalPrivateDns(DAR, conf.mode!!.id, host)
result == DevicePolicyManager.PRIVATE_DNS_SET_NO_ERROR
} catch (e: Exception) {
e.printStackTrace()
@@ -1669,7 +1724,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getRpTokenState(): RpTokenState {
return try {
RpTokenState(true, DPM.isResetPasswordTokenActive(DAR))
} catch (_: IllegalArgumentException) {
} catch (_: IllegalStateException) {
RpTokenState(false, false)
}
}

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
object NotificationUtils {
fun checkPermission(context: Context): Boolean {
@@ -14,36 +15,57 @@ object NotificationUtils {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
else false
}
fun registerChannels(context: Context) {
if(Build.VERSION.SDK_INT < 26) return
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < 26) return
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val lockTaskMode = NotificationChannel(Channel.LOCK_TASK_MODE, context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH)
val events = NotificationChannel(Channel.EVENTS, context.getString(R.string.events), NotificationManager.IMPORTANCE_HIGH)
val lockTaskMode = NotificationChannel(
MyNotificationChannel.LockTaskMode.id,
context.getString(MyNotificationChannel.LockTaskMode.text),
NotificationManager.IMPORTANCE_HIGH
)
val events = NotificationChannel(
MyNotificationChannel.Events.id,
context.getString(MyNotificationChannel.Events.text),
NotificationManager.IMPORTANCE_HIGH
)
nm.createNotificationChannels(listOf(lockTaskMode, events))
}
fun notify(context: Context, id: Int, notification: Notification) {
val sp = context.getSharedPreferences("data", Context.MODE_PRIVATE)
if(sp.getBoolean("n_$id", true) && checkPermission(context)) {
registerChannels(context)
fun notifyEvent(context: Context, type: NotificationType, text: String) {
val notification = NotificationCompat.Builder(context, MyNotificationChannel.Events.id)
.setSmallIcon(type.icon)
.setContentTitle(context.getString(type.text))
.setContentText(text)
.build()
notify(context, type, notification)
}
fun notify(context: Context, type: NotificationType, notification: Notification) {
val enabledNotifications = SP.notifications?.split(',')?.mapNotNull { it.toIntOrNull() }
if (enabledNotifications == null || type.id in enabledNotifications) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(id, notification)
nm.notify(type.id, notification)
}
}
object Channel {
const val LOCK_TASK_MODE = "LockTaskMode"
const val EVENTS = "Events"
}
object ID {
const val LOCK_TASK_MODE = 1
const val PASSWORD_CHANGED = 2
const val USER_ADDED = 3
const val USER_STARTED = 4
const val USER_SWITCHED = 5
const val USER_STOPPED = 6
const val USER_REMOVED = 7
const val BUG_REPORT_SHARED = 8
const val BUG_REPORT_SHARING_DECLINED = 9
const val BUG_REPORT_FAILED = 10
const val SYSTEM_UPDATE_PENDING = 11
fun cancel(context: Context, type: NotificationType) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(type.id)
}
}
enum class NotificationType(val id: Int, val text: Int, val icon: Int) {
LockTaskMode(1, R.string.lock_task_mode, R.drawable.lock_fill0),
PasswordChanged(2, R.string.password_changed, R.drawable.password_fill0),
UserAdded(3, R.string.user_added, R.drawable.person_add_fill0),
UserStarted(4, R.string.user_started, R.drawable.person_fill0),
UserSwitched(5, R.string.user_switched, R.drawable.person_fill0),
UserStopped(6, R.string.user_stopped, R.drawable.person_off),
UserRemoved(7, R.string.user_removed, R.drawable.person_remove_fill0),
BugReportShared(8, R.string.bug_report_shared, R.drawable.bug_report_fill0),
BugReportSharingDeclined(9, R.string.bug_report_sharing_declined, R.drawable.bug_report_fill0),
BugReportFailed(10, R.string.bug_report_failed, R.drawable.bug_report_fill0),
SystemUpdatePending(11, R.string.system_update_pending, R.drawable.system_update_fill0)
}
enum class MyNotificationChannel(val id: String, val text: Int) {
LockTaskMode("LockTaskMode", R.string.lock_task_mode),
Events("Events", R.string.events)
}

View File

@@ -1,6 +1,5 @@
package com.bintianqi.owndroid
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DeviceAdminReceiver
import android.content.ComponentName
@@ -17,9 +16,6 @@ import com.bintianqi.owndroid.dpm.processSecurityLogs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class Receiver : DeviceAdminReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -75,68 +71,59 @@ class Receiver : DeviceAdminReceiver() {
override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
super.onLockTaskModeEntering(context, intent, pkg)
if(!NotificationUtils.checkPermission(context)) return
NotificationUtils.registerChannels(context)
if (!NotificationUtils.checkPermission(context)) return
val intent = Intent(context, this::class.java).setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE")
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.LOCK_TASK_MODE)
val builder = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id)
.setContentTitle(context.getText(R.string.lock_task_mode))
.setSmallIcon(R.drawable.lock_fill0)
.addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build())
NotificationUtils.notify(context, NotificationUtils.ID.LOCK_TASK_MODE, builder.build())
NotificationUtils.notify(context, NotificationType.LockTaskMode, builder.build())
}
override fun onLockTaskModeExiting(context: Context, intent: Intent) {
super.onLockTaskModeExiting(context, intent)
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(NotificationUtils.ID.LOCK_TASK_MODE)
NotificationUtils.cancel(context, NotificationType.LockTaskMode)
}
override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) {
super.onPasswordChanged(context, intent, userHandle)
sendUserRelatedNotification(context, userHandle, NotificationUtils.ID.PASSWORD_CHANGED, R.string.password_changed, R.drawable.password_fill0)
sendUserRelatedNotification(context, userHandle, NotificationType.PasswordChanged)
}
override fun onUserAdded(context: Context, intent: Intent, addedUser: UserHandle) {
super.onUserAdded(context, intent, addedUser)
sendUserRelatedNotification(context, addedUser, NotificationUtils.ID.USER_ADDED, R.string.user_added, R.drawable.person_add_fill0)
sendUserRelatedNotification(context, addedUser, NotificationType.UserAdded)
}
override fun onUserStarted(context: Context, intent: Intent, startedUser: UserHandle) {
super.onUserStarted(context, intent, startedUser)
sendUserRelatedNotification(context, startedUser, NotificationUtils.ID.USER_STARTED, R.string.user_started, R.drawable.person_fill0)
sendUserRelatedNotification(context, startedUser, NotificationType.UserStarted)
}
override fun onUserSwitched(context: Context, intent: Intent, switchedUser: UserHandle) {
super.onUserSwitched(context, intent, switchedUser)
sendUserRelatedNotification(context, switchedUser, NotificationUtils.ID.USER_SWITCHED, R.string.user_switched, R.drawable.person_fill0)
sendUserRelatedNotification(context, switchedUser, NotificationType.UserSwitched)
}
override fun onUserStopped(context: Context, intent: Intent, stoppedUser: UserHandle) {
super.onUserStopped(context, intent, stoppedUser)
sendUserRelatedNotification(context, stoppedUser, NotificationUtils.ID.USER_STOPPED, R.string.user_stopped, R.drawable.person_fill0)
sendUserRelatedNotification(context, stoppedUser, NotificationType.UserStopped)
}
override fun onUserRemoved(context: Context, intent: Intent, removedUser: UserHandle) {
super.onUserRemoved(context, intent, removedUser)
sendUserRelatedNotification(context, removedUser, NotificationUtils.ID.USER_REMOVED, R.string.user_removed, R.drawable.person_remove_fill0)
sendUserRelatedNotification(context, removedUser, NotificationType.UserRemoved)
}
override fun onBugreportShared(context: Context, intent: Intent, hash: String) {
super.onBugreportShared(context, intent, hash)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS)
.setContentTitle(context.getString(R.string.bug_report_shared))
.setContentText("SHA-256 hash: $hash")
.setSmallIcon(R.drawable.bug_report_fill0)
NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_SHARED, builder.build())
NotificationUtils.notifyEvent(context, NotificationType.BugReportShared, "SHA-256 hash: $hash")
}
override fun onBugreportSharingDeclined(context: Context, intent: Intent) {
super.onBugreportSharingDeclined(context, intent)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS)
.setContentTitle(context.getString(R.string.bug_report_sharing_declined))
.setSmallIcon(R.drawable.bug_report_fill0)
NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_SHARING_DECLINED, builder.build())
NotificationUtils.notifyEvent(context, NotificationType.BugReportSharingDeclined, "")
}
override fun onBugreportFailed(context: Context, intent: Intent, failureCode: Int) {
@@ -146,30 +133,21 @@ class Receiver : DeviceAdminReceiver() {
BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE -> R.string.bug_report_failure_no_longer_available
else -> R.string.place_holder
}
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS)
.setContentTitle(context.getString(R.string.bug_report_failed))
.setContentText(context.getString(message))
.setSmallIcon(R.drawable.bug_report_fill0)
NotificationUtils.notify(context, NotificationUtils.ID.BUG_REPORT_FAILED, builder.build())
NotificationUtils.notifyEvent(context, NotificationType.BugReportFailed, context.getString(message))
}
override fun onSystemUpdatePending(context: Context, intent: Intent, receivedTime: Long) {
super.onSystemUpdatePending(context, intent, receivedTime)
val time = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(receivedTime))
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS)
.setContentTitle(context.getString(R.string.system_update_pending))
.setContentText(context.getString(R.string.received_time) + ": $time")
.setSmallIcon(R.drawable.system_update_fill0)
NotificationUtils.notify(context, NotificationUtils.ID.SYSTEM_UPDATE_PENDING, builder.build())
val text = context.getString(R.string.received_time) + ": " + formatDate(receivedTime)
NotificationUtils.notifyEvent(context, NotificationType.SystemUpdatePending, text)
}
private fun sendUserRelatedNotification(context: Context, userHandle: UserHandle, id: Int, title: Int, icon: Int) {
private fun sendUserRelatedNotification(
context: Context, userHandle: UserHandle, type: NotificationType
) {
val um = context.getSystemService(Context.USER_SERVICE) as UserManager
val serial = um.getSerialNumberForUser(userHandle)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS)
.setContentTitle(context.getString(title))
.setContentText(context.getString(R.string.serial_number) + ": $serial")
.setSmallIcon(icon)
NotificationUtils.notify(context, id, builder.build())
val text = context.getString(R.string.serial_number) + ": $serial"
NotificationUtils.notifyEvent(context, type, text)
}
}

View File

@@ -17,9 +17,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -39,24 +37,23 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ui.FunctionItem
@@ -64,8 +61,8 @@ import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.SwitchItem
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -142,20 +139,25 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
@Serializable object SettingsOptions
@Composable
fun SettingsOptionsScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
fun SettingsOptionsScreen(
getDisplayDangerousFeatures: () -> Boolean, getShortcutsEnabled: () -> Boolean,
setDisplayDangerousFeatures: (Boolean) -> Unit, setShortcutsEnabled: (Boolean) -> Unit,
onNavigateUp: () -> Unit
) {
var dangerousFeatures by remember { mutableStateOf(getDisplayDangerousFeatures()) }
var shortcuts by remember { mutableStateOf(getShortcutsEnabled()) }
MyScaffold(R.string.options, onNavigateUp, 0.dp) {
SwitchItem(
R.string.show_dangerous_features, icon = R.drawable.warning_fill0,
getState = { SP.displayDangerousFeatures },
onCheckedChange = { SP.displayDangerousFeatures = it }
R.string.show_dangerous_features, dangerousFeatures, {
setDisplayDangerousFeatures(it)
dangerousFeatures = it
}, R.drawable.warning_fill0
)
SwitchItem(
R.string.shortcuts, icon = R.drawable.open_in_new,
getState = { SP.shortcuts }, onCheckedChange = {
SP.shortcuts = it
ShortcutUtils.setAllShortcuts(context)
}
R.string.shortcuts, shortcuts, {
setShortcutsEnabled(it)
shortcuts = it
}, R.drawable.open_in_new
)
}
}
@@ -163,13 +165,12 @@ fun SettingsOptionsScreen(onNavigateUp: () -> Unit) {
@Serializable object Appearance
@Composable
fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onThemeChange: (ThemeSettings) -> Unit) {
fun AppearanceScreen(
onNavigateUp: () -> Unit, currentTheme: StateFlow<ThemeSettings>,
setTheme: (ThemeSettings) -> Unit
) {
var darkThemeMenu by remember { mutableStateOf(false) }
var theme by remember { mutableStateOf(currentTheme) }
fun update(it: ThemeSettings) {
theme = it
onThemeChange(it)
}
val theme by currentTheme.collectAsStateWithLifecycle()
val darkThemeTextID = when(theme.darkTheme) {
1 -> R.string.on
0 -> R.string.off
@@ -180,7 +181,7 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh
SwitchItem(
R.string.material_you_color,
state = theme.materialYou,
onCheckedChange = { update(theme.copy(materialYou = it)) }
onCheckedChange = { setTheme(theme.copy(materialYou = it)) }
)
}
Box {
@@ -192,22 +193,21 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh
DropdownMenuItem(
text = { Text(stringResource(R.string.follow_system)) },
onClick = {
update(theme.copy(darkTheme = -1))
setTheme(theme.copy(darkTheme = -1))
darkThemeMenu = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.on)) },
onClick = {
update(theme.copy(darkTheme = 1))
theme = theme.copy(darkTheme = 1)
setTheme(theme.copy(darkTheme = 1))
darkThemeMenu = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.off)) },
onClick = {
update(theme.copy(darkTheme = 0))
setTheme(theme.copy(darkTheme = 0))
darkThemeMenu = false
}
)
@@ -216,62 +216,67 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh
AnimatedVisibility(theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme())) {
SwitchItem(
R.string.black_theme, state = theme.blackTheme,
onCheckedChange = { update(theme.copy(blackTheme = it)) }
onCheckedChange = { setTheme(theme.copy(blackTheme = it)) }
)
}
}
}
data class AppLockConfig(
/** null means no password, empty means password already set */
val password: String?, val biometrics: Boolean, val whenLeaving: Boolean
)
@Serializable object AppLockSettings
@Composable
fun AppLockSettingsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.app_lock, onNavigateUp, 0.dp) {
val fm = LocalFocusManager.current
fun AppLockSettingsScreen(
getConfig: () -> AppLockConfig, setConfig: (AppLockConfig) -> Unit,
onNavigateUp: () -> Unit
) = MyScaffold(R.string.app_lock, onNavigateUp) {
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var allowBiometrics by remember { mutableStateOf(SP.biometricsUnlock) }
var lockWhenLeaving by remember { mutableStateOf(SP.lockWhenLeaving) }
val fr = remember { FocusRequester() }
val alreadySet = !SP.lockPasswordHash.isNullOrEmpty()
val isInputLegal = password.length !in 1..3 && (alreadySet || (password.isNotEmpty() && password.isNotBlank()))
Column(Modifier
.widthIn(max = 300.dp)
.align(Alignment.CenterHorizontally)) {
var allowBiometrics by remember { mutableStateOf(false) }
var lockWhenLeaving by remember { mutableStateOf(false) }
var alreadySet by remember { mutableStateOf(false) }
val isInputLegal = password.length !in 1..3 && (alreadySet || password.isNotBlank())
LaunchedEffect(Unit) {
val config = getConfig()
password = config.password ?: ""
allowBiometrics = config.biometrics
lockWhenLeaving = config.whenLeaving
alreadySet = config.password != null
}
OutlinedTextField(
password, { password = it }, Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
password, { password = it }, Modifier.fillMaxWidth().padding(vertical = 4.dp),
label = { Text(stringResource(R.string.password)) },
supportingText = { Text(stringResource(if(alreadySet) R.string.leave_empty_to_remain_unchanged else R.string.minimum_length_4)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { fr.requestFocus() }
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next)
)
OutlinedTextField(
confirmPassword, { confirmPassword = it }, Modifier
.fillMaxWidth()
.focusRequester(fr),
confirmPassword, { confirmPassword = it }, Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.confirm_password)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { fm.clearFocus() }
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done)
)
if(VERSION.SDK_INT >= 28) Row(Modifier
.fillMaxWidth()
.padding(vertical = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
if (VERSION.SDK_INT >= 28) Row(
Modifier.fillMaxWidth().padding(vertical = 6.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Text(stringResource(R.string.allow_biometrics))
Switch(allowBiometrics, { allowBiometrics = it })
}
Row(Modifier
.fillMaxWidth()
.padding(bottom = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
Row(
Modifier.fillMaxWidth().padding(bottom = 6.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Text(stringResource(R.string.lock_when_leaving))
Switch(lockWhenLeaving, { lockWhenLeaving = it })
}
Button(
onClick = {
fm.clearFocus()
if(password.isNotEmpty()) SP.lockPasswordHash = password.hash()
SP.biometricsUnlock = allowBiometrics
SP.lockWhenLeaving = lockWhenLeaving
setConfig(AppLockConfig(password, allowBiometrics, lockWhenLeaving))
onNavigateUp()
},
modifier = Modifier.fillMaxWidth(),
@@ -279,85 +284,70 @@ fun AppLockSettingsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.app_lo
) {
Text(stringResource(if(alreadySet) R.string.update else R.string.set))
}
if(alreadySet) FilledTonalButton(
if (alreadySet) FilledTonalButton(
onClick = {
fm.clearFocus()
SP.lockPasswordHash = ""
SP.biometricsUnlock = false
SP.lockWhenLeaving = false
setConfig(AppLockConfig(null, false, false))
onNavigateUp()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.disable))
}
}
}
@Serializable object ApiSettings
@Composable
fun ApiSettings(onNavigateUp: () -> Unit) {
fun ApiSettings(
getEnabled: () -> Boolean, setKey: (String) -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current
var alreadyEnabled by remember { mutableStateOf(getEnabled()) }
MyScaffold(R.string.api, onNavigateUp) {
var enabled by remember { mutableStateOf(SP.isApiEnabled) }
var enabled by remember { mutableStateOf(alreadyEnabled) }
var key by remember { mutableStateOf("") }
SwitchItem(R.string.enable, state = enabled, onCheckedChange = {
enabled = it
SP.isApiEnabled = it
if(!it) SP.sharedPrefs.edit { remove("api.key") }
}, padding = false)
if(enabled) {
var key by remember { mutableStateOf("") }
if (enabled) {
OutlinedTextField(
value = key, onValueChange = { key = it }, label = { Text(stringResource(R.string.api_key)) },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp), readOnly = true,
key, { key = it }, Modifier.fillMaxWidth().padding(bottom = 4.dp),
label = { Text(stringResource(R.string.api_key)) },
trailingIcon = {
IconButton(
onClick = {
val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
val sr = SecureRandom()
key = (1..20).map { charset[sr.nextInt(charset.length)] }.joinToString("")
}
) {
Icon(painter = painterResource(R.drawable.casino_fill0), contentDescription = "Random")
IconButton({ key = generateBase64Key(10) }) {
Icon(painterResource(R.drawable.casino_fill0), null)
}
}
)
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp),
onClick = {
SP.apiKey = key
setKey(if (enabled) key else "")
alreadyEnabled = enabled
context.showOperationResultToast(true)
},
enabled = key.isNotEmpty()
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
enabled = !enabled || key.length !in 0..7
) {
Text(stringResource(R.string.apply))
}
if(SP.apiKey != null) Notes(R.string.api_key_exist)
}
if (enabled && alreadyEnabled) Notes(R.string.api_key_exist)
}
}
@Serializable object Notifications
@Composable
fun NotificationsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.notifications, onNavigateUp, 0.dp) {
val sp = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE)
val map = mapOf(
NotificationUtils.ID.PASSWORD_CHANGED to R.string.password_changed, NotificationUtils.ID.USER_ADDED to R.string.user_added,
NotificationUtils.ID.USER_STARTED to R.string.user_started, NotificationUtils.ID.USER_SWITCHED to R.string.user_switched,
NotificationUtils.ID.USER_STOPPED to R.string.user_stopped, NotificationUtils.ID.USER_REMOVED to R.string.user_removed,
NotificationUtils.ID.BUG_REPORT_SHARED to R.string.bug_report_shared,
NotificationUtils.ID.BUG_REPORT_SHARING_DECLINED to R.string.bug_report_sharing_declined,
NotificationUtils.ID.BUG_REPORT_FAILED to R.string.bug_report_failed,
NotificationUtils.ID.SYSTEM_UPDATE_PENDING to R.string.system_update_pending
)
map.forEach { (k, v) ->
SwitchItem(v, getState = { sp.getBoolean("n_$k", true) }, onCheckedChange = { sp.edit(true) { putBoolean("n_$k", it) } })
fun NotificationsScreen(
getState: () -> List<NotificationType>, setNotification: (NotificationType, Boolean) -> Unit,
onNavigateUp: () -> Unit
) = MyScaffold(R.string.notifications, onNavigateUp, 0.dp) {
val enabledNotifications = remember { mutableStateListOf(*getState().toTypedArray()) }
NotificationType.entries.forEach { type ->
SwitchItem(type.text, type in enabledNotifications, {
setNotification(type, it)
enabledNotifications.run { if (it) plusAssign(type) else minusAssign(type) }
})
}
}

View File

@@ -13,8 +13,7 @@ class SharedPrefs(context: Context) {
var dhizuku by BooleanSharedPref("dhizuku_mode")
var isDefaultAffiliationIdSet by BooleanSharedPref("default_affiliation_id_set")
var displayDangerousFeatures by BooleanSharedPref("display_dangerous_features")
var isApiEnabled by BooleanSharedPref("api.enabled")
var apiKey by StringSharedPref("api.key")
var apiKeyHash by StringSharedPref("api_key_hash")
var materialYou by BooleanSharedPref("theme.material_you", Build.VERSION.SDK_INT >= 31)
/** -1: follow system, 0: off, 1: on */
var darkTheme by IntSharedPref("theme.dark", -1)
@@ -25,6 +24,8 @@ class SharedPrefs(context: Context) {
var applicationsListView by BooleanSharedPref("applications.list_view", true)
var shortcuts by BooleanSharedPref("shortcuts")
var dhizukuServer by BooleanSharedPref("dhizuku_server")
var notifications by StringSharedPref("notifications")
var shortcutKey by StringSharedPref("shortcut_key")
}
private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty<SharedPrefs, Boolean> {

View File

@@ -5,10 +5,13 @@ import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import java.security.SecureRandom
import kotlin.io.encoding.Base64
object ShortcutUtils {
fun setAllShortcuts(context: Context) {
if (SP.shortcuts) {
fun setAllShortcuts(context: Context, enabled: Boolean) {
if (enabled) {
setShortcutKey()
val list = listOf(
createShortcut(context, MyShortcut.Lock, true),
createShortcut(context, MyShortcut.DisableCamera,
@@ -22,6 +25,7 @@ object ShortcutUtils {
}
}
fun setShortcut(context: Context, shortcut: MyShortcut, state: Boolean) {
setShortcutKey()
ShortcutManagerCompat.pushDynamicShortcut(
context, createShortcut(context, shortcut, state)
)
@@ -41,9 +45,47 @@ object ShortcutUtils {
.setIntent(
Intent(context, ShortcutsReceiverActivity::class.java)
.setAction("com.bintianqi.owndroid.action.${shortcut.id}")
.putExtra("key", SP.shortcutKey)
)
.build()
}
/** @param state If true, set the user restriction */
fun createUserRestrictionShortcut(context: Context, id: String, state: Boolean): ShortcutInfoCompat {
val restriction = UserRestrictionsRepository.findRestrictionById(id)
val label = context.getString(if (state) R.string.disable else R.string.enable) + " " +
context.getString(restriction.name)
setShortcutKey()
return ShortcutInfoCompat.Builder(context, "USER_RESTRICTION-$id")
.setIcon(IconCompat.createWithResource(context, restriction.icon))
.setShortLabel(label)
.setIntent(
Intent(context, ShortcutsReceiverActivity::class.java)
.setAction("com.bintianqi.owndroid.action.USER_RESTRICTION")
.putExtra("restriction", id)
.putExtra("state", state)
.putExtra("key", SP.shortcutKey)
)
.build()
}
fun setUserRestrictionShortcut(context: Context, id: String, state: Boolean): Boolean {
val shortcut = createUserRestrictionShortcut(context, id, state)
return ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
}
fun updateUserRestrictionShortcut(context: Context, id: String, state: Boolean, checkExist: Boolean) {
if (checkExist) {
val shortcuts = ShortcutManagerCompat.getShortcuts(
context, ShortcutManagerCompat.FLAG_MATCH_PINNED
)
if (shortcuts.find { it.id == "USER_RESTRICTION-$id" } == null) return
}
val shortcut = createUserRestrictionShortcut(context, id, state)
ShortcutManagerCompat.updateShortcuts(context, listOf(shortcut))
}
fun setShortcutKey() {
if (SP.shortcutKey.isNullOrEmpty()) {
SP.shortcutKey = generateBase64Key(10)
}
}
}
enum class MyShortcut(

View File

@@ -9,7 +9,9 @@ class ShortcutsReceiverActivity : Activity() {
super.onCreate(savedInstanceState)
try {
val action = intent.action?.removePrefix("com.bintianqi.owndroid.action.")
if (action != null && SP.shortcuts) {
val key = SP.shortcutKey
val requestKey = intent?.getStringExtra("key")
if (action != null && SP.shortcuts && key != null && requestKey == key) {
when (action) {
"LOCK" -> Privilege.DPM.lockNow()
"DISABLE_CAMERA" -> {
@@ -22,9 +24,22 @@ class ShortcutsReceiverActivity : Activity() {
Privilege.DPM.setMasterVolumeMuted(Privilege.DAR, !state)
ShortcutUtils.setShortcut(this, MyShortcut.Mute, state)
}
"USER_RESTRICTION" -> {
val state = intent?.getBooleanExtra("state", false)
val id = intent?.getStringExtra("restriction")
if (state == null || id == null) return
if (state) {
Privilege.DPM.addUserRestriction(Privilege.DAR, id)
} else {
Privilege.DPM.clearUserRestriction(Privilege.DAR, id)
}
ShortcutUtils.updateUserRestrictionShortcut(this, id, !state, false)
}
}
Log.d(TAG, "Received intent: $action")
showOperationResultToast(true)
} else {
showOperationResultToast(false)
}
} finally {
finish()

View File

@@ -98,6 +98,13 @@ object UserRestrictionsRepository {
UserRestrictionCategory.Other -> other
}.filter { Build.VERSION.SDK_INT >= it.requiresApi }
}
fun findRestrictionById(id: String): Restriction {
listOf(network, connectivity, applications, media, users, other).forEach { list ->
val restriction = list.find { it.id == id }
if (restriction != null) return restriction
}
throw Exception("User restriction not found")
}
}
enum class UserRestrictionCategory(val title: Int, val icon: Int) {

View File

@@ -18,10 +18,12 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.io.encoding.Base64
var zhCN = true
@@ -64,8 +66,8 @@ fun formatFileSize(bytes: Long): String {
val Boolean.yesOrNo
@StringRes get() = if(this) R.string.yes else R.string.no
fun formatTime(ms: Long): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(ms))
fun formatDate(ms: Long): String {
return formatDate(Date(ms))
}
fun formatDate(date: Date): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(date)
@@ -121,3 +123,9 @@ class SerializableSaver<T>(val serializer: KSerializer<T>) : Saver<T, String> {
return Json.encodeToString(serializer, value)
}
}
fun generateBase64Key(length: Int): String {
val ba = ByteArray(length)
SecureRandom().nextBytes(ba)
return Base64.withPadding(Base64.PaddingOption.ABSENT).encode(ba)
}

View File

@@ -443,13 +443,13 @@ fun handlePrivilegeChange(context: Context) {
SP.dhizukuServer = false
SP.shortcuts = privilege.activated
if (privilege.activated) {
ShortcutUtils.setAllShortcuts(context)
ShortcutUtils.setAllShortcuts(context, true)
if (!privilege.dhizuku) {
setDefaultAffiliationID()
}
} else {
SP.isDefaultAffiliationIdSet = false
ShortcutUtils.setAllShortcuts(context)
SP.isApiEnabled = false
ShortcutUtils.setAllShortcuts(context, false)
SP.apiKeyHash = ""
}
}

View File

@@ -70,7 +70,6 @@ import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
@@ -115,7 +114,7 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.ErrorDialog
@@ -511,7 +510,7 @@ fun UpdateNetworkScreen(info: WifiInfo, setNetwork: (WifiInfo) -> Boolean, onNav
TopAppBar(
{ Text(stringResource(R.string.update_network)) },
navigationIcon = { NavIcon(onNavigateUp) },
colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer)
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainer)
)
},
contentWindowInsets = WindowInsets.ime
@@ -1046,14 +1045,14 @@ fun NetworkStatsScreen(
}
}
OutlinedTextField(
value = startTime.let { if(it == -1L) "" else formatTime(it) }, onValueChange = {}, readOnly = true,
value = startTime.let { if(it == -1L) "" else formatDate(it) }, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.start_time)) },
interactionSource = startTimeIs,
isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
)
OutlinedTextField(
value = formatTime(endTime), onValueChange = {}, readOnly = true,
value = formatDate(endTime), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.end_time)) },
interactionSource = endTimeIs,
isError = startTime >= endTime,
@@ -1292,7 +1291,7 @@ fun NetworkStatsViewerScreen(
HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page ->
val item = data[index]
Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) {
Text(formatTime(item.startTime) + "\n~\n" + formatTime(item.endTime),
Text(formatDate(item.startTime) + "\n~\n" + formatDate(item.endTime),
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center)
Spacer(Modifier.height(5.dp))
val txBytes = item.txBytes
@@ -1350,7 +1349,7 @@ enum class PrivateDnsMode(val id: Int, val text: Int) {
Host(DevicePolicyManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.string.enabled)
}
data class PrivateDnsConfiguration(val mode: PrivateDnsMode, val host: String)
data class PrivateDnsConfiguration(val mode: PrivateDnsMode?, val host: String)
@Serializable object PrivateDns
@@ -1362,7 +1361,7 @@ fun PrivateDnsScreen(
) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
var mode by remember { mutableStateOf(PrivateDnsMode.Opportunistic) }
var mode by remember { mutableStateOf<PrivateDnsMode?>(PrivateDnsMode.Opportunistic) }
var inputHost by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
val conf = getPrivateDns()
@@ -1385,7 +1384,8 @@ fun PrivateDnsScreen(
val result = setPrivateDns(PrivateDnsConfiguration(mode, inputHost))
context.showOperationResultToast(result)
},
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
enabled = mode != null
) {
Text(stringResource(R.string.apply))
}

View File

@@ -30,6 +30,8 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
@@ -45,6 +47,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -56,6 +59,7 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.generateBase64Key
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -250,7 +254,12 @@ fun ResetPasswordTokenScreen(
OutlinedTextField(
token, { token = it }, Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.token)) },
supportingText = { Text("${token.length}/32") }
supportingText = { Text("${token.length}/32") },
trailingIcon = {
IconButton({ token = generateBase64Key(24) }) {
Icon(painterResource(R.drawable.casino_fill0), null)
}
}
)
Button(
onClick = {
@@ -265,7 +274,12 @@ fun ResetPasswordTokenScreen(
}
if (state.set && !state.active) Button(
onClick = {
getIntent()?.let { launcher.launch(it) }
val intent = getIntent()
if (intent == null) {
context.showOperationResultToast(false)
} else {
launcher.launch(intent)
}
},
modifier = Modifier.fillMaxWidth()
) {

View File

@@ -112,7 +112,7 @@ import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -588,7 +588,7 @@ fun ChangeTimeScreen(setTime: (Long, Boolean) -> Boolean, onNavigateUp: () -> Un
) {
if(page == 0) {
OutlinedTextField(
value = datePickerState.selectedDateMillis?.let { formatTime(it) } ?: "",
value = datePickerState.selectedDateMillis?.let { formatDate(it) } ?: "",
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.date)) },
interactionSource = dateInteractionSource,
@@ -1436,9 +1436,9 @@ fun CaCertScreen(
Text("Issuer", style = typography.labelLarge)
SelectionContainer { Text(cert.issuer) }
Text("Issued on", style = typography.labelLarge)
SelectionContainer { Text(formatTime(cert.issuedTime)) }
SelectionContainer { Text(formatDate(cert.issuedTime)) }
Text("Expires on", style = typography.labelLarge)
SelectionContainer { Text(formatTime(cert.expiresTime)) }
SelectionContainer { Text(formatDate(cert.expiresTime)) }
Text("SHA-256 fingerprint", style = typography.labelLarge)
SelectionContainer { Text(cert.hash) }
if (dialog == 2) Row(
@@ -1929,7 +1929,7 @@ fun SystemUpdatePolicyScreen(
if (VERSION.SDK_INT >= 26) {
Column(Modifier.padding(HorizontalPadding)) {
if (pendingUpdate.exists) {
Text(stringResource(R.string.update_received_time, formatTime(pendingUpdate.time)))
Text(stringResource(R.string.update_received_time, formatDate(pendingUpdate.time)))
Text(stringResource(R.string.is_security_patch,
stringResource(pendingUpdate.securityPatch.yesOrNo)))
} else {

View File

@@ -1,6 +1,8 @@
package com.bintianqi.owndroid.dpm
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -14,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
@@ -21,6 +24,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -54,6 +58,7 @@ import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.UserRestrictionCategory
import com.bintianqi.owndroid.UserRestrictionsRepository
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.MyLazyScaffold
@@ -117,6 +122,17 @@ fun UserRestrictionScreen(
onNavigate(UserRestrictionOptions(it.name))
}
}
Row(
Modifier
.padding(HorizontalPadding, 10.dp)
.fillMaxWidth()
.background(colorScheme.primaryContainer, RoundedCornerShape(8.dp))
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Outlined.Info, null, Modifier.padding(end = 8.dp), colorScheme.onPrimaryContainer)
Text(stringResource(R.string.user_restriction_tip), color = colorScheme.onPrimaryContainer)
}
}
}
}
@@ -128,7 +144,8 @@ data class UserRestrictionOptions(val id: String)
@Composable
fun UserRestrictionOptionsScreen(
args: UserRestrictionOptions, userRestrictions: StateFlow<Map<String, Boolean>>,
setRestriction: (String, Boolean) -> Boolean, onNavigateUp: () -> Unit
setRestriction: (String, Boolean) -> Boolean, setShortcut: (String) -> Boolean,
onNavigateUp: () -> Unit
) {
val context = LocalContext.current
val status by userRestrictions.collectAsStateWithLifecycle()
@@ -136,7 +153,12 @@ fun UserRestrictionOptionsScreen(
MyLazyScaffold(title, onNavigateUp) {
items(items) { restriction ->
Row(
Modifier.fillMaxWidth().padding(15.dp, 6.dp),
Modifier
.fillMaxWidth()
.combinedClickable(onClick = {}, onLongClick = {
if (!setShortcut(restriction.id)) context.popToast(R.string.unsupported)
})
.padding(15.dp, 6.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {

View File

@@ -58,7 +58,7 @@ import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
@@ -205,7 +205,7 @@ fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) {
if (VERSION.SDK_INT >= 23) InfoItem(R.string.system_user, info.system.yesOrNo)
if (VERSION.SDK_INT >= 34) InfoItem(R.string.admin_user, info.admin.yesOrNo)
if (VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, info.demo.yesOrNo)
if (info.time != 0L) InfoItem(R.string.creation_time, formatTime(info.time))
if (info.time != 0L) InfoItem(R.string.creation_time, formatDate(info.time))
if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.logout_enabled, info.logout.yesOrNo)

View File

@@ -476,7 +476,6 @@
<!--Пароль и блокировка экрана-->
<string name="password_and_keyguard">Пароль и блокировка экрана</string>
<string name="password_info">Информация о пароле</string>
<string name="reset_pwd_desc">Оставьте пустым, чтобы удалить пароль</string>
<string name="max_pwd_fail">Максимальное количество неудачных попыток ввода пароля</string>
<string name="max_pwd_fail_textfield">Максимальное количество неудачных попыток</string>
<string name="pwd_expiration_timeout">Время истечения срока действия пароля</string>
@@ -492,17 +491,14 @@
<string name="unified_password">Единый пароль</string>
<string name="reset_password_token">Сбросить токен пароля</string>
<string name="token">Токен</string>
<string name="token_must_longer_than_32_byte">Токен должен быть длиннее 32 байт</string>
<string name="token_activated">Тoken activated</string><!--TODO-->
<string name="clear">Очистить</string>
<string name="set">Установить</string>
<string name="please_set_a_token">Установите токен</string>
<string name="activate_token_not_required_when_no_password">Токен будет автоматически активирован, если пароль не установлен.</string>
<string name="reset_password">Сбросить пароль</string>
<string name="confirm_password">Подтвердите пароль</string>
<string name="do_not_ask_credentials_on_boot">Не запрашивать учетные данные при загрузке</string>
<string name="reset_password_require_entry">Требовать ввод</string>
<string name="reset_password_with_token">Сбросить пароль с помощью токена</string>
<string name="required_password_complexity">Требуемая сложность пароля</string>
<string name="disable_keyguard_features">Функции блокировки экрана (Keyguard)</string>
<string name="enable_all">Включить все</string>

View File

@@ -501,7 +501,6 @@
<!--Password&Keyguard-->
<string name="password_and_keyguard">Parola ve Kilit Ekranı</string>
<string name="password_info">Parola Bilgisi</string>
<string name="reset_pwd_desc">Parolayı kaldırmak için boş bırakın</string>
<string name="max_pwd_fail">Maksimum Başarısız Parola</string>
<string name="max_pwd_fail_textfield">Maksimum başarısız deneme sayısı</string>
<string name="pwd_expiration_timeout">Parola Son Kullanma Zaman Aşımı</string>
@@ -516,17 +515,14 @@
<string name="unified_password">Birleşik Parola</string>
<string name="reset_password_token">Parola Sıfırlama Jetonu</string>
<string name="token">Jeton</string>
<string name="token_must_longer_than_32_byte">Jeton 32 bayttan uzun olmalıdır</string>
<string name="token_activated">Token activated</string><!--TODO-->
<string name="clear">Temizle</string>
<string name="set">Ayarla</string>
<string name="please_set_a_token">Lütfen bir jeton ayarlayın</string>
<string name="activate_token_not_required_when_no_password">Parola ayarlanmamışsa jeton otomatik olarak etkinleştirilir.</string>
<string name="reset_password">Parolayı Sıfırla</string>
<string name="confirm_password">Parolayı Onayla</string>
<string name="do_not_ask_credentials_on_boot">Başlangıçta kimlik bilgisi sorma</string>
<string name="reset_password_require_entry">Giriş gerektir</string>
<string name="reset_password_with_token">Jeton ile parolayı sıfırla</string>
<string name="required_password_complexity">Gerekli Parola Karmaşıklığı</string>
<string name="disable_keyguard_features">Kilit Ekranı Özelliklerini Devre Dışı Bırak</string>
<string name="enable_all">Hepsini Etkinleştir</string>

View File

@@ -372,6 +372,7 @@
<!--UserRestriction-->
<string name="user_restriction">用户限制</string>
<string name="user_restriction_tip">长按一个条目以创建快捷方式</string>
<string name="profile_owner_is_restricted">Profile owner无法使用部分功能</string>
<string name="switch_to_disable_feature">打开开关后会禁用对应的功能</string>
<string name="some_features_invalid_in_work_profile">工作资料中部分功能无效</string>
@@ -489,7 +490,6 @@
<!--Password&Keyguard-->
<string name="password_and_keyguard">密码与锁屏</string>
<string name="password_info">密码信息</string>
<string name="reset_pwd_desc">留空以清除密码</string>
<string name="max_pwd_fail">最大密码错误次数</string>
<string name="max_pwd_fail_textfield">错误次数</string>
<string name="pwd_expiration_timeout">密码失效超时</string>
@@ -504,18 +504,15 @@
<string name="unified_password">一致的密码</string>
<string name="reset_password_token">密码重置令牌</string>
<string name="token">令牌</string>
<string name="token_must_longer_than_32_byte">令牌必须大于32字节</string>
<string name="activate_reset_password_token">激活密码重置令牌</string>
<string name="token_activated">令牌已激活</string>
<string name="clear">清除</string>
<string name="set">设置</string>
<string name="please_set_a_token">请先设置令牌</string>
<string name="activate_token_not_required_when_no_password">没有密码时会自动激活令牌</string>
<string name="reset_password">重置密码</string>
<string name="confirm_password">确认密码</string>
<string name="do_not_ask_credentials_on_boot">启动(boot)时不要求密码</string>
<string name="reset_password_require_entry">不允许其他设备管理员重置密码直至用户输入一次密码</string>
<string name="reset_password_with_token">使用令牌重置密码</string>
<string name="required_password_complexity">密码复杂度要求</string>
<string name="disable_keyguard_features">锁屏功能</string>
<string name="enable_all">启用全部</string>

View File

@@ -406,6 +406,7 @@
<!--UserRestriction-->
<string name="user_restriction">User restriction</string>
<string name="user_restriction_tip">Long press an item to create a shortcut</string>
<string name="profile_owner_is_restricted">Profile owner can use limited function</string>
<string name="switch_to_disable_feature">Turn on a switch to disable that function. </string>
<string name="some_features_invalid_in_work_profile">Functions in work profile is limited. </string>
@@ -525,7 +526,6 @@
<!--Password&Keyguard-->
<string name="password_and_keyguard">Password and keyguard</string>
<string name="password_info">Password Info</string>
<string name="reset_pwd_desc">Keep empty to remove password</string>
<string name="max_pwd_fail">Max failed passwords</string>
<string name="max_pwd_fail_textfield">Maximum failed attempts</string>
<string name="pwd_expiration_timeout">Password expiration timeout</string>
@@ -540,18 +540,15 @@
<string name="unified_password">Unified password</string>
<string name="reset_password_token">Reset password token</string>
<string name="token">Token</string>
<string name="token_must_longer_than_32_byte">The token must be longer than 32 byte</string>
<string name="activate_reset_password_token">Activate reset password token</string>
<string name="token_activated">Token activated</string>
<string name="clear">Clear</string>
<string name="set">Set</string>
<string name="please_set_a_token">Please set a token</string>
<string name="activate_token_not_required_when_no_password">Token will be automatically activated if no password is set. </string>
<string name="reset_password">Reset password</string>
<string name="confirm_password">Confirm password</string>
<string name="do_not_ask_credentials_on_boot">Do not ask credentials on boot</string>
<string name="reset_password_require_entry">Require entry</string>
<string name="reset_password_with_token">Reset password with token</string>
<string name="required_password_complexity">Required password complexity</string>
<string name="disable_keyguard_features">Keyguard features</string>
<string name="enable_all">Enable all</string>