Skip to content

Commit

Permalink
app: action.sh support (#13)
Browse files Browse the repository at this point in the history
Co-authored-by: LoveSy <[email protected]>
  • Loading branch information
1q23lyc45 and yujincheng08 authored Dec 21, 2024
1 parent 9caed0e commit 0fc898c
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class LocalModule(
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
val isZygisk: Boolean get() = zygiskFolder.exists()
val zygiskUnloaded: Boolean get() = unloaded.exists()
val hasAction: Boolean;

var enable: Boolean
get() = !disableFile.exists()
Expand Down Expand Up @@ -100,6 +101,8 @@ data class LocalModule(
if (name.isEmpty()) {
name = id
}

hasAction = RootUtils.fs.getFile(path, "action.sh").exists()
}

suspend fun fetch(): Boolean {
Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/topjohnwu/magisk/core/tasks/RunAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.topjohnwu.magisk.core.tasks

import android.net.Uri
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

open class RunAction(
private val module: String,
private val console: MutableList<String>,
private val logs: MutableList<String>
) {
@Throws(IOException::class)
private suspend fun run(): Boolean {
return Shell.cmd("run_action \'$module\'").to(console, logs).exec().isSuccess
}

open suspend fun exec() = withContext(Dispatchers.IO) {
try {
if (!run()) {
console.add("! Run action failed")
false
} else {
true
}
} catch (e: IOException) {
Timber.e(e)
false
}
}
}
115 changes: 115 additions & 0 deletions app/src/main/java/com/topjohnwu/magisk/ui/module/ActionFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.topjohnwu.magisk.ui.module

import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.databinding.FragmentActionMd2Binding
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import timber.log.Timber

class ActionFragment : BaseFragment<FragmentActionMd2Binding>(), MenuProvider {

override val layoutRes = R.layout.fragment_action_md2
override val viewModel by viewModel<ActionViewModel>()
override val snackbarView: View get() = binding.snackbarContainer

private var defaultOrientation = -1

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.args = ActionFragmentArgs.fromBundle(requireArguments())
}

override fun onStart() {
super.onStart()
activity?.setTitle(viewModel.args.name)
binding.closeBtn.setOnClickListener {
activity?.onBackPressed();
}

viewModel.state.observe(this) {
activity?.supportActionBar?.setSubtitle(
when (it) {
ActionViewModel.State.RUNNING -> R.string.running
ActionViewModel.State.SUCCESS -> R.string.done
ActionViewModel.State.FAILED -> R.string.failure
}
)
when (it) {
ActionViewModel.State.SUCCESS -> {
activity?.apply {
toast(
getString(
com.topjohnwu.magisk.R.string.done_action,
this@ActionFragment.viewModel.args.name
), Toast.LENGTH_LONG
)
onBackPressed()
}
}

ActionViewModel.State.FAILED -> {
binding.closeBtn.apply {
if (!this.isVisible) this.show()
if (!this.isFocused) this.requestFocus()
}
}

else -> {}
}
}
}

override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_flash, menu)
}

override fun onMenuItemSelected(item: MenuItem): Boolean {
return viewModel.onMenuItemClicked(item)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
if (savedInstanceState == null) {
viewModel.startRunAction()
}
}

@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}

override fun onKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> true

else -> false
}
}

override fun onBackPressed(): Boolean {
if (viewModel.state.value == ActionViewModel.State.RUNNING) return true
return super.onBackPressed()
}

override fun onPreBind(binding: FragmentActionMd2Binding) = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.ui.module

import android.view.MenuItem
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.tasks.RunAction
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.flash.ConsoleItem
import com.topjohnwu.superuser.CallbackList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class ActionViewModel : BaseViewModel() {

enum class State {
RUNNING, SUCCESS, FAILED
}

private val _state = MutableLiveData(State.RUNNING)
val state: LiveData<State> get() = _state
val running = state.map { it == State.RUNNING }

val items = ObservableArrayList<ConsoleItem>()
lateinit var args: ActionFragmentArgs

private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() {
override fun onAddElement(e: String?) {
e ?: return
items.add(ConsoleItem(e))
logItems.add(e)
}
}

fun startRunAction() {
viewModelScope.launch {
onResult(RunAction(args.id, outItems, logItems).exec())
}
}

private fun onResult(success: Boolean) {
_state.value = if (success) State.SUCCESS else State.FAILED
}

fun onMenuItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> savePressed()
}
return true
}

private fun savePressed() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format(
args.name,
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
SnackbarEvent(file.toString()).publish()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class LocalModuleRvItem(
override val layoutRes = R.layout.item_module_md2

val showNotice: Boolean
val showAction: Boolean
val noticeText: TextHolder

init {
Expand All @@ -34,6 +35,7 @@ class LocalModuleRvItem(
showNotice = zygiskUnloaded ||
(Info.isZygiskEnabled && isRiru) ||
(!Info.isZygiskEnabled && isZygisk)
showAction = item.hasAction && !showNotice
noticeText =
when {
zygiskUnloaded -> R.string.zygisk_module_unloaded.asText()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import androidx.databinding.Bindable
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule
Expand Down Expand Up @@ -95,6 +97,10 @@ class ModuleViewModel : AsyncLoadViewModel() {
}
}

fun runAction(id: String, name: String) {
MainDirections.actionActionFragment(id, name).navigate()
}

companion object {
private val uri = MutableLiveData<Uri?>()
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_action_md2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>

</vector>
68 changes: 68 additions & 0 deletions app/src/main/res/layout/fragment_action_md2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ActionViewModel" />

</data>

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/internal_action_bar_size"
app:layout_fitsSystemWindowsInsets="top"
tools:layout_marginTop="@dimen/internal_action_bar_size">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/flash_content"
scrollToLast="@{true}"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
app:fitsSystemWindowsInsets="start|end|bottom"
app:items="@{viewModel.items}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_console_md2" />

</HorizontalScrollView>

<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/close_btn"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:clickable="true"
android:enabled="true"
android:focusable="true"
android:text="@string/close"
android:textAllCaps="false"
android:textColor="?colorOnPrimary"
android:textStyle="bold"
app:backgroundTint="?colorPrimary"
app:icon="@drawable/ic_close_md2"
app:iconTint="?colorOnPrimary"
app:layout_fitsSystemWindowsInsets="bottom" />

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="top|bottom" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>
Loading

0 comments on commit 0fc898c

Please sign in to comment.