11package com.tool.tree
22
33import android.Manifest
4+ import android.content.Context
45import android.content.Intent
56import android.content.pm.PackageManager
7+ import android.content.res.Configuration
68import android.net.Uri
79import android.os.Build
810import android.os.Bundle
@@ -20,180 +22,179 @@ import com.omarea.common.shell.ShellExecutor
2022import com.omarea.common.ui.DialogHelper
2123import com.omarea.krscript.executor.ScriptEnvironmen
2224import com.tool.tree.databinding.ActivitySplashBinding
23- import kotlinx.coroutines.*
25+ import kotlinx.coroutines.Dispatchers
26+ import kotlinx.coroutines.launch
27+ import kotlinx.coroutines.withContext
2428import java.io.BufferedReader
2529import java.io.DataOutputStream
2630import java.io.File
27- import android.content.res.Configuration
2831import java.util.Locale
2932
3033class SplashActivity : AppCompatActivity () {
3134
3235 private lateinit var binding: ActivitySplashBinding
3336 private val REQUEST_CODE_PERMISSIONS = 1001
34- private val REQUEST_CODE_MANAGE_STORAGE = 1002
37+ private val REQUEST_CODE_MANAGE_ALL_FILES = 1002
3538
3639 private var hasRoot = false
37- private var startingJob: Job ? = null // Dùng Job để quản lý luồng khởi động
38-
39- private val rows = mutableListOf<String >()
40- private var ignored = false
41- private val maxLines = 5
40+ private var started = false
41+ private var starting = false
4242
4343 override fun onCreate (savedInstanceState : Bundle ? ) {
4444 applyAppLanguage()
4545 super .onCreate(savedInstanceState)
46-
4746 ThemeModeState .switchTheme(this )
4847 binding = ActivitySplashBinding .inflate(layoutInflater)
4948 setContentView(binding.root)
5049
51- WindowCompat .setDecorFitsSystemWindows(window, false )
52-
53- // Kiểm tra khởi tạo nhanh
50+ // 1. Kiểm tra nếu script đã chạy hoặc đang chạy thì vào thẳng Home
5451 if (ScriptEnvironmen .isInited() && isTaskRoot &&
5552 ! intent.getBooleanExtra(" force_reset" , false )) {
5653 gotoHome()
5754 return
5855 }
5956
60- if (! hasAgreed()) {
57+ // 2. Logic khởi đầu: Nếu chưa đồng ý hoặc thiếu quyền thì hiện Dialog
58+ if (hasAgreed() && hasRequiredPermissions()) {
59+ checkPermissionsNextStep()
60+ } else {
6161 showAgreementDialog()
6262 }
6363
6464 binding.startLogoXml.startAnimation(AnimationUtils .loadAnimation(this , R .anim.ic_settings_rotate))
6565 applyTheme()
6666 }
6767
68- // Luồng khởi động duy nhất
69- private fun startLogic () {
70- if (startingJob?.isActive == true ) return // Nếu đang chạy thì không chạy thêm luồng mới
71-
72- startingJob = lifecycleScope.launch(Dispatchers .Main ) {
73- saveAgreement()
74-
75- // Bước 1: Kiểm tra Root với Timeout tránh treo cứng
76- hasRoot = withContext(Dispatchers .IO ) {
77- try {
78- withTimeoutOrNull(5000 ) { KeepShellPublic .checkRoot() } ? : false
79- } catch (e: Exception ) { false }
80- }
81-
82- // Bước 2: Chuẩn bị config và chạy script
83- binding.startStateText.text = getString(R .string.pop_started)
84- val config = KrScriptConfig ().init (this @SplashActivity)
85-
86- if (config.beforeStartSh.isNotEmpty()) {
87- executeShellLogic(config, hasRoot)
88- } else {
89- gotoHome()
90- }
91- }
92- }
93-
94- private suspend fun executeShellLogic (config : KrScriptConfig , hasRoot : Boolean ) {
95- withContext(Dispatchers .IO ) {
96- var process: Process ? = null
97- try {
98- process = if (hasRoot) ShellExecutor .getSuperUserRuntime() else ShellExecutor .getRuntime()
99- process?.let { p ->
100- // Đọc log song song
101- val logJob = launch {
102- p.inputStream.bufferedReader().useLines { lines ->
103- lines.forEach { line ->
104- withContext(Dispatchers .Main ) { onLogOutput(line) }
105- }
106- }
107- }
68+ // =================== LOGIC XỬ LÝ QUYỀN ===================
10869
109- // Thực thi shell
110- DataOutputStream (p.outputStream).use { os ->
111- ScriptEnvironmen .executeShell(
112- this @SplashActivity, os, config.beforeStartSh, config.variables, null , " pio-splash"
113- )
114- }
115-
116- // Đợi tối đa 15s cho script
117- withTimeoutOrNull(15000 ) { p.waitFor() }
118- logJob.cancel()
119- }
120- } catch (e: Exception ) {
121- e.printStackTrace()
122- } finally {
123- process?.destroy() // Giải phóng tiến trình
124- withContext(Dispatchers .Main ) { gotoHome() }
125- }
126- }
70+ private fun showAgreementDialog () {
71+ DialogHelper .warning(
72+ this ,
73+ getString(R .string.permission_dialog_title),
74+ getString(R .string.permission_dialog_message),
75+ Runnable {
76+ // Người dùng đồng ý bảng Dialog -> Chuyển sang xin quyền bộ nhớ hệ thống
77+ requestRequiredPermissions()
78+ },
79+ Runnable { finishAffinity() } // Không đồng ý -> Thoát ứng dụng
80+ ).setCancelable(false )
12781 }
12882
129- override fun onResume () {
130- super .onResume()
131- // Chỉ chạy nếu đã đồng ý và các quyền đã sẵn sàng
132- if (hasAgreed() && startingJob == null && hasPermissions()) {
133- startLogic()
83+ private fun hasRequiredPermissions (): Boolean {
84+ val permissions = mutableListOf<String >()
85+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
86+ permissions.add(Manifest .permission.READ_MEDIA_IMAGES )
87+ permissions.add(Manifest .permission.READ_MEDIA_VIDEO )
88+ } else {
89+ permissions.add(Manifest .permission.READ_EXTERNAL_STORAGE )
90+ permissions.add(Manifest .permission.WRITE_EXTERNAL_STORAGE )
13491 }
135- }
136-
137- // --- Giữ nguyên các hàm bổ trợ của bạn ---
138-
139- private fun hasPermissions (): Boolean {
140- val basic = getRequiredPermissions().all {
92+ return permissions.all {
14193 ContextCompat .checkSelfPermission(this , it) == PackageManager .PERMISSION_GRANTED
14294 }
143- val manage = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) Environment .isExternalStorageManager() else true
144- return basic && manage
14595 }
14696
147- private fun getRequiredPermissions (): Array <String > {
148- return when {
149- Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU -> arrayOf(Manifest .permission.READ_MEDIA_IMAGES , Manifest .permission.READ_MEDIA_VIDEO )
150- else -> arrayOf(Manifest .permission.READ_EXTERNAL_STORAGE , Manifest .permission.WRITE_EXTERNAL_STORAGE )
97+ private fun requestRequiredPermissions () {
98+ val permissions = mutableListOf<String >()
99+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
100+ permissions.add(Manifest .permission.READ_MEDIA_IMAGES )
101+ permissions.add(Manifest .permission.READ_MEDIA_VIDEO )
102+ } else {
103+ permissions.add(Manifest .permission.READ_EXTERNAL_STORAGE )
104+ permissions.add(Manifest .permission.WRITE_EXTERNAL_STORAGE )
151105 }
106+ ActivityCompat .requestPermissions(this , permissions.toTypedArray(), REQUEST_CODE_PERMISSIONS )
152107 }
153108
154- private fun requestAppPermissions () {
155- val missing = getRequiredPermissions().filter { ContextCompat .checkSelfPermission(this , it) != PackageManager .PERMISSION_GRANTED }
156- if (missing.isNotEmpty()) {
157- ActivityCompat .requestPermissions(this , missing.toTypedArray(), REQUEST_CODE_PERMISSIONS )
158- } else if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R && ! Environment .isExternalStorageManager()) {
159- requestManageStoragePermission()
109+ private fun checkPermissionsNextStep () {
110+ // Ưu tiên sau: Nếu là Android 11+ và chưa có quyền "All Files Access"
111+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R && ! Environment .isExternalStorageManager()) {
112+ requestManageAllFilesPermission()
160113 } else {
161- startLogic ()
114+ checkRootAndStart ()
162115 }
163116 }
164117
165- private fun requestManageStoragePermission () {
166- val intent = Intent (Settings .ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION ).apply { data = Uri .parse(" package:$packageName " ) }
167- try { startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE ) }
168- catch (e: Exception ) { startActivityForResult(Intent (Settings .ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION ), REQUEST_CODE_MANAGE_STORAGE ) }
118+ private fun requestManageAllFilesPermission () {
119+ try {
120+ val intent = Intent (Settings .ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION ).apply {
121+ data = Uri .parse(" package:$packageName " )
122+ }
123+ startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES )
124+ } catch (e: Exception ) {
125+ val intent = Intent (Settings .ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION )
126+ startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES )
127+ }
169128 }
170129
171130 override fun onRequestPermissionsResult (requestCode : Int , permissions : Array <out String >, grantResults : IntArray ) {
172131 super .onRequestPermissionsResult(requestCode, permissions, grantResults)
173132 if (requestCode == REQUEST_CODE_PERMISSIONS ) {
174- if (grantResults.all { it == PackageManager .PERMISSION_GRANTED }) requestAppPermissions() else finish()
133+ // Nếu người dùng đồng ý các quyền bộ nhớ cơ bản
134+ if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager .PERMISSION_GRANTED }) {
135+ checkPermissionsNextStep()
136+ } else {
137+ // Nếu từ chối bất kỳ quyền nào -> Thoát app
138+ finishAffinity()
139+ }
175140 }
176141 }
177142
178143 override fun onActivityResult (requestCode : Int , resultCode : Int , data : Intent ? ) {
179144 super .onActivityResult(requestCode, resultCode, data)
180- if (requestCode == REQUEST_CODE_MANAGE_STORAGE ) {
181- if (hasPermissions()) startLogic() else finish()
145+ if (requestCode == REQUEST_CODE_MANAGE_ALL_FILES ) {
146+ // Sau khi từ màn hình cài đặt All Files quay lại, tiến hành check root
147+ checkRootAndStart()
182148 }
183149 }
184150
185- private fun onLogOutput (log : String ) {
186- synchronized(rows) {
187- if (rows.size >= maxLines) { rows.removeAt(0 ); ignored = true }
188- rows.add(log)
189- binding.startStateText.text = rows.joinToString(" \n " , if (ignored) " ……\n " else " " )
151+ // =================== LOGIC ROOT & KHỞI CHẠY ===================
152+
153+ @Synchronized
154+ private fun checkRootAndStart () {
155+ if (starting) return
156+ starting = true
157+ started = true
158+
159+ // Khi đã vượt qua hết các bước xin quyền và bắt đầu check root -> Lưu trạng thái đồng ý
160+ saveAgreement()
161+
162+ lifecycleScope.launch(Dispatchers .IO ) {
163+ hasRoot = KeepShellPublic .checkRoot()
164+ withContext(Dispatchers .Main ) {
165+ starting = false
166+ startToFinish()
167+ }
168+ }
169+ }
170+
171+ private fun startToFinish () {
172+ binding.startStateText.text = getString(R .string.pop_started)
173+ val config = KrScriptConfig ().init (this )
174+
175+ if (config.beforeStartSh.isNotEmpty()) {
176+ runBeforeStartSh(config, hasRoot)
177+ } else {
178+ gotoHome()
190179 }
191180 }
192181
182+ private fun hasAgreed (): Boolean =
183+ getSharedPreferences(" kr-script-config" , MODE_PRIVATE ).getBoolean(" agreed_permissions" , false )
184+
185+ private fun saveAgreement () {
186+ getSharedPreferences(" kr-script-config" , MODE_PRIVATE )
187+ .edit()
188+ .putBoolean(" agreed_permissions" , true )
189+ .apply ()
190+ }
191+
192+ // =================== CÁC HÀM TIỆN ÍCH KHÁC ===================
193+
193194 private fun applyAppLanguage () {
194195 runCatching {
195196 val langFile = File (filesDir, " home/log/language" )
196- val lang = langFile.takeIf { it.exists() }?.readText()?.trim() ? : return
197+ val lang = langFile.takeIf { it.exists() }?.readText()?.trim()?. takeIf { it.isNotEmpty() } ? : return
197198 val locale = Locale .forLanguageTag(lang.replace(" _" , " -" ))
198199 if (Locale .getDefault() != locale) {
199200 Locale .setDefault(locale)
@@ -206,28 +207,69 @@ class SplashActivity : AppCompatActivity() {
206207 private fun applyTheme () {
207208 val typedValue = TypedValue ()
208209 theme.resolveAttribute(android.R .attr.windowBackground, typedValue, true )
209- val bgColor = if (typedValue.resourceId != 0 ) ContextCompat .getColor(this , typedValue.resourceId) else typedValue.data
210+ val bgColor = if (typedValue.resourceId != 0 ) getColor(typedValue.resourceId) else typedValue.data
211+
212+ WindowCompat .setDecorFitsSystemWindows(window, false )
210213 window.statusBarColor = bgColor
211214 window.navigationBarColor = bgColor
212- WindowCompat .getInsetsController(window, window.decorView).apply {
213- val isDark = ThemeModeState .isDarkMode()
214- isAppearanceLightStatusBars = ! isDark
215- isAppearanceLightNavigationBars = ! isDark
215+
216+ val controller = WindowCompat .getInsetsController(window, window.decorView)
217+ val isDark = ThemeModeState .isDarkMode()
218+ controller.isAppearanceLightStatusBars = ! isDark
219+ controller.isAppearanceLightNavigationBars = ! isDark
220+ }
221+
222+ private fun gotoHome () {
223+ val intentToStart = if (intent?.getBooleanExtra(" JumpActionPage" , false ) == true ) {
224+ Intent (this , ActionPage ::class .java).apply { putExtras(intent!! ) }
225+ } else {
226+ Intent (this , MainActivity ::class .java)
216227 }
228+ startActivity(intentToStart)
229+ finish()
217230 }
218231
219- private fun showAgreementDialog () {
220- DialogHelper .warning(this , getString(R .string.permission_dialog_title), getString(R .string.permission_dialog_message), { requestAppPermissions() }, { finish() }).setCancelable(false )
232+ private fun runBeforeStartSh (config : KrScriptConfig , hasRoot : Boolean ) {
233+ lifecycleScope.launch(Dispatchers .IO ) {
234+ try {
235+ val process = if (hasRoot) ShellExecutor .getSuperUserRuntime() else ShellExecutor .getRuntime()
236+ process?.let {
237+ DataOutputStream (it.outputStream).use { os ->
238+ ScriptEnvironmen .executeShell(this @SplashActivity, os, config.beforeStartSh, config.variables, null , " pio-splash" )
239+ }
240+ launch { readStreamAsync(it.inputStream.bufferedReader()) }
241+ launch { readStreamAsync(it.errorStream.bufferedReader()) }
242+ it.waitFor()
243+ }
244+ } finally {
245+ withContext(Dispatchers .Main ) {
246+ gotoHome()
247+ }
248+ }
249+ }
221250 }
222251
223- private fun hasAgreed () = getSharedPreferences(" kr-script-config" , MODE_PRIVATE ).getBoolean(" agreed_permissions" , false )
224- private fun saveAgreement () = getSharedPreferences(" kr-script-config" , MODE_PRIVATE ).edit().putBoolean(" agreed_permissions" , true ).apply ()
252+ private val rows = mutableListOf<String >()
253+ private var ignored = false
254+ private val maxLines = 5
255+ private val handler = android.os.Handler (android.os.Looper .getMainLooper())
225256
226- private fun gotoHome () {
227- val nextIntent = if (intent?.getBooleanExtra(" JumpActionPage" , false ) == true ) {
228- Intent (this , ActionPage ::class .java).apply { intent?.extras?.let { putExtras(it) } }
229- } else Intent (this , MainActivity ::class .java)
230- startActivity(nextIntent)
231- finish()
257+ private fun readStreamAsync (reader : BufferedReader ) {
258+ try {
259+ reader.forEachLine { line -> onLogOutput(line) }
260+ } catch (e: Exception ) {}
261+ }
262+
263+ private fun onLogOutput (log : String ) {
264+ handler.post {
265+ synchronized(rows) {
266+ if (rows.size >= maxLines) {
267+ rows.removeAt(0 )
268+ ignored = true
269+ }
270+ rows.add(log)
271+ binding.startStateText.text = rows.joinToString(" \n " , if (ignored) " ……\n " else " " )
272+ }
273+ }
232274 }
233275}
0 commit comments