diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d047048..cb456cd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,7 @@ android { minSdk = 26 targetSdk = 36 versionCode = 1 - versionName = "0.0.5" + versionName = "0.0.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/aidl/IShellService.aidl b/app/src/main/aidl/IShellService.aidl index b98cdd8..67b3355 100644 --- a/app/src/main/aidl/IShellService.aidl +++ b/app/src/main/aidl/IShellService.aidl @@ -1,4 +1,5 @@ interface IShellService { void exec(String pluginExecuteEntryPoint, String pluginPackageDirectory, IShellCallback callback); boolean kill(int progressPid); + void command(String commandContent, IShellCallback callback); } \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/components/shellScreen/ShellScreenNecessaryComponents.kt b/app/src/main/java/com/baidaidai/rootless_store/components/shellScreen/ShellScreenNecessaryComponents.kt new file mode 100644 index 0000000..b99d5c5 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/components/shellScreen/ShellScreenNecessaryComponents.kt @@ -0,0 +1,62 @@ +package com.baidaidai.rootless_store.components.shellScreen + +import com.baidaidai.rootless_store.R +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.painterResource +import kotlinx.coroutines.launch + +object ShellScreenNecessaryComponents { + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun ShellScreenScreenTopAppBar( + onTopIconClick:suspend ()-> Unit = {}, + onBottomIconClick:suspend ()-> Unit = {}, + onDeleteIconClick:()-> Unit = {}, + ){ + val coroutineScope = rememberCoroutineScope() + + TopAppBar( + title = { + Text("ShellScreen") + }, + actions = { + IconButton( + onClick = { + coroutineScope.launch { + onTopIconClick() + } + } + ){ + Icon( + painterResource(R.drawable.material_symbols_top), + contentDescription = "To Top" + ) + } + IconButton( + onClick = { + coroutineScope.launch { + onBottomIconClick() + } + } + ){ + Icon( + painterResource(R.drawable.material_symbols_bottom), + contentDescription = "To Top" + ) + } + IconButton(onClick = onDeleteIconClick){ + Icon( + painterResource(R.drawable.material_symbols_delete), + contentDescription = "To Top" + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/data/shell/gateway/ExecuteShellGatewayImpl.kt b/app/src/main/java/com/baidaidai/rootless_store/data/shell/gateway/ExecuteShellGatewayImpl.kt new file mode 100644 index 0000000..03483a6 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/data/shell/gateway/ExecuteShellGatewayImpl.kt @@ -0,0 +1,116 @@ +package com.baidaidai.rootless_store.data.shell.gateway + +import android.util.Log +import com.baidaidai.rootless_store.data.shizuku.repository.ShizukuAdbRepositoryImpl +import com.baidaidai.rootless_store.data.shizuku.server.ShizukuEndpointCallback +import com.baidaidai.rootless_store.domain.execute.model.ExecuteResult +import com.baidaidai.rootless_store.domain.execute.model.ResultTag +import com.baidaidai.rootless_store.domain.shell.model.ShellResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ExecuteShellGatewayImpl @Inject constructor( + private val shizukuAdbRepositoryImpl: ShizukuAdbRepositoryImpl, +) { + + fun runCommandByAppShell(commandContent: String): Flow = callbackFlow { + val process = ProcessBuilder("sh", "-c", commandContent).start() + + launch(Dispatchers.IO) { + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { result -> + send( + ShellResult( + resulTag = ResultTag.Normal, + command = "~ $commandContent", + content = result, + ) + ) + } + } + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { error -> + send( + ShellResult( + resulTag = ResultTag.RedLine, + command = "~ $commandContent", + content = error, + ) + ) + } + } + } + + awaitClose {} + + }.flowOn(Dispatchers.IO) + + fun runCommandByADBShell(commandContent: String): Flow = callbackFlow { + launch(Dispatchers.IO) { + val callback = ShizukuEndpointCallback( + onExecuteCallback = { session -> + trySend( + ShellResult( + resulTag = ResultTag.Normal, + command = "~ $commandContent", + content = session.toString(), + ) + ) + }, + onErrorCallback = { error -> + trySend( + ShellResult( + resulTag = ResultTag.RedLine, + command = "~ $commandContent", + content = error.toString(), + ) + ) + } + ) + + Log.d("exam",(shizukuAdbRepositoryImpl.getShizukuEndpoint()==null).toString()) + + shizukuAdbRepositoryImpl.getShizukuEndpoint() + ?.command(commandContent, callback) + } + awaitClose { } + } + + fun runCommandByRootShell(commandContent: String): Flow = callbackFlow { + val process = ProcessBuilder("su", "-c", commandContent).start() + + launch(Dispatchers.IO) { + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { result -> + send( + ShellResult( + resulTag = ResultTag.Normal, + command = "# $commandContent", + content = result, + ) + ) + } + } + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { error -> + send( + ShellResult( + resulTag = ResultTag.RedLine, + command = "# $commandContent", + content = error, + ) + ) + } + } + } + + awaitClose {} + + }.flowOn(Dispatchers.IO) + +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/data/shell/repository/ExecuteShellRepositoryImpl.kt b/app/src/main/java/com/baidaidai/rootless_store/data/shell/repository/ExecuteShellRepositoryImpl.kt new file mode 100644 index 0000000..b018ecb --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/data/shell/repository/ExecuteShellRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.baidaidai.rootless_store.data.shell.repository + +import com.baidaidai.rootless_store.data.shell.gateway.ExecuteShellGatewayImpl +import com.baidaidai.rootless_store.domain.shell.model.ShellCommandContainer +import com.baidaidai.rootless_store.domain.shell.model.ShellEnvironment +import com.baidaidai.rootless_store.domain.shell.model.ShellResult +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class ExecuteShellRepositoryImpl @Inject constructor( + private val executeShellGatewayImpl: ExecuteShellGatewayImpl +) { + + private fun runCommandByAppShell(commandContent: String) = executeShellGatewayImpl.runCommandByAppShell(commandContent) + + private fun runCommandByADBShell(commandContent: String) = executeShellGatewayImpl.runCommandByADBShell(commandContent) + + private fun runCommandByRootShell(commandContent: String) = executeShellGatewayImpl.runCommandByRootShell(commandContent) + + fun runCommand(shellCommandContainer: ShellCommandContainer): Flow{ + return when(shellCommandContainer.shellEnvironment){ + ShellEnvironment.AppShell -> runCommandByAppShell(shellCommandContainer.commandContent) + ShellEnvironment.ADBShell -> runCommandByADBShell(shellCommandContainer.commandContent) + ShellEnvironment.RootShell -> runCommandByRootShell(shellCommandContainer.commandContent) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/data/shizuku/server/ShizukuEndpointTemplate.kt b/app/src/main/java/com/baidaidai/rootless_store/data/shizuku/server/ShizukuEndpointTemplate.kt index 31f889d..a2ce9b1 100644 --- a/app/src/main/java/com/baidaidai/rootless_store/data/shizuku/server/ShizukuEndpointTemplate.kt +++ b/app/src/main/java/com/baidaidai/rootless_store/data/shizuku/server/ShizukuEndpointTemplate.kt @@ -37,4 +37,26 @@ internal class ShizukuEndpointTemplate : IShellService.Stub() { .waitFor() return process == 0 } + + override fun command(commandContent: String, callback: IShellCallback ){ + val process = ProcessBuilder("sh","-c",commandContent).start() + + process + .inputStream + .bufferedReader() + .useLines{ line -> + line.forEach { + callback.onExecute(it) + } + } + + process + .errorStream + .bufferedReader() + .useLines{ line -> + line.forEach { + callback.onError(it) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/data/status/gateway/StoreStatusGatewayImpl.kt b/app/src/main/java/com/baidaidai/rootless_store/data/status/gateway/StoreStatusGatewayImpl.kt index 7ad3b46..1514d59 100644 --- a/app/src/main/java/com/baidaidai/rootless_store/data/status/gateway/StoreStatusGatewayImpl.kt +++ b/app/src/main/java/com/baidaidai/rootless_store/data/status/gateway/StoreStatusGatewayImpl.kt @@ -86,4 +86,13 @@ class StoreStatusGatewayImpl @Inject constructor( HosterOverallStatus.LIMITED } } + + fun getRootStatus(): Boolean { + return Shell.getShell().isRoot + } + + fun getShizukuStatus(): Boolean { + return shizukuEndpointManager.bind() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/data/status/repository/StoreStatusRepositoryImpl.kt b/app/src/main/java/com/baidaidai/rootless_store/data/status/repository/StoreStatusRepositoryImpl.kt index afbb537..1aad043 100644 --- a/app/src/main/java/com/baidaidai/rootless_store/data/status/repository/StoreStatusRepositoryImpl.kt +++ b/app/src/main/java/com/baidaidai/rootless_store/data/status/repository/StoreStatusRepositoryImpl.kt @@ -7,6 +7,7 @@ import com.baidaidai.rootless_store.domain.status.model.MemoryStatus import com.baidaidai.rootless_store.domain.status.model.SELinuxStatus import com.baidaidai.rootless_store.domain.status.model.StorageStatus import com.baidaidai.rootless_store.domain.status.model.TempStatus +import com.topjohnwu.superuser.Shell import jakarta.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,4 +28,8 @@ class StoreStatusRepositoryImpl @Inject constructor( fun getAndroidAndAPIStatus(): AndroidAndAPIStatus = storeStatusGatewayImpl.getAndroidAndAPIStatus() fun getOverallStatus(): HosterOverallStatus = storeStatusGatewayImpl.getHosterOverallStatus() + + fun getRootStatus(): Boolean = storeStatusGatewayImpl.getRootStatus() + + fun getShizukuStatus(): Boolean = storeStatusGatewayImpl.getShizukuStatus() } diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellCommandContainer.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellCommandContainer.kt new file mode 100644 index 0000000..77bb3e5 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellCommandContainer.kt @@ -0,0 +1,6 @@ +package com.baidaidai.rootless_store.domain.shell.model + +data class ShellCommandContainer( + val shellEnvironment: ShellEnvironment, + val commandContent: String +) diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellEnvironment.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellEnvironment.kt new file mode 100644 index 0000000..9db4014 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellEnvironment.kt @@ -0,0 +1,5 @@ +package com.baidaidai.rootless_store.domain.shell.model + +enum class ShellEnvironment { + AppShell,ADBShell,RootShell +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellResult.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellResult.kt new file mode 100644 index 0000000..4a6df8b --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/model/ShellResult.kt @@ -0,0 +1,10 @@ +package com.baidaidai.rootless_store.domain.shell.model + +import com.baidaidai.rootless_store.domain.execute.model.ResultTag +import kotlin.String + +data class ShellResult( + val resulTag: ResultTag, + val command: String?, + val content: String +) diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetADBShellStatusUseCase.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetADBShellStatusUseCase.kt new file mode 100644 index 0000000..b34c400 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetADBShellStatusUseCase.kt @@ -0,0 +1,10 @@ +package com.baidaidai.rootless_store.domain.shell.usecase + +import com.baidaidai.rootless_store.data.status.repository.StoreStatusRepositoryImpl +import javax.inject.Inject + +class GetADBShellStatusUseCase @Inject constructor( + private val storeStatusRepositoryImpl: StoreStatusRepositoryImpl +) { + operator fun invoke () = storeStatusRepositoryImpl.getShizukuStatus() +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetRootShellStatusUseCase.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetRootShellStatusUseCase.kt new file mode 100644 index 0000000..760f501 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/GetRootShellStatusUseCase.kt @@ -0,0 +1,10 @@ +package com.baidaidai.rootless_store.domain.shell.usecase + +import com.baidaidai.rootless_store.data.status.repository.StoreStatusRepositoryImpl +import javax.inject.Inject + +class GetRootShellStatusUseCase @Inject constructor( + private val storeStatusRepositoryImpl: StoreStatusRepositoryImpl +) { + operator fun invoke () = storeStatusRepositoryImpl.getRootStatus() +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/RunCommandUseCase.kt b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/RunCommandUseCase.kt new file mode 100644 index 0000000..d6ff4c4 --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/domain/shell/usecase/RunCommandUseCase.kt @@ -0,0 +1,11 @@ +package com.baidaidai.rootless_store.domain.shell.usecase + +import com.baidaidai.rootless_store.data.shell.repository.ExecuteShellRepositoryImpl +import com.baidaidai.rootless_store.domain.shell.model.ShellCommandContainer +import javax.inject.Inject + +class RunCommandUseCase @Inject constructor( + private val executeShellRepositoryImpl: ExecuteShellRepositoryImpl +) { + operator fun invoke(shellCommandContainer: ShellCommandContainer) = executeShellRepositoryImpl.runCommand(shellCommandContainer) +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/ui/model/RootLessStoreShellScreenViewModel.kt b/app/src/main/java/com/baidaidai/rootless_store/ui/model/RootLessStoreShellScreenViewModel.kt new file mode 100644 index 0000000..fb28fab --- /dev/null +++ b/app/src/main/java/com/baidaidai/rootless_store/ui/model/RootLessStoreShellScreenViewModel.kt @@ -0,0 +1,71 @@ +package com.baidaidai.rootless_store.ui.model + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.baidaidai.rootless_store.data.shell.repository.ExecuteShellRepositoryImpl +import com.baidaidai.rootless_store.domain.shell.model.ShellCommandContainer +import com.baidaidai.rootless_store.domain.shell.model.ShellResult +import com.baidaidai.rootless_store.domain.shell.usecase.GetADBShellStatusUseCase +import com.baidaidai.rootless_store.domain.shell.usecase.GetRootShellStatusUseCase +import com.baidaidai.rootless_store.domain.shell.usecase.RunCommandUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RootLessStoreShellScreenViewModel @Inject constructor( + private val executeShellRepositoryImpl: ExecuteShellRepositoryImpl, + private val runCommandUseCase: RunCommandUseCase, + private val getRootShellStatusUseCase: GetRootShellStatusUseCase, + private val getADBShellStatusUseCase: GetADBShellStatusUseCase +) : ViewModel(){ + + private var _shellOutputList = MutableStateFlow(emptyList()) + private val _rootShellStatus = MutableStateFlow(getRootShellStatusUseCase()) + private val _adbShellStatus = MutableStateFlow(getADBShellStatusUseCase()) + private var _lastCommandContent = MutableStateFlow("") + val shellOutputList = _shellOutputList.asStateFlow() + val rootShellStatus = _rootShellStatus.asStateFlow() + val adbShellStatus = _adbShellStatus.asStateFlow() + val lastCommandContent = _lastCommandContent.asStateFlow() + + fun runCommand(shellCommandContainer: ShellCommandContainer){ + viewModelScope.launch { + val shellOutput = runCommandUseCase(shellCommandContainer) + shellOutput.collect { shellResult -> + + Log.d("ShellViewModel","shellResult.command: ${shellResult.command}") + Log.d("ShellViewModel","lastCommandContent: ${lastCommandContent.value}") + Log.d("ShellViewModel","lastCommandContent == shellResult.command: ${lastCommandContent.value == shellResult.command}") + + if (_lastCommandContent.value != shellResult.command && shellResult.command != null){ + + _lastCommandContent.value = shellResult.command + _shellOutputList.value += shellResult + + }else if(_lastCommandContent.value == shellResult.command){ + + _shellOutputList.value += shellResult.copy(command = null) + + } + + + + } + } + _lastCommandContent.value = "" + } + + fun cleanShellOutputList() { + _shellOutputList.value = emptyList() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/ui/screens/ShellScreen.kt b/app/src/main/java/com/baidaidai/rootless_store/ui/screens/ShellScreen.kt index 96b213b..7f8266c 100644 --- a/app/src/main/java/com/baidaidai/rootless_store/ui/screens/ShellScreen.kt +++ b/app/src/main/java/com/baidaidai/rootless_store/ui/screens/ShellScreen.kt @@ -1,5 +1,6 @@ package com.baidaidai.rootless_store.ui.screens +import android.graphics.Color import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,153 +11,313 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.Card -import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.DropdownMenuGroup +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.DropdownMenuPopup +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SplitButtonDefaults +import androidx.compose.material3.SplitButtonLayout import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.baidaidai.rootless_store.R +import com.baidaidai.rootless_store.components.shellScreen.ShellScreenNecessaryComponents +import com.baidaidai.rootless_store.domain.execute.model.ResultTag +import com.baidaidai.rootless_store.domain.shell.model.ShellCommandContainer +import com.baidaidai.rootless_store.domain.shell.model.ShellEnvironment +import com.baidaidai.rootless_store.ui.model.RootLessStoreShellScreenViewModel +import androidx.compose.ui.graphics.* import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.launch - -private fun runShell( - shellCommand: String -): List<*> { - val runShell = Shell.cmd(shellCommand).exec() - return runShell.out -} +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ShellScreen( - contentPaddingValues: PaddingValues + contentPaddingValues: PaddingValues, + shellScreenViewModel: RootLessStoreShellScreenViewModel, + lazyColumnState: LazyListState ){ - var value by remember { mutableStateOf("") } - - val lazyListState = rememberLazyListState() + var commandContent by remember { mutableStateOf("") } + var shellEnvironment by remember { mutableStateOf(ShellEnvironment.AppShell) } + var trailingButtonStatus by remember { mutableStateOf(false) } - val coroutineScope = rememberCoroutineScope() + val shellOutputList by shellScreenViewModel.shellOutputList.collectAsState() + val rootShellStatus by shellScreenViewModel.rootShellStatus.collectAsState() + val adbShellStatus by shellScreenViewModel.adbShellStatus.collectAsState() - data class ShellResult( - val command: String, - val output: List<*> - ) + LaunchedEffect(shellOutputList.size) { + if (shellOutputList.isNotEmpty()) { + lazyColumnState.scrollToItem(shellOutputList.lastIndex) + } + } - val shellOutPutList = remember { mutableStateListOf() } + val shellEnvironmentSymbol = remember(shellEnvironment){ + when(shellEnvironment){ + ShellEnvironment.AppShell, ShellEnvironment.ADBShell -> "~" + ShellEnvironment.RootShell -> "#" + } + } Column( modifier = Modifier .padding(contentPaddingValues) + .padding(vertical = 15.dp, horizontal = 15.dp) .fillMaxSize() - .padding(horizontal = 20.dp) ) { - Card( + + val trailingButtonContentPaddingAfterClick = PaddingValues(start = 15.dp, end = 15.dp) + val trailingButtonContentPaddingBeforeClick = PaddingValues(start = 13.dp, end = 17.dp) + val trailingButtonSizeBeforeClick = SplitButtonDefaults.trailingButtonShapesFor(56.dp).shape + + val trailingButtonContentPadding = remember(trailingButtonStatus) { + if (trailingButtonStatus){ + trailingButtonContentPaddingAfterClick + }else{ + trailingButtonContentPaddingBeforeClick + } + } + + val trailingButtonSize = remember(trailingButtonStatus) { + if (trailingButtonStatus){ + CircleShape + }else{ + trailingButtonSizeBeforeClick + } + } + + val shellCommandContainer = remember(key1 = commandContent, key2 = shellEnvironment) { + ShellCommandContainer(shellEnvironment, commandContent = commandContent) + } + + Box( modifier = Modifier - .fillMaxWidth() - ){ - Box( - modifier = Modifier - .padding(30.dp) - .wrapContentSize() - ) { - Column{ - OutlinedTextField( - value = value, - onValueChange = {value = it}, - maxLines = 1, - label = { - Text("Shell Command") - }, - modifier = Modifier - .fillMaxWidth() - ) - Spacer( - modifier = Modifier - .height(20.dp) - ) - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - ){ - OutlinedButton( + .wrapContentSize() + ) { + Column{ + OutlinedTextField( + leadingIcon = { + Text(shellEnvironmentSymbol) + }, + trailingIcon = { + IconButton( onClick = { - value = "" + commandContent = "" } ) { - Text("Clear") + Icon( + painterResource(R.drawable.outline_close_24), + contentDescription = "Delete" + ) } - Button( - onClick = { - val shellOutput = runShell(value) - if (shellOutput.isNotEmpty()) { - shellOutPutList.add( - ShellResult( - command = value, - output = shellOutput - ) + }, + value = commandContent, + onValueChange = { + commandContent = it + }, + maxLines = 1, + label = { + Text("Shell Command") + }, + modifier = Modifier + .fillMaxWidth() + ) + Spacer( + modifier = Modifier + .height(20.dp) + ) + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + ){ + SplitButtonLayout( + leadingButton = { + Button( + onClick = { + shellScreenViewModel.runCommand(shellCommandContainer) + }, + contentPadding = SplitButtonDefaults.MediumLeadingButtonContentPadding, + shape = SplitButtonDefaults.leadingButtonShapesFor( + SplitButtonDefaults.MediumContainerHeight).shape, + modifier = Modifier + .height(56.dp) + + ) { + Icon( + painterResource(R.drawable.terminal_24px), + contentDescription = "Run Command", + modifier = Modifier + .size(SplitButtonDefaults.LeadingIconSize) + ) + Spacer( + modifier = Modifier + .size(8.dp) + ) + Text(text = "Run") + } + }, + trailingButton = { + Column{ + Button( + onClick = { + trailingButtonStatus = !trailingButtonStatus + }, + shape = trailingButtonSize, + contentPadding = trailingButtonContentPadding, + modifier = Modifier + .height(56.dp) + ){ + Icon( + painterResource(R.drawable.material_symbols_keyboard_arrow_down_icon), + contentDescription = "Expand More", + modifier = Modifier + .size(26.dp) + .rotate(if (trailingButtonStatus) 0f else -90f ) ) } - coroutineScope.launch { - if(shellOutPutList.isNotEmpty()){ - lazyListState.scrollToItem(shellOutPutList.size-1) + + DropdownMenuPopup( + expanded = trailingButtonStatus, + onDismissRequest = { trailingButtonStatus = !trailingButtonStatus}, + offset = DpOffset(x = 0.dp, y = 8.dp) + ) { + DropdownMenuGroup( + shapes = MenuDefaults.groupShapes(), + ) { + + DropdownMenuItem( + selected = shellEnvironment == ShellEnvironment.AppShell, + shapes = MenuDefaults.itemShape(1,4), + leadingIcon = { + Icon( + painterResource(R.drawable.material_symbols_applicaitons), + contentDescription = "App shell" + ) + }, + text = { + Text("App Shell") + }, + onClick = { + shellEnvironment = ShellEnvironment.AppShell + }, + ) + Spacer(modifier = Modifier.height(2.dp)) + DropdownMenuItem( + enabled = adbShellStatus, + selected = shellEnvironment == ShellEnvironment.ADBShell, + shapes = MenuDefaults.itemShape(2,4), + leadingIcon = { + Icon( + painterResource(R.drawable.material_symbols_adb), + contentDescription = "ADB shell" + ) + }, + text = { + Text("ADB shell") + }, + onClick = { + shellEnvironment = ShellEnvironment.ADBShell + } + ) + Spacer(modifier = Modifier.height(2.dp)) + DropdownMenuItem( + enabled = rootShellStatus, + selected = shellEnvironment == ShellEnvironment.RootShell, + shapes = MenuDefaults.itemShape(3,4), + leadingIcon = { + Icon( + painterResource(R.drawable.material_symbols_cyclone), + contentDescription = "Root shell" + ) + }, + text = { + Text("Root Shell") + }, + onClick = { + shellEnvironment = ShellEnvironment.RootShell + } + ) } } } - ) { - Text(text = "Run") } - } + ) } } } + Spacer( modifier = Modifier .height(30.dp) ) + Card( modifier = Modifier .height(500.dp) .fillMaxWidth() ) { - Column( + LazyColumn( + state = lazyColumnState, modifier = Modifier - .padding(30.dp) - ) { - LazyColumn( - state = lazyListState - ) { - items( - items = shellOutPutList - ) { result -> - Text("~ ${result.command}") - result.output.forEach { line -> - Text(line.toString()) - } + .padding(horizontal = 25.dp, vertical = 15.dp) + ){ + items( + items = shellOutputList + ){ shellResult -> + if(shellResult.command != null){ + Text(shellResult.command) + } + + if (shellResult.resulTag == ResultTag.RedLine){ + Text( + shellResult.content, + color = Color(Color.RED) + ) + }else{ + Text( + shellResult.content, + ) } } + } } } } -@Composable -@PreviewLightDark -private fun GreetingScreenPreview(){ - Scaffold() { contentPadding-> - ShellScreen(contentPaddingValues = contentPadding) - } -} \ No newline at end of file +//@Composable +//@PreviewLightDark +//private fun GreetingScreenPreview(){ +// Scaffold( +// topBar = { +// ShellScreenNecessaryComponents.ShellScreenScreenTopAppBar() +// } +// ) { contentPadding-> +// ShellScreen(contentPaddingValues = contentPadding) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/baidaidai/rootless_store/ui/screens/StartScreenContainer.kt b/app/src/main/java/com/baidaidai/rootless_store/ui/screens/StartScreenContainer.kt index df97a65..21c5003 100644 --- a/app/src/main/java/com/baidaidai/rootless_store/ui/screens/StartScreenContainer.kt +++ b/app/src/main/java/com/baidaidai/rootless_store/ui/screens/StartScreenContainer.kt @@ -6,6 +6,7 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Scaffold @@ -30,6 +31,7 @@ import com.baidaidai.rootless_store.ShizukuActivity import com.baidaidai.rootless_store.components.executeScreen.executeScreenNecessaryComponents import com.baidaidai.rootless_store.components.marketScreen.MarketScreenNecessaryComponents import com.baidaidai.rootless_store.components.pluginsScreen.PluginScreenNecessaryComponents +import com.baidaidai.rootless_store.components.shellScreen.ShellScreenNecessaryComponents import com.baidaidai.rootless_store.components.sourcesScreen.SourcesScreenNecessaryComponents import com.baidaidai.rootless_store.components.startScreen.StartScreenErrorDialog import com.baidaidai.rootless_store.components.startScreen.StartScreenRepositoryDialog @@ -38,6 +40,7 @@ import com.baidaidai.rootless_store.domain.error.RootlessStoreError import com.baidaidai.rootless_store.ui.model.RootLessStoreExecuteScreenViewModel import com.baidaidai.rootless_store.ui.model.RootLessStoreMarketScreenViewModel import com.baidaidai.rootless_store.ui.model.RootLessStorePluginScreenViewModel +import com.baidaidai.rootless_store.ui.model.RootLessStoreShellScreenViewModel import com.baidaidai.rootless_store.ui.model.RootLessStoreSourceScreenViewModel import com.baidaidai.rootless_store.ui.theme.RootlessStoreTheme @@ -53,6 +56,7 @@ fun RootlessStoreStartScreenContainer( // VM & VM Data val marketScreenViewModel = hiltViewModel() val executeScreenViewModel = hiltViewModel() + val shellScreenViewModel = hiltViewModel() val pluginInfoCount by pluginScreenViewModel.pluginInfoCount.collectAsState() val sourceCount by sourceScreenViewModel.sourceCount.collectAsState() @@ -72,6 +76,9 @@ fun RootlessStoreStartScreenContainer( } } + val lazyColumnState = rememberLazyListState() + val totalListLength = shellScreenViewModel.shellOutputList.collectAsState().value.size + LaunchedEffect(fileIntentUri) { val uri = fileIntentUri ?: return@LaunchedEffect navController.navigate("PluginScreen") { @@ -135,6 +142,18 @@ fun RootlessStoreStartScreenContainer( "MarketScreen" -> MarketScreenNecessaryComponents.MarketScreenScreenTopAppBar( sourceName = currentPluginSource!!.sourceName ) + "ShellScreen" -> ShellScreenNecessaryComponents.ShellScreenScreenTopAppBar( + onTopIconClick = { + lazyColumnState.scrollToItem(0) + }, + onBottomIconClick = { + + lazyColumnState.scrollToItem(totalListLength) + }, + onDeleteIconClick = { + shellScreenViewModel.cleanShellOutputList() + } + ) else -> StartScreenNecessaryComponents.StartScreenTopAppBar(scrollBehavior) } }, @@ -234,7 +253,11 @@ fun RootlessStoreStartScreenContainer( composable( route = "ShellScreen" ){ - ShellScreen(contentPaddingValues = contentPadding) + ShellScreen( + contentPaddingValues = contentPadding, + shellScreenViewModel = shellScreenViewModel, + lazyColumnState = lazyColumnState + ) } composable( route = "ExecuteScreen" diff --git a/app/src/main/res/drawable/material_symbols_adb.xml b/app/src/main/res/drawable/material_symbols_adb.xml new file mode 100644 index 0000000..58550a1 --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_adb.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_applicaitons.xml b/app/src/main/res/drawable/material_symbols_applicaitons.xml new file mode 100644 index 0000000..a69d9fd --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_applicaitons.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_bottom.xml b/app/src/main/res/drawable/material_symbols_bottom.xml new file mode 100644 index 0000000..2b40461 --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_bottom.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_cyclone.xml b/app/src/main/res/drawable/material_symbols_cyclone.xml new file mode 100644 index 0000000..0eb67cd --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_cyclone.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_delete.xml b/app/src/main/res/drawable/material_symbols_delete.xml new file mode 100644 index 0000000..d724c2e --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_keyboard_arrow_down_icon.xml b/app/src/main/res/drawable/material_symbols_keyboard_arrow_down_icon.xml new file mode 100644 index 0000000..3f4697d --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_keyboard_arrow_down_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/material_symbols_top.xml b/app/src/main/res/drawable/material_symbols_top.xml new file mode 100644 index 0000000..5cd27f8 --- /dev/null +++ b/app/src/main/res/drawable/material_symbols_top.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be70515..2ac19d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ RootLess Store - v0.0.5 + v0.0.7 \ No newline at end of file diff --git a/asset/markdown/release/v0.0.7.md b/asset/markdown/release/v0.0.7.md new file mode 100644 index 0000000..645171f --- /dev/null +++ b/asset/markdown/release/v0.0.7.md @@ -0,0 +1,77 @@ +# Shell 工作流初步成型版 + +Welcome to Rootless Store v0.0.7! + +This update focuses on bringing a more complete Shell workflow into Rootless Store. In v0.0.7, the app introduces its initial command execution pipeline, adds environment status checks, and redesigns the Shell screen to make command-based capabilities more approachable through a clearer GUI experience. + +欢迎来到 Rootless Store v0.0.7! + +这个版本重点围绕 Shell 工作流展开,让 Rootless Store 在命令执行这条链路上更完整、更可用。v0.0.7 带来了初版命令执行管线、Shell 环境状态检查,以及重新设计的 Shell 页面,让原本偏命令行的能力以更清晰的 GUI 方式呈现出来。 + +--- + +### What's New in v0.0.7 + +* **Initial Shell Execution Workflow**: + * **Command Execution Pipeline**: Added the first complete Shell command execution flow, allowing commands to be submitted and handled inside the app. + * **Multiple Shell Environments**: The Shell workflow now supports switching between different environments such as App Shell, ADB Shell, and Root Shell. +* **Shell Status Awareness**: + * **Environment Status Queries**: Added status checks for available Shell environments, making it easier to understand which execution paths are currently usable. +* **Shell Screen Redesign**: + * **Layout & Interaction Refresh**: The Shell screen has been redesigned to improve structure, interaction flow, and overall readability. + * **Control Icons Added**: Added dedicated control and environment icons for the Shell screen, making actions more intuitive and easier to scan. + * **Output Handling Improvements**: Improved Shell output presentation and interaction behavior so command results are easier to follow. +* **Ongoing UI Polishing**: + * Continued refining Shell-related assets and interaction details to support a smoother developer-facing experience. + +### v0.0.7 更新内容 + +* **初版 Shell 执行工作流上线**: + * **命令执行管线**: 新增了第一版完整的 Shell 命令执行流程,允许用户在应用内发起并处理命令执行。 + * **多 Shell 环境切换**: Shell 工作流现已支持 App Shell、ADB Shell、Root Shell 等不同环境之间的切换。 +* **Shell 环境状态感知**: + * **环境状态检查**: 新增了可用 Shell 环境的状态查询能力,让用户更容易判断当前有哪些执行路径可用。 +* **Shell 页面重构**: + * **布局与交互刷新**: 对 Shell 页面进行了重新设计,优化了页面结构、交互流程与整体可读性。 + * **新增控制图标**: 为 Shell 页面补充了专用控制图标与环境图标,使操作更直观、信息更容易快速识别。 + * **输出处理优化**: 改进了 Shell 输出结果的呈现与交互方式,让命令执行结果更容易跟踪。 +* **持续性的 UI 打磨**: + * 持续补充并优化与 Shell 相关的资源和交互细节,提升整体的开发者使用体验。 + +--- + +### Key Features + +* **GUI-first Shell Workflow**: Rootless Store is beginning to offer a more approachable Shell experience by moving common command workflows into a structured interface. +* **Multi-environment Command Execution**: Commands can now be routed through App Shell, ADB Shell, or Root Shell depending on device capability and current state. +* **Visible Runtime Feedback**: Shell environment availability and command output are becoming more transparent inside the app. +* **Broader Platform Direction**: This Shell work continues Rootless Store’s goal of making advanced Android capabilities easier to access and manage. + +### 主要功能 + +* **GUI 优先的 Shell 工作流**: Rootless Store 正在把常见命令执行流程逐步迁移到更容易理解和操作的图形界面中。 +* **多环境命令执行能力**: 命令现可根据设备能力与当前状态,分别通过 App Shell、ADB Shell 或 Root Shell 执行。 +* **更可见的运行时反馈**: Shell 环境是否可用、命令输出结果如何,正在应用内变得更加透明和直观。 +* **更完整的平台演进方向**: 这部分 Shell 能力的加入,延续了 Rootless Store 让高级安卓能力更易接近、更易管理的目标。 + +--- + +### Known Issues & Future Plans + +* As an early version of the Shell workflow, some advanced terminal-like behaviors may still need refinement. +* Output presentation, command history, and richer interaction details are expected to improve in later versions. +* Future updates may continue expanding Shell capabilities, improve usability around long-running commands, and strengthen integration with Shizuku and other execution flows. + +### 已知问题与未来计划 + +* 作为 Shell 工作流的早期版本,一些更高级、更接近终端的交互行为仍有继续打磨的空间。 +* 输出展示、命令历史,以及更丰富的交互细节,后续版本仍会继续完善。 +* 未来更新预计会继续扩展 Shell 能力,优化长命令执行体验,并进一步加强与 Shizuku 等执行链路的衔接。 + +--- + +### Thanks + +Thank you for continuing to follow Rootless Store as it grows. Each iteration is helping shape a more complete and more approachable Android capability platform. + +感谢你持续关注 Rootless Store 的演进。每一次迭代,都在推动它朝着一个更完整、更容易接近的安卓能力平台继续前进。