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() { class ApiReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val requestKey = intent.getStringExtra("key") val requestKey = intent.getStringExtra("key")
var log = "OwnDroid API request received. action: ${intent.action}\nkey: $requestKey" var log = "OwnDroid API request received. action: ${intent.action}"
if(!SP.isApiEnabled) return val key = SP.apiKeyHash
val key = SP.apiKey if(!key.isNullOrEmpty() && key == requestKey?.hash()) {
if(!key.isNullOrEmpty() && key == requestKey) {
val app = intent.getStringExtra("package") val app = intent.getStringExtra("package")
val permission = intent.getStringExtra("permission") val permission = intent.getStringExtra("permission")
val restriction = intent.getStringExtra("restriction") val restriction = intent.getStringExtra("restriction")

View File

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

View File

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

View File

@@ -122,6 +122,7 @@ import kotlin.reflect.jvm.jvmErasure
class MyViewModel(application: Application): AndroidViewModel(application) { class MyViewModel(application: Application): AndroidViewModel(application) {
val myRepo = getApplication<MyApplication>().myRepo val myRepo = getApplication<MyApplication>().myRepo
val PM = application.packageManager val PM = application.packageManager
val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)) val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme))
fun changeTheme(newTheme: ThemeSettings) { fun changeTheme(newTheme: ThemeSettings) {
theme.value = newTheme theme.value = newTheme
@@ -129,6 +130,54 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
SP.darkTheme = newTheme.darkTheme SP.darkTheme = newTheme.darkTheme
SP.blackTheme = newTheme.blackTheme 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) val chosenPackage = Channel<String>(1, BufferOverflow.DROP_LATEST)
@@ -1083,11 +1132,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.clearUserRestriction(DAR, name) DPM.clearUserRestriction(DAR, name)
} }
userRestrictions.update { it.plus(name to state) } userRestrictions.update { it.plus(name to state) }
ShortcutUtils.updateUserRestrictionShortcut(application, name, !state, true)
true true
} catch (_: SecurityException) { } catch (_: SecurityException) {
false false
} }
} }
fun createUserRestrictionShortcut(id: String): Boolean {
return ShortcutUtils.setUserRestrictionShortcut(
application, id, userRestrictions.value[id] ?: true
)
}
fun createWorkProfile(options: CreateWorkProfileOptions): Intent { fun createWorkProfile(options: CreateWorkProfileOptions): Intent {
val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
if (VERSION.SDK_INT >= 23) { if (VERSION.SDK_INT >= 23) {
@@ -1478,7 +1533,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getPrivateDns(): PrivateDnsConfiguration { fun getPrivateDns(): PrivateDnsConfiguration {
val mode = DPM.getGlobalPrivateDnsMode(DAR) val mode = DPM.getGlobalPrivateDnsMode(DAR)
return PrivateDnsConfiguration( return PrivateDnsConfiguration(
PrivateDnsMode.entries.find { it.id == mode }!!, DPM.getGlobalPrivateDnsHost(DAR) ?: "" PrivateDnsMode.entries.find { it.id == mode }, DPM.getGlobalPrivateDnsHost(DAR) ?: ""
) )
} }
@Suppress("PrivateApi") @Suppress("PrivateApi")
@@ -1489,7 +1544,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
field.isAccessible = true field.isAccessible = true
val dpm = field.get(DPM) as IDevicePolicyManager val dpm = field.get(DPM) as IDevicePolicyManager
val host = if (conf.mode == PrivateDnsMode.Host) conf.host else null 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 result == DevicePolicyManager.PRIVATE_DNS_SET_NO_ERROR
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -1669,7 +1724,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getRpTokenState(): RpTokenState { fun getRpTokenState(): RpTokenState {
return try { return try {
RpTokenState(true, DPM.isResetPasswordTokenActive(DAR)) RpTokenState(true, DPM.isResetPasswordTokenActive(DAR))
} catch (_: IllegalArgumentException) { } catch (_: IllegalStateException) {
RpTokenState(false, false) RpTokenState(false, false)
} }
} }

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat
object NotificationUtils { object NotificationUtils {
fun checkPermission(context: Context): Boolean { fun checkPermission(context: Context): Boolean {
@@ -14,36 +15,57 @@ object NotificationUtils {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
else false else false
} }
fun registerChannels(context: Context) { fun createChannels(context: Context) {
if(Build.VERSION.SDK_INT < 26) return if (Build.VERSION.SDK_INT < 26) return
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 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 lockTaskMode = NotificationChannel(
val events = NotificationChannel(Channel.EVENTS, context.getString(R.string.events), NotificationManager.IMPORTANCE_HIGH) 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)) nm.createNotificationChannels(listOf(lockTaskMode, events))
} }
fun notify(context: Context, id: Int, notification: Notification) { fun notifyEvent(context: Context, type: NotificationType, text: String) {
val sp = context.getSharedPreferences("data", Context.MODE_PRIVATE) val notification = NotificationCompat.Builder(context, MyNotificationChannel.Events.id)
if(sp.getBoolean("n_$id", true) && checkPermission(context)) { .setSmallIcon(type.icon)
registerChannels(context) .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 val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(id, notification) nm.notify(type.id, notification)
} }
} }
object Channel { fun cancel(context: Context, type: NotificationType) {
const val LOCK_TASK_MODE = "LockTaskMode" val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
const val EVENTS = "Events" nm.cancel(type.id)
}
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
} }
} }
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 package com.bintianqi.owndroid
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.admin.DeviceAdminReceiver import android.app.admin.DeviceAdminReceiver
import android.content.ComponentName import android.content.ComponentName
@@ -17,9 +16,6 @@ import com.bintianqi.owndroid.dpm.processSecurityLogs
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class Receiver : DeviceAdminReceiver() { class Receiver : DeviceAdminReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -75,68 +71,59 @@ class Receiver : DeviceAdminReceiver() {
override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) { override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
super.onLockTaskModeEntering(context, intent, pkg) super.onLockTaskModeEntering(context, intent, pkg)
if(!NotificationUtils.checkPermission(context)) return if (!NotificationUtils.checkPermission(context)) return
NotificationUtils.registerChannels(context)
val intent = Intent(context, this::class.java).setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") 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 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)) .setContentTitle(context.getText(R.string.lock_task_mode))
.setSmallIcon(R.drawable.lock_fill0) .setSmallIcon(R.drawable.lock_fill0)
.addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build()) .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) { override fun onLockTaskModeExiting(context: Context, intent: Intent) {
super.onLockTaskModeExiting(context, intent) super.onLockTaskModeExiting(context, intent)
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager NotificationUtils.cancel(context, NotificationType.LockTaskMode)
nm.cancel(NotificationUtils.ID.LOCK_TASK_MODE)
} }
override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) { override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) {
super.onPasswordChanged(context, intent, 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) { override fun onUserAdded(context: Context, intent: Intent, addedUser: UserHandle) {
super.onUserAdded(context, intent, addedUser) 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) { override fun onUserStarted(context: Context, intent: Intent, startedUser: UserHandle) {
super.onUserStarted(context, intent, startedUser) 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) { override fun onUserSwitched(context: Context, intent: Intent, switchedUser: UserHandle) {
super.onUserSwitched(context, intent, switchedUser) 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) { override fun onUserStopped(context: Context, intent: Intent, stoppedUser: UserHandle) {
super.onUserStopped(context, intent, stoppedUser) 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) { override fun onUserRemoved(context: Context, intent: Intent, removedUser: UserHandle) {
super.onUserRemoved(context, intent, removedUser) 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) { override fun onBugreportShared(context: Context, intent: Intent, hash: String) {
super.onBugreportShared(context, intent, hash) super.onBugreportShared(context, intent, hash)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) NotificationUtils.notifyEvent(context, NotificationType.BugReportShared, "SHA-256 hash: $hash")
.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())
} }
override fun onBugreportSharingDeclined(context: Context, intent: Intent) { override fun onBugreportSharingDeclined(context: Context, intent: Intent) {
super.onBugreportSharingDeclined(context, intent) super.onBugreportSharingDeclined(context, intent)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) NotificationUtils.notifyEvent(context, NotificationType.BugReportSharingDeclined, "")
.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())
} }
override fun onBugreportFailed(context: Context, intent: Intent, failureCode: Int) { 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 BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE -> R.string.bug_report_failure_no_longer_available
else -> R.string.place_holder else -> R.string.place_holder
} }
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) NotificationUtils.notifyEvent(context, NotificationType.BugReportFailed, context.getString(message))
.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())
} }
override fun onSystemUpdatePending(context: Context, intent: Intent, receivedTime: Long) { override fun onSystemUpdatePending(context: Context, intent: Intent, receivedTime: Long) {
super.onSystemUpdatePending(context, intent, receivedTime) super.onSystemUpdatePending(context, intent, receivedTime)
val time = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(receivedTime)) val text = context.getString(R.string.received_time) + ": " + formatDate(receivedTime)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) NotificationUtils.notifyEvent(context, NotificationType.SystemUpdatePending, text)
.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())
} }
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 um = context.getSystemService(Context.USER_SERVICE) as UserManager
val serial = um.getSerialNumberForUser(userHandle) val serial = um.getSerialNumberForUser(userHandle)
val builder = NotificationCompat.Builder(context, NotificationUtils.Channel.EVENTS) val text = context.getString(R.string.serial_number) + ": $serial"
.setContentTitle(context.getString(title)) NotificationUtils.notifyEvent(context, type, text)
.setContentText(context.getString(R.string.serial_number) + ": $serial")
.setSmallIcon(icon)
NotificationUtils.notify(context, id, builder.build())
} }
} }

View File

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

View File

@@ -13,8 +13,7 @@ class SharedPrefs(context: Context) {
var dhizuku by BooleanSharedPref("dhizuku_mode") var dhizuku by BooleanSharedPref("dhizuku_mode")
var isDefaultAffiliationIdSet by BooleanSharedPref("default_affiliation_id_set") var isDefaultAffiliationIdSet by BooleanSharedPref("default_affiliation_id_set")
var displayDangerousFeatures by BooleanSharedPref("display_dangerous_features") var displayDangerousFeatures by BooleanSharedPref("display_dangerous_features")
var isApiEnabled by BooleanSharedPref("api.enabled") var apiKeyHash by StringSharedPref("api_key_hash")
var apiKey by StringSharedPref("api.key")
var materialYou by BooleanSharedPref("theme.material_you", Build.VERSION.SDK_INT >= 31) var materialYou by BooleanSharedPref("theme.material_you", Build.VERSION.SDK_INT >= 31)
/** -1: follow system, 0: off, 1: on */ /** -1: follow system, 0: off, 1: on */
var darkTheme by IntSharedPref("theme.dark", -1) var darkTheme by IntSharedPref("theme.dark", -1)
@@ -25,6 +24,8 @@ class SharedPrefs(context: Context) {
var applicationsListView by BooleanSharedPref("applications.list_view", true) var applicationsListView by BooleanSharedPref("applications.list_view", true)
var shortcuts by BooleanSharedPref("shortcuts") var shortcuts by BooleanSharedPref("shortcuts")
var dhizukuServer by BooleanSharedPref("dhizuku_server") 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> { 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.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import java.security.SecureRandom
import kotlin.io.encoding.Base64
object ShortcutUtils { object ShortcutUtils {
fun setAllShortcuts(context: Context) { fun setAllShortcuts(context: Context, enabled: Boolean) {
if (SP.shortcuts) { if (enabled) {
setShortcutKey()
val list = listOf( val list = listOf(
createShortcut(context, MyShortcut.Lock, true), createShortcut(context, MyShortcut.Lock, true),
createShortcut(context, MyShortcut.DisableCamera, createShortcut(context, MyShortcut.DisableCamera,
@@ -22,6 +25,7 @@ object ShortcutUtils {
} }
} }
fun setShortcut(context: Context, shortcut: MyShortcut, state: Boolean) { fun setShortcut(context: Context, shortcut: MyShortcut, state: Boolean) {
setShortcutKey()
ShortcutManagerCompat.pushDynamicShortcut( ShortcutManagerCompat.pushDynamicShortcut(
context, createShortcut(context, shortcut, state) context, createShortcut(context, shortcut, state)
) )
@@ -41,9 +45,47 @@ object ShortcutUtils {
.setIntent( .setIntent(
Intent(context, ShortcutsReceiverActivity::class.java) Intent(context, ShortcutsReceiverActivity::class.java)
.setAction("com.bintianqi.owndroid.action.${shortcut.id}") .setAction("com.bintianqi.owndroid.action.${shortcut.id}")
.putExtra("key", SP.shortcutKey)
) )
.build() .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( enum class MyShortcut(

View File

@@ -9,7 +9,9 @@ class ShortcutsReceiverActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
try { try {
val action = intent.action?.removePrefix("com.bintianqi.owndroid.action.") 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) { when (action) {
"LOCK" -> Privilege.DPM.lockNow() "LOCK" -> Privilege.DPM.lockNow()
"DISABLE_CAMERA" -> { "DISABLE_CAMERA" -> {
@@ -22,9 +24,22 @@ class ShortcutsReceiverActivity : Activity() {
Privilege.DPM.setMasterVolumeMuted(Privilege.DAR, !state) Privilege.DPM.setMasterVolumeMuted(Privilege.DAR, !state)
ShortcutUtils.setShortcut(this, MyShortcut.Mute, 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") Log.d(TAG, "Received intent: $action")
showOperationResultToast(true) showOperationResultToast(true)
} else {
showOperationResultToast(false)
} }
} finally { } finally {
finish() finish()

View File

@@ -98,6 +98,13 @@ object UserRestrictionsRepository {
UserRestrictionCategory.Other -> other UserRestrictionCategory.Other -> other
}.filter { Build.VERSION.SDK_INT >= it.requiresApi } }.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) { 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.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.io.encoding.Base64
var zhCN = true var zhCN = true
@@ -64,8 +66,8 @@ fun formatFileSize(bytes: Long): String {
val Boolean.yesOrNo val Boolean.yesOrNo
@StringRes get() = if(this) R.string.yes else R.string.no @StringRes get() = if(this) R.string.yes else R.string.no
fun formatTime(ms: Long): String { fun formatDate(ms: Long): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(ms)) return formatDate(Date(ms))
} }
fun formatDate(date: Date): String { fun formatDate(date: Date): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(date) 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) 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.dhizukuServer = false
SP.shortcuts = privilege.activated SP.shortcuts = privilege.activated
if (privilege.activated) { if (privilege.activated) {
ShortcutUtils.setAllShortcuts(context) ShortcutUtils.setAllShortcuts(context, true)
if (!privilege.dhizuku) { if (!privilege.dhizuku) {
setDefaultAffiliationID() setDefaultAffiliationID()
} }
} else { } else {
SP.isDefaultAffiliationIdSet = false SP.isDefaultAffiliationIdSet = false
ShortcutUtils.setAllShortcuts(context) ShortcutUtils.setAllShortcuts(context, false)
SP.isApiEnabled = false SP.apiKeyHash = ""
} }
} }

View File

@@ -70,7 +70,6 @@ import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -115,7 +114,7 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.formatTime import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.ErrorDialog import com.bintianqi.owndroid.ui.ErrorDialog
@@ -511,7 +510,7 @@ fun UpdateNetworkScreen(info: WifiInfo, setNetwork: (WifiInfo) -> Boolean, onNav
TopAppBar( TopAppBar(
{ Text(stringResource(R.string.update_network)) }, { Text(stringResource(R.string.update_network)) },
navigationIcon = { NavIcon(onNavigateUp) }, navigationIcon = { NavIcon(onNavigateUp) },
colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainer)
) )
}, },
contentWindowInsets = WindowInsets.ime contentWindowInsets = WindowInsets.ime
@@ -1046,14 +1045,14 @@ fun NetworkStatsScreen(
} }
} }
OutlinedTextField( 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)) }, label = { Text(stringResource(R.string.start_time)) },
interactionSource = startTimeIs, interactionSource = startTimeIs,
isError = startTime >= endTime, isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
) )
OutlinedTextField( OutlinedTextField(
value = formatTime(endTime), onValueChange = {}, readOnly = true, value = formatDate(endTime), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.end_time)) }, label = { Text(stringResource(R.string.end_time)) },
interactionSource = endTimeIs, interactionSource = endTimeIs,
isError = startTime >= endTime, isError = startTime >= endTime,
@@ -1292,7 +1291,7 @@ fun NetworkStatsViewerScreen(
HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page -> HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page ->
val item = data[index] val item = data[index]
Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) { 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) Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center)
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
val txBytes = item.txBytes 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) 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 @Serializable object PrivateDns
@@ -1362,7 +1361,7 @@ fun PrivateDnsScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusMgr = LocalFocusManager.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("") } var inputHost by remember { mutableStateOf("") }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val conf = getPrivateDns() val conf = getPrivateDns()
@@ -1385,7 +1384,8 @@ fun PrivateDnsScreen(
val result = setPrivateDns(PrivateDnsConfiguration(mode, inputHost)) val result = setPrivateDns(PrivateDnsConfiguration(mode, inputHost))
context.showOperationResultToast(result) context.showOperationResultToast(result)
}, },
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding) modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
enabled = mode != null
) { ) {
Text(stringResource(R.string.apply)) 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults 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.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -45,6 +47,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType 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.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.generateBase64Key
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -250,7 +254,12 @@ fun ResetPasswordTokenScreen(
OutlinedTextField( OutlinedTextField(
token, { token = it }, Modifier.fillMaxWidth(), token, { token = it }, Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.token)) }, 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( Button(
onClick = { onClick = {
@@ -265,7 +274,12 @@ fun ResetPasswordTokenScreen(
} }
if (state.set && !state.active) Button( if (state.set && !state.active) Button(
onClick = { onClick = {
getIntent()?.let { launcher.launch(it) } val intent = getIntent()
if (intent == null) {
context.showOperationResultToast(false)
} else {
launcher.launch(intent)
}
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {

View File

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

View File

@@ -1,6 +1,8 @@
package com.bintianqi.owndroid.dpm package com.bintianqi.owndroid.dpm
import androidx.annotation.RequiresApi 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.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.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll 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.Add
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -54,6 +58,7 @@ import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.UserRestrictionCategory import com.bintianqi.owndroid.UserRestrictionCategory
import com.bintianqi.owndroid.UserRestrictionsRepository import com.bintianqi.owndroid.UserRestrictionsRepository
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.MyLazyScaffold import com.bintianqi.owndroid.ui.MyLazyScaffold
@@ -117,6 +122,17 @@ fun UserRestrictionScreen(
onNavigate(UserRestrictionOptions(it.name)) 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 @Composable
fun UserRestrictionOptionsScreen( fun UserRestrictionOptionsScreen(
args: UserRestrictionOptions, userRestrictions: StateFlow<Map<String, Boolean>>, 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 context = LocalContext.current
val status by userRestrictions.collectAsStateWithLifecycle() val status by userRestrictions.collectAsStateWithLifecycle()
@@ -136,7 +153,12 @@ fun UserRestrictionOptionsScreen(
MyLazyScaffold(title, onNavigateUp) { MyLazyScaffold(title, onNavigateUp) {
items(items) { restriction -> items(items) { restriction ->
Row( 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 Arrangement.SpaceBetween, Alignment.CenterVertically
) { ) {
Row(Modifier.weight(1F), verticalAlignment = 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.MyViewModel
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatTime import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog 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 >= 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 >= 34) InfoItem(R.string.admin_user, info.admin.yesOrNo)
if (VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, info.demo.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) { if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.logout_enabled, info.logout.yesOrNo) InfoItem(R.string.logout_enabled, info.logout.yesOrNo)

View File

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

View File

@@ -501,7 +501,6 @@
<!--Password&Keyguard--> <!--Password&Keyguard-->
<string name="password_and_keyguard">Parola ve Kilit Ekranı</string> <string name="password_and_keyguard">Parola ve Kilit Ekranı</string>
<string name="password_info">Parola Bilgisi</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">Maksimum Başarısız Parola</string>
<string name="max_pwd_fail_textfield">Maksimum başarısız deneme sayısı</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> <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="unified_password">Birleşik Parola</string>
<string name="reset_password_token">Parola Sıfırlama Jetonu</string> <string name="reset_password_token">Parola Sıfırlama Jetonu</string>
<string name="token">Jeton</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="token_activated">Token activated</string><!--TODO-->
<string name="clear">Temizle</string> <string name="clear">Temizle</string>
<string name="set">Ayarla</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="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="reset_password">Parolayı Sıfırla</string>
<string name="confirm_password">Parolayı Onayla</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="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_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="required_password_complexity">Gerekli Parola Karmaşıklığı</string>
<string name="disable_keyguard_features">Kilit Ekranı Özelliklerini Devre Dışı Bırak</string> <string name="disable_keyguard_features">Kilit Ekranı Özelliklerini Devre Dışı Bırak</string>
<string name="enable_all">Hepsini Etkinleştir</string> <string name="enable_all">Hepsini Etkinleştir</string>

View File

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

View File

@@ -406,6 +406,7 @@
<!--UserRestriction--> <!--UserRestriction-->
<string name="user_restriction">User restriction</string> <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="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="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> <string name="some_features_invalid_in_work_profile">Functions in work profile is limited. </string>
@@ -525,7 +526,6 @@
<!--Password&Keyguard--> <!--Password&Keyguard-->
<string name="password_and_keyguard">Password and keyguard</string> <string name="password_and_keyguard">Password and keyguard</string>
<string name="password_info">Password Info</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">Max failed passwords</string>
<string name="max_pwd_fail_textfield">Maximum failed attempts</string> <string name="max_pwd_fail_textfield">Maximum failed attempts</string>
<string name="pwd_expiration_timeout">Password expiration timeout</string> <string name="pwd_expiration_timeout">Password expiration timeout</string>
@@ -540,18 +540,15 @@
<string name="unified_password">Unified password</string> <string name="unified_password">Unified password</string>
<string name="reset_password_token">Reset password token</string> <string name="reset_password_token">Reset password token</string>
<string name="token">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="activate_reset_password_token">Activate reset password token</string>
<string name="token_activated">Token activated</string> <string name="token_activated">Token activated</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="set">Set</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="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="reset_password">Reset password</string>
<string name="confirm_password">Confirm 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="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_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="required_password_complexity">Required password complexity</string>
<string name="disable_keyguard_features">Keyguard features</string> <string name="disable_keyguard_features">Keyguard features</string>
<string name="enable_all">Enable all</string> <string name="enable_all">Enable all</string>