manager: Fixed an issue where ksud failed to release properly during the first installation.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.sukisu.ultra.ui
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -8,27 +9,40 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
|
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
import com.sukisu.ultra.ui.activity.component.BottomBar
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ui.activity.util.*
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
import com.sukisu.ultra.ui.component.InstallConfirmationDialog
|
|
||||||
import com.sukisu.ultra.ui.theme.KernelSUTheme
|
import com.sukisu.ultra.ui.theme.KernelSUTheme
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.install
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.sukisu.ultra.ui.webui.initPlatform
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.ui.activity.component.BottomBar
|
||||||
|
import com.sukisu.ultra.ui.activity.util.*
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var superUserViewModel: SuperUserViewModel
|
||||||
|
private lateinit var homeViewModel: HomeViewModel
|
||||||
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||||
|
|
||||||
data class SettingsState(
|
data class SettingsState(
|
||||||
@@ -36,6 +50,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
val showKpmInfo: Boolean = false
|
val showKpmInfo: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private var showConfirmationDialog = mutableStateOf(false)
|
||||||
|
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
|
||||||
|
|
||||||
|
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||||
|
|
||||||
// 标记避免重复初始化
|
// 标记避免重复初始化
|
||||||
private var isInitialized = false
|
private var isInitialized = false
|
||||||
|
|
||||||
@@ -46,8 +65,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
try {
|
try {
|
||||||
// 应用主题配置
|
// 确保应用正确的语言设置
|
||||||
ThemeUtils.applyFullThemeConfiguration(this)
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
|
|
||||||
|
// 应用自定义 DPI
|
||||||
|
DisplayUtils.applyCustomDpi(this)
|
||||||
|
|
||||||
// Enable edge to edge
|
// Enable edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -60,16 +82,155 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
// 使用标记控制初始化流程
|
// 使用标记控制初始化流程
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
lifecycleScope.launch {
|
initializeViewModels()
|
||||||
ActivityInitializer.initialize(this@MainActivity, settingsStateFlow)
|
initializeData()
|
||||||
}
|
|
||||||
ThemeUtils.registerThemeChangeObserver(this)
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if launched with a ZIP file
|
||||||
|
val zipUri: ArrayList<Uri>? = when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||||
|
}
|
||||||
|
uri?.let { arrayListOf(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> when {
|
||||||
|
intent?.data != null -> arrayListOf(intent.data!!)
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
|
||||||
|
intent.getParcelableArrayListExtra("uris", Uri::class.java)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableArrayListExtra("uris")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
KernelSUTheme {
|
KernelSUTheme {
|
||||||
MainScreenContent()
|
val navController = rememberNavController()
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||||
|
|
||||||
|
val bottomBarRoutes = remember {
|
||||||
|
BottomBarDestination.entries.map { it.direction.route }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
|
|
||||||
|
InstallConfirmationDialog(
|
||||||
|
show = showConfirmationDialog.value,
|
||||||
|
zipFiles = pendingZipFiles.value,
|
||||||
|
onConfirm = { confirmedFiles ->
|
||||||
|
showConfirmationDialog.value = false
|
||||||
|
UltraActivityUtils.navigateToFlashScreen(this, confirmedFiles, navigator)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showConfirmationDialog.value = false
|
||||||
|
pendingZipFiles.value = emptyList()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(zipUri) {
|
||||||
|
if (!zipUri.isNullOrEmpty()) {
|
||||||
|
// 检测 ZIP 文件类型并显示确认对话框
|
||||||
|
lifecycleScope.launch {
|
||||||
|
UltraActivityUtils.detectZipTypeAndShowConfirmation(this@MainActivity, zipUri) { infos ->
|
||||||
|
if (infos.isNotEmpty()) {
|
||||||
|
pendingZipFiles.value = infos
|
||||||
|
showConfirmationDialog.value = true
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showBottomBar = when (currentDestination?.route) {
|
||||||
|
ExecuteModuleActionScreenDestination.route -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
initPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalSnackbarHost provides snackBarHostState
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||||
|
showBottomBar = showBottomBar,
|
||||||
|
content = { BottomBar(navController) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
|
) { innerPadding ->
|
||||||
|
DestinationsNavHost(
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||||
|
navController = navController,
|
||||||
|
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||||
|
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||||
|
// If the target is a detail page (not a bottom navigation page), slide in from the right
|
||||||
|
if (targetState.destination.route !in bottomBarRoutes) {
|
||||||
|
slideInHorizontally(initialOffsetX = { it })
|
||||||
|
} else {
|
||||||
|
// Otherwise (switching between bottom navigation pages), use fade in
|
||||||
|
fadeIn(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||||
|
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
|
||||||
|
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
|
||||||
|
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
|
||||||
|
} else {
|
||||||
|
// Otherwise (switching between bottom navigation pages), use fade out
|
||||||
|
fadeOut(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||||
|
// If returning to the home page (bottom navigation page), slide in from the left
|
||||||
|
if (targetState.destination.route in bottomBarRoutes) {
|
||||||
|
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
|
||||||
|
} else {
|
||||||
|
// Otherwise (e.g., returning between multiple detail pages), use default fade in
|
||||||
|
fadeIn(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||||
|
// If returning from a detail page (not a bottom navigation page), scale down and fade out
|
||||||
|
if (initialState.destination.route !in bottomBarRoutes) {
|
||||||
|
scaleOut(targetScale = 0.9f) + fadeOut()
|
||||||
|
} else {
|
||||||
|
// Otherwise, use default fade out
|
||||||
|
fadeOut(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -77,66 +238,33 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun initializeViewModels() {
|
||||||
private fun MainScreenContent() {
|
superUserViewModel = SuperUserViewModel()
|
||||||
val navController = rememberNavController()
|
homeViewModel = HomeViewModel()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
|
||||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
|
||||||
|
|
||||||
// 处理ZIP文件
|
// 设置主题变化监听器
|
||||||
var zipUri by remember { mutableStateOf<ArrayList<Uri>?>(null) }
|
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
// 在 LaunchedEffect 中处理 ZIP 文件
|
private fun initializeData() {
|
||||||
LaunchedEffect(Unit) {
|
lifecycleScope.launch {
|
||||||
zipUri = ZipFileManager.handleZipFiles(intent)
|
try {
|
||||||
}
|
superUserViewModel.fetchAppList()
|
||||||
|
} catch (e: Exception) {
|
||||||
InstallConfirmationDialog(
|
e.printStackTrace()
|
||||||
show = ZipFileManager.showConfirmationDialog.value,
|
|
||||||
zipFiles = ZipFileManager.pendingZipFiles.value,
|
|
||||||
onConfirm = { confirmedFiles ->
|
|
||||||
ZipFileManager.navigateToFlashScreen(
|
|
||||||
this@MainActivity,
|
|
||||||
confirmedFiles,
|
|
||||||
navigator,
|
|
||||||
lifecycleScope
|
|
||||||
)
|
|
||||||
ZipFileManager.clearZipFileState()
|
|
||||||
},
|
|
||||||
onDismiss = {
|
|
||||||
ZipFileManager.clearZipFileState()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(zipUri) {
|
|
||||||
zipUri?.let { uris ->
|
|
||||||
ZipFileManager.detectZipTypeAndShowConfirmation(this@MainActivity, uris)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBottomBar = NavigationUtils.shouldShowBottomBar(currentDestination?.route)
|
// 数据刷新协程
|
||||||
|
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
|
||||||
|
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
|
||||||
|
|
||||||
CompositionLocalProvider(
|
// 初始化主题相关设置
|
||||||
LocalSnackbarHost provides snackBarHostState
|
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
|
||||||
) {
|
|
||||||
Scaffold(
|
val isManager = Natives.becomeManager(packageName)
|
||||||
bottomBar = {
|
if (isManager) {
|
||||||
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
install()
|
||||||
showBottomBar = showBottomBar,
|
|
||||||
content = { BottomBar(navController) }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
|
||||||
) { innerPadding ->
|
|
||||||
DestinationsNavHost(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
|
||||||
navController = navController,
|
|
||||||
defaultTransitions = NavigationUtils.createNavHostAnimations()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +285,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun refreshData() {
|
private fun refreshData() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ViewModelManager.refreshViewModelData()
|
try {
|
||||||
DataRefreshUtils.refreshData(lifecycleScope)
|
superUserViewModel.fetchAppList()
|
||||||
|
DataRefreshUtils.refreshData(lifecycleScope)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +305,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
try {
|
try {
|
||||||
ThemeUtils.unregisterThemeChangeObserver(this)
|
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.sukisu.ultra.ui.activity.util.AppData.getKpmVersionUse
|
|||||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
|
||||||
@SuppressLint("ContextCastToActivity")
|
@SuppressLint("ContextCastToActivity")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.sukisu.ultra.ui.activity.util
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
|
||||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
|
||||||
|
|
||||||
object NavigationUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取底部导航栏路由集合
|
|
||||||
*/
|
|
||||||
fun getBottomBarRoutes(): Set<String> {
|
|
||||||
return BottomBarDestination.entries.map { it.direction.route }.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否应该显示底部导航栏
|
|
||||||
*/
|
|
||||||
fun shouldShowBottomBar(currentRoute: String?): Boolean {
|
|
||||||
return when (currentRoute) {
|
|
||||||
ExecuteModuleActionScreenDestination.route -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建导航动画样式
|
|
||||||
*/
|
|
||||||
fun createNavHostAnimations(): NavHostAnimatedDestinationStyle {
|
|
||||||
val bottomBarRoutes = getBottomBarRoutes()
|
|
||||||
|
|
||||||
return object : NavHostAnimatedDestinationStyle() {
|
|
||||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
|
||||||
// If the target is a detail page (not a bottom navigation page), slide in from the right
|
|
||||||
if (targetState.destination.route !in bottomBarRoutes) {
|
|
||||||
slideInHorizontally(initialOffsetX = { it })
|
|
||||||
} else {
|
|
||||||
// Otherwise (switching between bottom navigation pages), use fade in
|
|
||||||
fadeIn(animationSpec = tween(340))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
|
||||||
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
|
|
||||||
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
|
|
||||||
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
|
|
||||||
} else {
|
|
||||||
// Otherwise (switching between bottom navigation pages), use fade out
|
|
||||||
fadeOut(animationSpec = tween(340))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
|
||||||
// If returning to the home page (bottom navigation page), slide in from the left
|
|
||||||
if (targetState.destination.route in bottomBarRoutes) {
|
|
||||||
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
|
|
||||||
} else {
|
|
||||||
// Otherwise (e.g., returning between multiple detail pages), use default fade in
|
|
||||||
fadeIn(animationSpec = tween(340))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
|
||||||
// If returning from a detail page (not a bottom navigation page), scale down and fade out
|
|
||||||
if (initialState.destination.route !in bottomBarRoutes) {
|
|
||||||
scaleOut(targetScale = 0.9f) + fadeOut()
|
|
||||||
} else {
|
|
||||||
// Otherwise, use default fade out
|
|
||||||
fadeOut(animationSpec = tween(340))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package com.sukisu.ultra.ui.activity.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.sukisu.ultra.ui.MainActivity
|
import com.sukisu.ultra.ui.MainActivity
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
@@ -19,16 +20,8 @@ class ThemeChangeContentObserver(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 主题管理工具类
|
|
||||||
*/
|
|
||||||
object ThemeUtils {
|
object ThemeUtils {
|
||||||
|
|
||||||
private var themeChangeObserver: ThemeChangeContentObserver? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化主题设置
|
|
||||||
*/
|
|
||||||
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
||||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||||
@@ -53,9 +46,6 @@ object ThemeUtils {
|
|||||||
CardConfig.load(activity.applicationContext)
|
CardConfig.load(activity.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册主题变化观察者
|
|
||||||
*/
|
|
||||||
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
||||||
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
@@ -67,28 +57,18 @@ object ThemeUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activity.contentResolver.registerContentObserver(
|
activity.contentResolver.registerContentObserver(
|
||||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
Settings.System.getUriFor("ui_night_mode"),
|
||||||
false,
|
false,
|
||||||
contentObserver
|
contentObserver
|
||||||
)
|
)
|
||||||
|
|
||||||
themeChangeObserver = contentObserver
|
|
||||||
return contentObserver
|
return contentObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) {
|
||||||
* 注销主题变化观察者
|
activity.contentResolver.unregisterContentObserver(observer)
|
||||||
*/
|
|
||||||
fun unregisterThemeChangeObserver(activity: MainActivity) {
|
|
||||||
themeChangeObserver?.let { observer ->
|
|
||||||
activity.contentResolver.unregisterContentObserver(observer)
|
|
||||||
}
|
|
||||||
themeChangeObserver = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity暂停时的主题处理
|
|
||||||
*/
|
|
||||||
fun onActivityPause(activity: MainActivity) {
|
fun onActivityPause(activity: MainActivity) {
|
||||||
CardConfig.save(activity.applicationContext)
|
CardConfig.save(activity.applicationContext)
|
||||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||||
@@ -97,39 +77,21 @@ object ThemeUtils {
|
|||||||
ThemeConfig.preventBackgroundRefresh = true
|
ThemeConfig.preventBackgroundRefresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity恢复时的主题处理
|
|
||||||
*/
|
|
||||||
fun onActivityResume() {
|
fun onActivityResume() {
|
||||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
loadCustomBackground()
|
loadCustomBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用完整的主题配置到Activity
|
|
||||||
*/
|
|
||||||
fun applyFullThemeConfiguration(activity: MainActivity) {
|
|
||||||
// 确保应用正确的语言设置
|
|
||||||
LocaleUtils.applyLanguageSetting(activity)
|
|
||||||
|
|
||||||
// 应用自定义 DPI
|
|
||||||
DisplayUtils.applyCustomDpi(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadThemeMode() {
|
private fun loadThemeMode() {
|
||||||
// 主题模式加载逻辑
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadThemeColors() {
|
private fun loadThemeColors() {
|
||||||
// 主题颜色加载逻辑
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDynamicColorState() {
|
private fun loadDynamicColorState() {
|
||||||
// 动态颜色状态加载逻辑
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCustomBackground() {
|
private fun loadCustomBackground() {
|
||||||
// 自定义背景加载逻辑
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,29 +2,18 @@ package com.sukisu.ultra.ui.activity.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ui.MainActivity
|
import com.sukisu.ultra.ui.MainActivity
|
||||||
import com.sukisu.ultra.ui.component.ZipFileDetector
|
|
||||||
import com.sukisu.ultra.ui.component.ZipFileInfo
|
|
||||||
import com.sukisu.ultra.ui.component.ZipType
|
|
||||||
import com.sukisu.ultra.ui.screen.FlashIt
|
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
|
||||||
import com.sukisu.ultra.ui.webui.initPlatform
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -32,8 +21,18 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.ui.component.ZipFileDetector
|
||||||
|
import com.sukisu.ultra.ui.component.ZipFileInfo
|
||||||
|
import com.sukisu.ultra.ui.component.ZipType
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
||||||
|
import com.sukisu.ultra.ui.screen.FlashIt
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
object AnimatedBottomBar {
|
object AnimatedBottomBar {
|
||||||
@Composable
|
@Composable
|
||||||
@@ -51,9 +50,58 @@ object AnimatedBottomBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
object UltraActivityUtils {
|
||||||
* 应用数据管理工具类
|
|
||||||
*/
|
suspend fun detectZipTypeAndShowConfirmation(
|
||||||
|
activity: MainActivity,
|
||||||
|
zipUris: ArrayList<Uri>,
|
||||||
|
onResult: (List<ZipFileInfo>) -> Unit
|
||||||
|
) {
|
||||||
|
val infos = ZipFileDetector.detectAndParseZipFiles(activity, zipUris)
|
||||||
|
withContext(Dispatchers.Main) { onResult(infos) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToFlashScreen(
|
||||||
|
activity: MainActivity,
|
||||||
|
zipFiles: List<ZipFileInfo>,
|
||||||
|
navigator: DestinationsNavigator
|
||||||
|
) {
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
|
||||||
|
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
|
||||||
|
|
||||||
|
when {
|
||||||
|
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
||||||
|
if (kernelUris.size == 1 && rootAvailable()) {
|
||||||
|
navigator.navigate(
|
||||||
|
InstallScreenDestination(
|
||||||
|
preselectedKernelUri = kernelUris.first().toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setAutoExitAfterFlash(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleUris.isNotEmpty() -> {
|
||||||
|
navigator.navigate(
|
||||||
|
FlashScreenDestination(
|
||||||
|
FlashIt.FlashModules(ArrayList(moduleUris))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setAutoExitAfterFlash(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAutoExitAfterFlash(activity: Context) {
|
||||||
|
activity.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putBoolean("auto_exit_after_flash", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object AppData {
|
object AppData {
|
||||||
object DataRefreshManager {
|
object DataRefreshManager {
|
||||||
// 私有状态流
|
// 私有状态流
|
||||||
@@ -136,160 +184,7 @@ object AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ZIP文件处理工具类
|
|
||||||
*/
|
|
||||||
object ZipFileManager {
|
|
||||||
val showConfirmationDialog = mutableStateOf(false)
|
|
||||||
val pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理传入的ZIP文件URI
|
|
||||||
*/
|
|
||||||
fun handleZipFiles(intent: Intent?): ArrayList<Uri>? {
|
|
||||||
return when (intent?.action) {
|
|
||||||
Intent.ACTION_SEND -> {
|
|
||||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
|
||||||
}
|
|
||||||
uri?.let { arrayListOf(it) }
|
|
||||||
}
|
|
||||||
Intent.ACTION_SEND_MULTIPLE -> {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> when {
|
|
||||||
intent?.data != null -> arrayListOf(intent.data!!)
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
|
|
||||||
intent?.getParcelableArrayListExtra("uris", Uri::class.java)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
(intent?.getParcelableArrayListExtra("uris"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检测ZIP文件类型并显示确认对话框
|
|
||||||
*/
|
|
||||||
suspend fun detectZipTypeAndShowConfirmation(context: Context, zipUris: ArrayList<Uri>) {
|
|
||||||
try {
|
|
||||||
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(context, zipUris)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (zipFileInfos.isNotEmpty()) {
|
|
||||||
pendingZipFiles.value = zipFileInfos
|
|
||||||
showConfirmationDialog.value = true
|
|
||||||
} else {
|
|
||||||
(context as MainActivity).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
(context as MainActivity).finish()
|
|
||||||
}
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导航到内核刷写界面
|
|
||||||
*/
|
|
||||||
fun navigateToFlashScreen(
|
|
||||||
context: Context,
|
|
||||||
zipFiles: List<ZipFileInfo>,
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
scope: LifecycleCoroutineScope
|
|
||||||
) {
|
|
||||||
scope.launch {
|
|
||||||
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
|
|
||||||
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
|
|
||||||
|
|
||||||
when {
|
|
||||||
// 内核文件
|
|
||||||
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
|
||||||
if (kernelUris.size == 1 && rootAvailable()) {
|
|
||||||
navigator.navigate(
|
|
||||||
InstallScreenDestination(
|
|
||||||
preselectedKernelUri = kernelUris.first().toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setAutoExitAfterFlash(context)
|
|
||||||
}
|
|
||||||
// 模块文件
|
|
||||||
moduleUris.isNotEmpty() -> {
|
|
||||||
navigator.navigate(
|
|
||||||
FlashScreenDestination(
|
|
||||||
FlashIt.FlashModules(ArrayList(moduleUris))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setAutoExitAfterFlash(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置内核刷写后自动退出
|
|
||||||
*/
|
|
||||||
private fun setAutoExitAfterFlash(context: Context) {
|
|
||||||
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
|
|
||||||
sharedPref.edit {
|
|
||||||
putBoolean("auto_exit_after_flash", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理ZIP文件状态
|
|
||||||
*/
|
|
||||||
fun clearZipFileState() {
|
|
||||||
showConfirmationDialog.value = false
|
|
||||||
pendingZipFiles.value = emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel管理工具类
|
|
||||||
*/
|
|
||||||
object ViewModelManager {
|
|
||||||
lateinit var superUserViewModel: SuperUserViewModel
|
|
||||||
lateinit var homeViewModel: HomeViewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化ViewModel
|
|
||||||
*/
|
|
||||||
fun initializeViewModels() {
|
|
||||||
superUserViewModel = SuperUserViewModel()
|
|
||||||
homeViewModel = HomeViewModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新ViewModel数据
|
|
||||||
*/
|
|
||||||
suspend fun refreshViewModelData() {
|
|
||||||
try {
|
|
||||||
superUserViewModel.fetchAppList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据刷新工具类
|
|
||||||
*/
|
|
||||||
object DataRefreshUtils {
|
object DataRefreshUtils {
|
||||||
|
|
||||||
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
|
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
@@ -323,48 +218,7 @@ object DataRefreshUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity初始化工具类
|
|
||||||
*/
|
|
||||||
object ActivityInitializer {
|
|
||||||
/**
|
|
||||||
* 初始化Activity的所有组件
|
|
||||||
*/
|
|
||||||
suspend fun initialize(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
|
||||||
// 初始化ViewModel
|
|
||||||
ViewModelManager.initializeViewModels()
|
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
initializeData(activity, settingsStateFlow)
|
|
||||||
|
|
||||||
// 初始化平台
|
|
||||||
initPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun initializeData(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
|
||||||
// 获取应用列表
|
|
||||||
ViewModelManager.refreshViewModelData()
|
|
||||||
|
|
||||||
// 启动数据刷新协程
|
|
||||||
DataRefreshUtils.startDataRefreshCoroutine(activity.lifecycleScope)
|
|
||||||
DataRefreshUtils.startSettingsMonitorCoroutine(activity.lifecycleScope, activity, settingsStateFlow)
|
|
||||||
|
|
||||||
// 初始化主题相关设置
|
|
||||||
ThemeUtils.initializeThemeSettings(activity, settingsStateFlow)
|
|
||||||
|
|
||||||
// 安装管理器
|
|
||||||
val isManager = Natives.becomeManager(activity.packageName)
|
|
||||||
if (isManager) {
|
|
||||||
install()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示设置工具类
|
|
||||||
*/
|
|
||||||
object DisplayUtils {
|
object DisplayUtils {
|
||||||
|
|
||||||
fun applyCustomDpi(context: Context) {
|
fun applyCustomDpi(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val customDpi = prefs.getInt("app_dpi", 0)
|
val customDpi = prefs.getInt("app_dpi", 0)
|
||||||
@@ -384,11 +238,7 @@ object DisplayUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 语言本地化工具类
|
|
||||||
*/
|
|
||||||
object LocaleUtils {
|
object LocaleUtils {
|
||||||
|
|
||||||
@SuppressLint("ObsoleteSdkInt")
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
fun applyLanguageSetting(context: Context) {
|
fun applyLanguageSetting(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
Reference in New Issue
Block a user