├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── resources.properties │ │ │ ├── font │ │ │ │ └── nya.ttf │ │ │ ├── drawable │ │ │ │ ├── taffy_no.png │ │ │ │ ├── taffy_ok.png │ │ │ │ ├── ic_console.xml │ │ │ │ ├── rounded_send_24.xml │ │ │ │ ├── outline_refresh_24.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── rounded_power_settings_new_24.xml │ │ │ │ ├── ic_cat_fish_bones.xml │ │ │ │ └── outline_lock_person_24.xml │ │ │ ├── color │ │ │ │ ├── status_card_background.xml │ │ │ │ └── home_item_card_background.xml │ │ │ ├── color-night │ │ │ │ ├── status_card_background.xml │ │ │ │ └── home_item_card_background.xml │ │ │ ├── mipmap │ │ │ │ ├── ic_reboot.xml │ │ │ │ ├── ic_lock_screen.xml │ │ │ │ ├── ic_shutdown.xml │ │ │ │ └── ic_launcher.xml │ │ │ ├── layout │ │ │ │ ├── fragment_home_recycler_view.xml │ │ │ │ ├── preference_recyclerview.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── dialog_custom_title.xml │ │ │ │ ├── dialog_progress.xml │ │ │ │ ├── item_list_segmented_viewholder.xml │ │ │ │ ├── preference_edit_text.xml │ │ │ │ └── activity_settings.xml │ │ │ ├── navigation │ │ │ │ └── nav_graph.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ ├── data_extraction_rules.xml │ │ │ │ └── preference_settings.xml │ │ │ ├── values │ │ │ │ ├── arrays.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ ├── values-night │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── values-zh-rCN │ │ │ │ └── strings.xml │ │ │ ├── values-zh-rTW │ │ │ │ └── strings.xml │ │ │ ├── values-ja │ │ │ │ └── strings.xml │ │ │ ├── values-ru │ │ │ │ └── strings.xml │ │ │ └── values-es │ │ │ │ └── strings.xml │ │ ├── aidl │ │ │ ├── github │ │ │ │ └── daisukikaffuchino │ │ │ │ │ └── rebootnya │ │ │ │ │ └── IShellService.aidl │ │ │ └── android │ │ │ │ └── os │ │ │ │ └── IPowerManager.aidl │ │ ├── java │ │ │ └── github │ │ │ │ └── daisukikaffuchino │ │ │ │ └── rebootnya │ │ │ │ ├── data │ │ │ │ ├── AppLocales.kt │ │ │ │ ├── HomeListItemData.kt │ │ │ │ └── ListItemEnum.kt │ │ │ │ ├── utils │ │ │ │ ├── RootUtil.kt │ │ │ │ ├── ShortcutHelper.kt │ │ │ │ ├── NyaUtil.kt │ │ │ │ ├── ShizukuUtil.kt │ │ │ │ └── NyaSettings.kt │ │ │ │ ├── NyaApplication.kt │ │ │ │ ├── shizuku │ │ │ │ ├── NyaShellService.kt │ │ │ │ ├── NyaShellManager.kt │ │ │ │ └── NyaRemoteProcess.kt │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── adapter │ │ │ │ └── HomeRecyclerAdapter.kt │ │ │ │ ├── SettingsActivity.kt │ │ │ │ ├── preference │ │ │ │ ├── EditTextPreference.kt │ │ │ │ └── IntegerSimpleMenuPreference.kt │ │ │ │ └── fragment │ │ │ │ ├── SettingsFragment.kt │ │ │ │ └── HomeFragment.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── github │ │ │ └── daisukikaffuchino │ │ │ └── rebootnya │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── github │ │ └── daisukikaffuchino │ │ └── rebootnya │ │ └── ExampleInstrumentedTest.kt ├── release │ └── baselineProfiles │ │ ├── 0 │ │ └── app-release.dm │ │ └── 1 │ │ └── app-release.dm ├── proguard-rules.pro └── build.gradle ├── image ├── banner.png ├── screenshot.png └── badges │ └── get-it-on-github.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── crowdin.yml ├── .gitignore ├── settings.gradle ├── gradle.properties ├── README.zh.md ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /image/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/image/banner.png -------------------------------------------------------------------------------- /image/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/image/screenshot.png -------------------------------------------------------------------------------- /app/src/main/res/font/nya.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/app/src/main/res/font/nya.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /image/badges/get-it-on-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/image/badges/get-it-on-github.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/taffy_no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/app/src/main/res/drawable/taffy_no.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/taffy_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/app/src/main/res/drawable/taffy_ok.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: app/src/main/res/values/strings.xml 3 | translation: app/src/main/res/values-%android_code%/strings.xml 4 | -------------------------------------------------------------------------------- /app/release/baselineProfiles/0/app-release.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/app/release/baselineProfiles/0/app-release.dm -------------------------------------------------------------------------------- /app/release/baselineProfiles/1/app-release.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daisukiKaffuChino/RebootNya/HEAD/app/release/baselineProfiles/1/app-release.dm -------------------------------------------------------------------------------- /app/src/main/aidl/github/daisukikaffuchino/rebootnya/IShellService.aidl: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya; 2 | 3 | interface IShellService { 4 | int exec(String cmd); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/status_card_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color-night/status_card_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 01 19:44:50 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/os/IPowerManager.aidl: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | interface IPowerManager { 4 | void reboot(boolean confirm, String reason, boolean wait); 5 | void shutdown(boolean confirm, String reason, boolean wait); 6 | void goToSleep(long time, int reason, int flags); 7 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_reboot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_lock_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_shutdown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/home_item_card_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color-night/home_item_card_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home_recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/test/java/github/daisukikaffuchino/rebootnya/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/data/AppLocales.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.data 2 | 3 | object AppLocales { 4 | val LOCALES: Array = arrayOf( 5 | "SYSTEM", 6 | "en", 7 | "es", 8 | "ja", 9 | "ru", 10 | "zh-CN", 11 | "zh-TW" 12 | ) 13 | val DISPLAY_LOCALES: Array = arrayOf( 14 | "SYSTEM", 15 | "en", 16 | "es", 17 | "ja", 18 | "ru", 19 | "zh-Hans", 20 | "zh-Hant" 21 | ) 22 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/data/HomeListItemData.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.data 2 | 3 | class HomeListItemData { 4 | var text: String? = null 5 | var checked: Boolean = false 6 | var indexInSection: Int = 0 7 | var sectionCount: Int = 0 8 | 9 | constructor(text: String?, indexInSection: Int, sectionCount: Int, checked: Boolean) { 10 | this.text = text 11 | this.indexInSection = indexInSection 12 | this.sectionCount = sectionCount 13 | this.checked = checked 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.aab 16 | *.apk 17 | output-metadata.json 18 | 19 | # IntelliJ 20 | *.iml 21 | .idea/ 22 | misc.xml 23 | deploymentTargetDropDown.xml 24 | render.experimental.xml 25 | 26 | # Keystore files 27 | *.jks 28 | *.keystore 29 | 30 | # Google Services (e.g. APIs or Firebase) 31 | google-services.json 32 | 33 | # Android Profiling 34 | *.hprof 35 | 36 | # macOS 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_send_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | plugins { 15 | id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' 16 | } 17 | dependencyResolutionManagement { 18 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 19 | repositories { 20 | google() 21 | mavenCentral() 22 | maven { url 'https://jitpack.io' } 23 | } 24 | } 25 | 26 | rootProject.name = "RebootNya" 27 | include ':app' 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/preference_recyclerview.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/github/daisukikaffuchino/rebootnya/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("github.daisukikaffuchino.rebootnya", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_custom_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/utils/RootUtil.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.utils 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import com.topjohnwu.superuser.Shell 6 | import github.daisukikaffuchino.rebootnya.R 7 | import java.lang.Boolean 8 | import kotlin.Exception 9 | import kotlin.String 10 | 11 | class RootUtil(private val context: Context){ 12 | fun runRootCommandWithResult(cmd: String): kotlin.Boolean { 13 | if (Boolean.FALSE == Shell.isAppGrantedRoot()) { 14 | Toast.makeText(context, R.string.no_root, Toast.LENGTH_SHORT).show() 15 | return false 16 | } else { 17 | val result = Shell.cmd(cmd).exec() 18 | return result.isSuccess 19 | } 20 | } 21 | 22 | fun requestRoot() { 23 | Toast.makeText(context, R.string.ksu_tip, Toast.LENGTH_SHORT).show() 24 | var process: Process? = null 25 | try { 26 | process = Runtime.getRuntime().exec("su") 27 | } catch (e: Exception) { 28 | e.fillInStackTrace() 29 | } finally { 30 | process?.destroy() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/NyaApplication.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import androidx.appcompat.app.AppCompatDelegate 7 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 8 | import rikka.material.app.LocaleDelegate 9 | import kotlin.system.exitProcess 10 | 11 | class NyaApplication : Application() { 12 | companion object { 13 | @SuppressLint("StaticFieldLeak") 14 | lateinit var context: Context 15 | } 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | context = applicationContext 20 | NyaSettings.initialize(applicationContext) 21 | LocaleDelegate.defaultLocale = NyaSettings.getLocale() 22 | AppCompatDelegate.setDefaultNightMode(NyaSettings.getNightMode(this)) 23 | } 24 | 25 | override fun onTrimMemory(level: Int) { 26 | super.onTrimMemory(level) 27 | if (level == TRIM_MEMORY_UI_HIDDEN) { 28 | // 所有 UI 不可见后杀死进程 29 | android.os.Process.killProcess(android.os.Process.myPid()) 30 | exitProcess(0) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_power_settings_new_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shizuku 5 | Root 6 | 7 | 8 | 9 | 1 10 | 2 11 | 12 | 13 | 14 | Process 15 | UserService 16 | 17 | 18 | 19 | 1 20 | 2 21 | 22 | 23 | 24 | @string/always_off 25 | @string/always_on 26 | @string/follow_system 27 | 28 | 29 | 30 | 1 31 | 2 32 | -1 33 | 34 | 35 | 36 | @string/style_classic_list 37 | @string/style_md_buttons 38 | 39 | 40 | 41 | 1 42 | 2 43 | 44 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cat_fish_bones.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/data/ListItemEnum.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.data 2 | 3 | import android.content.Context 4 | import github.daisukikaffuchino.rebootnya.R 5 | 6 | enum class ListItemEnum(val displayName: String) { 7 | LOCK_SCREEN("lock_screen"), 8 | REBOOT("reboot"), 9 | SOFT_REBOOT("soft_reboot"), 10 | SYSTEM_UI("system_ui"), 11 | RECOVERY("Recovery"), 12 | BOOTLOADER("Bootloader"), 13 | SAFE_MODE("safe_mode"), 14 | POWER_OFF("power_off"); 15 | 16 | fun getLocalizedDisplayName(context: Context): String { 17 | return when (this) { 18 | LOCK_SCREEN -> context.getString(R.string.lock_screen) 19 | REBOOT -> context.getString(R.string.reboot) 20 | SOFT_REBOOT -> context.getString(R.string.soft_reboot) 21 | SYSTEM_UI -> context.getString(R.string.system_ui) 22 | SAFE_MODE -> context.getString(R.string.safe_mode) 23 | POWER_OFF -> context.getString(R.string.power_off) 24 | else -> displayName 25 | } 26 | } 27 | 28 | companion object { 29 | fun fromLocalizedDisplayName(context: Context, displayName: String): ListItemEnum { 30 | return entries.find { it.getLocalizedDisplayName(context) == displayName } ?: LOCK_SCREEN//默认锁屏 31 | } 32 | 33 | fun fromDisplayName(displayName: String): ListItemEnum { 34 | return entries.find { it.displayName == displayName } ?: LOCK_SCREEN 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/shizuku/NyaShellService.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.shizuku 2 | 3 | import android.content.Context 4 | import android.os.RemoteException 5 | import android.util.Log 6 | import androidx.annotation.Keep 7 | import github.daisukikaffuchino.rebootnya.IShellService 8 | import java.util.concurrent.Callable 9 | import java.util.concurrent.Executors 10 | import java.util.concurrent.TimeUnit 11 | import java.util.concurrent.TimeoutException 12 | 13 | 14 | class NyaShellService : IShellService.Stub { 15 | constructor() { 16 | Log.i("UserService", "constructor") 17 | } 18 | 19 | @Keep 20 | constructor(context: Context) { 21 | Log.i("UserService", "constructor with Context: context=$context") 22 | } 23 | 24 | @Throws(RemoteException::class) 25 | override fun exec(cmd: String): Int { 26 | return executeCommandWithTimeout(cmd, 3000) 27 | } 28 | 29 | fun executeCommandWithTimeout(cmd: String, timeoutMs: Long): Int { 30 | val executor = Executors.newSingleThreadExecutor() 31 | val future = executor.submit(Callable { 32 | Runtime.getRuntime().exec(cmd).waitFor() 33 | }) 34 | 35 | return try { 36 | future.get(timeoutMs, TimeUnit.MILLISECONDS) 37 | } catch (e: TimeoutException) { 38 | Log.d("ShellService", "Timeout",e) 39 | future.cancel(true) 40 | -2 //超时 41 | } catch (e: Exception) { 42 | Log.d("ShellService", "Error",e) 43 | -1 44 | } finally { 45 | executor.shutdown() 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.google.android.material.color.DynamicColors 7 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 8 | import rikka.core.util.ResourceUtils 9 | import rikka.material.app.LocaleDelegate 10 | 11 | open class BaseActivity : AppCompatActivity() { 12 | private val localeDelegate by lazy { 13 | LocaleDelegate() 14 | } 15 | 16 | lateinit var themeChanged: String 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | localeDelegate.onCreate(this) 20 | if (NyaSettings.preferences.getBoolean("dynamic_color", false)) 21 | DynamicColors.applyToActivityIfAvailable(this) 22 | super.onCreate(savedInstanceState) 23 | themeChanged = computeThemeChanged() 24 | } 25 | 26 | override fun attachBaseContext(newBase: Context) { 27 | val configuration = newBase.resources.configuration 28 | localeDelegate.updateConfiguration(configuration) 29 | super.attachBaseContext(newBase.createConfigurationContext(configuration)) 30 | } 31 | 32 | override fun onResume() { 33 | super.onResume() 34 | if (localeDelegate.isLocaleChanged || 35 | themeChanged != computeThemeChanged() 36 | ) recreate() 37 | } 38 | 39 | private fun computeThemeChanged(): String { 40 | return NyaSettings.isUsingSystemColor() 41 | .toString() + ResourceUtils.isNightMode(getResources().configuration).toString() 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_list_segmented_viewholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 27 | 28 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_lock_person_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keepattributes Signature 23 | -keepattributes *Annotation* 24 | 25 | -keep class * extends android.os.IInterface { *; } 26 | -keep interface * extends android.os.IInterface { *; } 27 | 28 | # 保持 ShizukuUtil 类及其方法不被混淆 29 | -keep class github.daisukikaffuchino.rebootnya.utils.ShizukuUtil { 30 | private int shizukuProcess(java.lang.String[]); 31 | } 32 | 33 | # 保持 Shizuku 类的 service 字段不被混淆 34 | -keep class rikka.shizuku.Shizuku { 35 | private static moe.shizuku.server.IShizukuService service; 36 | } 37 | 38 | # 保持 IShizukuService 接口不被混淆 39 | -keep interface moe.shizuku.server.IShizukuService { *; } 40 | 41 | # 保持 NyaRemoteProcess 类不被混淆 42 | -keep class github.daisukikaffuchino.rebootnya.shizuku.NyaRemoteProcess { *; } 43 | 44 | -keep class com.topjohnwu.superuser.** { *; } 45 | -keep public class * extends android.app.Activity 46 | -keep public class * extends android.app.Application 47 | -keep public class * extends android.app.Service -------------------------------------------------------------------------------- /app/src/main/res/layout/preference_edit_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 33 | 34 | 39 | 40 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace 'github.daisukikaffuchino.rebootnya' 8 | compileSdk 36 9 | 10 | defaultConfig { 11 | applicationId "github.daisukikaffuchino.rebootnya" 12 | minSdk 28 13 | targetSdk 36 14 | versionCode 251207 15 | versionName "1.7.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildFeatures { 21 | viewBinding = true 22 | aidl = true 23 | } 24 | 25 | androidResources { 26 | generateLocaleConfig true 27 | } 28 | 29 | buildTypes { 30 | release { 31 | minifyEnabled true 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_21 38 | targetCompatibility JavaVersion.VERSION_21 39 | } 40 | 41 | kotlin { 42 | jvmToolchain(21) 43 | } 44 | } 45 | 46 | configurations.configureEach { 47 | exclude group: 'androidx.appcompat', module: 'appcompat' 48 | } 49 | 50 | dependencies { 51 | 52 | implementation libs.dev.appcompat 53 | implementation libs.material 54 | implementation libs.activity 55 | implementation libs.constraintlayout 56 | implementation libs.navigation.fragment.ktx 57 | implementation libs.preference 58 | 59 | implementation libs.core 60 | implementation libs.core.ktx 61 | implementation libs.provider 62 | implementation libs.api 63 | implementation libs.material.preference 64 | implementation libs.borderview 65 | implementation libs.recyclerview.ktx 66 | 67 | testImplementation libs.junit 68 | androidTestImplementation libs.ext.junit 69 | androidTestImplementation libs.espresso.core 70 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.13.1" 3 | api = "13.1.5" 4 | appcompatVersion = "1.6.1" 5 | borderview = "1.1.0" 6 | core = "6.0.0" 7 | gradlePlugin = "5.2.0" 8 | junit = "4.13.2" 9 | junitVersion = "1.3.0" 10 | espressoCore = "3.7.0" 11 | #noinspection GradleDependency 12 | appcompat = "1.6.1" 13 | material = "1.14.0-alpha07" 14 | activity = "1.12.1" 15 | constraintlayout = "2.2.1" 16 | materialPreference = "2.0.0" 17 | navigationUi = "2.9.6" 18 | kotlin = "2.2.21" 19 | coreKtx = "1.17.0" 20 | preference = "1.2.1" 21 | recyclerviewKtx = "1.3.2" 22 | 23 | 24 | [libraries] 25 | api = { module = "dev.rikka.shizuku:api", version.ref = "api" } 26 | borderview = { module = "dev.rikka.rikkax.widget:borderview", version.ref = "borderview" } 27 | core = { module = "com.github.topjohnwu.libsu:core", version.ref = "core" } 28 | dev-appcompat = { module = "dev.rikka.rikkax.appcompat:appcompat", version.ref = "appcompatVersion" } 29 | gradle-plugin = { module = "com.github.megatronking.stringfog:gradle-plugin", version.ref = "gradlePlugin" } 30 | junit = { group = "junit", name = "junit", version.ref = "junit" } 31 | ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 32 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 33 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 34 | activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } 35 | constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } 36 | material-preference = { module = "dev.rikka.rikkax.material:material-preference", version.ref = "materialPreference" } 37 | navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationUi" } 38 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 39 | preference = { module = "androidx.preference:preference", version.ref = "preference" } 40 | provider = { module = "dev.rikka.shizuku:provider", version.ref = "api" } 41 | recyclerview-ktx = { module = "dev.rikka.rikkax.recyclerview:recyclerview-ktx", version.ref = "recyclerviewKtx" } 42 | 43 | 44 | [plugins] 45 | android-application = { id = "com.android.application", version.ref = "agp" } 46 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/utils/ShortcutHelper.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.content.pm.ShortcutInfoCompat 6 | import androidx.core.content.pm.ShortcutManagerCompat 7 | import androidx.core.graphics.drawable.IconCompat 8 | import github.daisukikaffuchino.rebootnya.MainActivity 9 | import github.daisukikaffuchino.rebootnya.R 10 | 11 | class ShortcutHelper(private val context: Context) { 12 | 13 | data class ShortcutItem( 14 | val id: String, 15 | val shortLabel: String, 16 | val iconRes: Int, 17 | ) 18 | 19 | val items: List 20 | get() { 21 | return listOf( 22 | ShortcutItem( 23 | "lock_screen", 24 | context.getString(R.string.lock_screen), 25 | R.mipmap.ic_lock_screen, 26 | ), 27 | ShortcutItem( 28 | "power_off", 29 | context.getString(R.string.power_off), 30 | R.mipmap.ic_shutdown, 31 | ), 32 | ShortcutItem( 33 | "reboot", 34 | context.getString(R.string.reboot), 35 | R.mipmap.ic_reboot, 36 | ) 37 | ) 38 | } 39 | 40 | private fun buildShortcutInfo(item: ShortcutItem): ShortcutInfoCompat { 41 | val builder = ShortcutInfoCompat.Builder(context, item.id) 42 | .setShortLabel(item.shortLabel) 43 | .setIcon(IconCompat.createWithResource(context, item.iconRes)) 44 | .setIntent(Intent(context, MainActivity::class.java).apply { 45 | action = Intent.ACTION_RUN 46 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 47 | putExtra("extra", item.id) 48 | }) 49 | 50 | return builder.build() 51 | } 52 | 53 | fun setDynamicShortcuts(items: List) { 54 | val shortcuts = items.map { buildShortcutInfo(it) } 55 | ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) 56 | } 57 | 58 | fun requestPinShortcut(item: ShortcutItem) { 59 | val shortcut = buildShortcutInfo(item) 60 | ShortcutManagerCompat.requestPinShortcut(context, shortcut, null) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/utils/NyaUtil.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.text.Spanned 7 | import android.widget.Toast 8 | import androidx.core.net.toUri 9 | import androidx.core.text.HtmlCompat 10 | import github.daisukikaffuchino.rebootnya.R 11 | 12 | //杂项 13 | fun getAppVer(context: Context): String { 14 | val manager = context.packageManager 15 | try { 16 | val info = manager.getPackageInfo(context.packageName, 0) 17 | val versionName = info.versionName 18 | val versionCode = info.longVersionCode 19 | return "$versionName ($versionCode)" 20 | } catch (e: PackageManager.NameNotFoundException) { 21 | e.fillInStackTrace() 22 | } 23 | return "err" 24 | } 25 | 26 | fun CharSequence.toHtml(flags: Int = 0): Spanned { 27 | return HtmlCompat.fromHtml(this.toString(), flags) 28 | } 29 | 30 | fun openUrlLink(context: Context, url: String) { 31 | val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { 32 | addCategory(Intent.CATEGORY_BROWSABLE) 33 | } 34 | 35 | try { 36 | context.startActivity(intent) 37 | } catch (_: Exception) { 38 | Toast.makeText(context, R.string.no_app_found_open_link, Toast.LENGTH_SHORT).show() 39 | } 40 | } 41 | 42 | fun exclude(source: List, vararg excludes: T): List { 43 | val result = mutableListOf() 44 | for (item in source) { 45 | var shouldExclude = false 46 | for (ex in excludes) { 47 | if (item == ex) { 48 | shouldExclude = true 49 | break 50 | } 51 | } 52 | if (!shouldExclude) { 53 | result.add(item) 54 | } 55 | } 56 | return result 57 | } 58 | 59 | fun sendEmail(context: Context) { 60 | val intent = Intent(Intent.ACTION_SENDTO).apply { 61 | data = "mailto:".toUri() // 只处理邮件应用 62 | putExtra(Intent.EXTRA_EMAIL, arrayOf("konohatamira@outlook.com")) 63 | putExtra(Intent.EXTRA_SUBJECT, "RebootNya Feedback") 64 | } 65 | if (intent.resolveActivity(context.packageManager) != null) { 66 | context.startActivity(intent) 67 | } else { 68 | Toast.makeText(context, R.string.no_app_found_open_link, Toast.LENGTH_SHORT).show() 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 简体中文 2 | 3 | Banner 4 | 5 | # RebootNya 6 | 7 | [Get it on Github](https://github.com/daisukiKaffuChino/RebootNya/releases) 11 | 12 | [![GitHub release](https://img.shields.io/github/release/daisukiKaffuChino/RebootNya.svg?logo=github)](https://github.com/daisukiKaffuChino/RebootNya/releases/latest) 13 | [![Crowdin](https://badges.crowdin.net/rebootnya/localized.svg)](https://crowdin.com/project/rebootnya) 14 | [![GitHub license](https://img.shields.io/github/license/daisukiKaffuChino/RebootNya)](https://github.com/daisukiKaffuChino/RebootNya/blob/master/LICENSE) 15 | 16 | RebootNya 是一款简单的高级重启应用,同时支持 **Root** 和 **[Shizuku](https://shizuku.rikka.app/)** 工作方式! 17 | 18 | 已在一些设备上进行测试,在 Android 8.1 ~ 16 上运行良好。 19 | 20 | > 在一些 ROM 的默认启动器上可能无法正确显示透明背景,例如 ColorOS 15。解决方案是更换启动器,比如 Lawnchair 等。 21 | 22 | ## 使用 Intent 进行控制 23 | 24 | RebootNya 现在支持通过特定 Intent(意图) 启动和关闭应用程序,允许与外部自动化工具集成。将以下意图发送给类 `github.daisukikaffuchino.rebootnya.MainActivity` 以使用该功能。 25 | 26 | ```xml 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | ## 开发背景 36 | 37 | 我的一台旧手机电源键与音量键都损坏了,因此迫切需要一个兼顾美观、轻量和易用的高级重启应用。 38 | 39 | ## 贡献者 40 | 41 | 欢迎参与贡献!你可以 [提出 Issue](https://github.com/daisukiKaffuChino/RebootNya/issues/new/choose) 或为我们提交拉取请求 (PR)。 42 | 43 | 本项目的存在需要感谢所有的贡献者。 44 | 45 | ![Contributors](https://contrib.rocks/image?repo=daisukiKaffuChino/RebootNya) 46 | 47 | ## 开源许可 48 | 49 | - **[RebootNya](https://github.com/daisukiKaffuChino/RebootNya)**: Apache-2.0 license 50 | - **[Android Jetpack](https://github.com/androidx/androidx)**: Apache-2.0 license 51 | - **[Material Components for Android](https://github.com/material-components/material-components-android)**: Apache-2.0 license 52 | - **[libsu](https://github.com/topjohnwu/libsu)**: Apache-2.0 license 53 | - **[RikkaX](https://github.com/RikkaApps/RikkaX)**: MIT license 54 | - **[Shizuku-API](https://github.com/RikkaApps/Shizuku-API)**: Apache-2.0 license 55 | 56 |
57 | counter
58 |
59 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #FFB1C8 3 | #541D32 4 | #703348 5 | #FFD9E2 6 | #E3BDC6 7 | #422931 8 | #5B3F47 9 | #FFD9E2 10 | #EFBD94 11 | #48290B 12 | #613F20 13 | #FFDCC1 14 | #FFB4AB 15 | #690005 16 | #93000A 17 | #FFDAD6 18 | #191113 19 | #EFDFE1 20 | #191113 21 | #EFDFE1 22 | #514347 23 | #D5C2C6 24 | #9E8C90 25 | #514347 26 | #000000 27 | #EFDFE1 28 | #372E30 29 | #8C4A60 30 | #FFD9E2 31 | #3A071D 32 | #FFB1C8 33 | #703348 34 | #FFD9E2 35 | #2B151C 36 | #E3BDC6 37 | #5B3F47 38 | #FFDCC1 39 | #2E1500 40 | #EFBD94 41 | #613F20 42 | #191113 43 | #413739 44 | #140C0E 45 | #22191C 46 | #261D20 47 | #31282A 48 | #3C3235 49 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README.zh.md) 2 | 3 | Banner 4 | 5 | # RebootNya 6 | 7 | [Get it on Github](https://github.com/daisukiKaffuChino/RebootNya/releases) 11 | 12 | [![GitHub release](https://img.shields.io/github/release/daisukiKaffuChino/RebootNya.svg?logo=github)](https://github.com/daisukiKaffuChino/RebootNya/releases/latest) 13 | [![Crowdin](https://badges.crowdin.net/rebootnya/localized.svg)](https://crowdin.com/project/rebootnya) 14 | [![GitHub license](https://img.shields.io/github/license/daisukiKaffuChino/RebootNya)](https://github.com/daisukiKaffuChino/RebootNya/blob/master/LICENSE) 15 | 16 | RebootNya is a simple yet advanced reboot app that supports both **Root** and **[Shizuku](https://shizuku.rikka.app/)**! 17 | 18 | Tested on some devices and works well on Android 8.1 to 16. 19 | 20 | > On some ROMs’ default launcher, the transparent background may not display correctly (e.g., ColorOS 15). The solution is to switch to another launcher, such as Lawnchair. 21 | 22 | ## Intent-based Control 23 | 24 | RebootNya now supports launching and closing the app via specific intents, allowing integration with external automation tools. Send the following intent to class `github.daisukikaffuchino.rebootnya.MainActivity` to use the feature. 25 | 26 | ```xml 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | ## Development Background 36 | 37 | One of my old phones has both the power and volume buttons broken, so I urgently needed an advanced reboot app that is aesthetically pleasing, lightweight, and easy to use. 38 | 39 | ## Contributors 40 | 41 | Feel free to dive in! [Open an issue](https://github.com/daisukiKaffuChino/RebootNya/issues/new/choose) or submit pull requests (PRs). 42 | 43 | This project exists thanks to all the people who contribute. 44 | 45 | ![Contributors](https://contrib.rocks/image?repo=daisukiKaffuChino/RebootNya) 46 | 47 | ## Licenses 48 | 49 | - **[RebootNya](https://github.com/daisukiKaffuChino/RebootNya)**: Apache-2.0 license 50 | - **[Android Jetpack](https://github.com/androidx/androidx)**: Apache-2.0 license 51 | - **[Material Components for Android](https://github.com/material-components/material-components-android)**: Apache-2.0 license 52 | - **[libsu](https://github.com/topjohnwu/libsu)**: Apache-2.0 license 53 | - **[RikkaX](https://github.com/RikkaApps/RikkaX)**: MIT license 54 | - **[Shizuku-API](https://github.com/RikkaApps/Shizuku-API)**: Apache-2.0 license 55 | 56 |
57 | counter
58 |
59 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | #FCE4EC 6 | #ED789F 7 | #8C4A60 8 | #FFFFFF 9 | #FFD9E2 10 | #703348 11 | #74565F 12 | #FFFFFF 13 | #FFD9E2 14 | #5B3F47 15 | #7C5635 16 | #FFFFFF 17 | #FFDCC1 18 | #613F20 19 | #BA1A1A 20 | #FFFFFF 21 | #FFDAD6 22 | #93000A 23 | #FFF8F8 24 | #22191C 25 | #FFF8F8 26 | #22191C 27 | #F2DDE1 28 | #514347 29 | #837377 30 | #D5C2C6 31 | #000000 32 | #372E30 33 | #FDEDEF 34 | #FFB1C8 35 | #FFD9E2 36 | #3A071D 37 | #FFB1C8 38 | #703348 39 | #FFD9E2 40 | #2B151C 41 | #E3BDC6 42 | #5B3F47 43 | #FFDCC1 44 | #2E1500 45 | #EFBD94 46 | #613F20 47 | #E6D6D9 48 | #FFF8F8 49 | #FFFFFF 50 | #FFF0F2 51 | #FAEAED 52 | #F5E4E7 53 | #EFDFE1 54 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/shizuku/NyaShellManager.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.shizuku 2 | 3 | import android.content.ComponentName 4 | import android.content.ServiceConnection 5 | import android.os.IBinder 6 | import android.os.RemoteException 7 | import android.util.Log 8 | import github.daisukikaffuchino.rebootnya.IShellService 9 | import github.daisukikaffuchino.rebootnya.utils.ShizukuUtil 10 | import rikka.shizuku.Shizuku 11 | import rikka.shizuku.Shizuku.UserServiceArgs 12 | 13 | 14 | object NyaShellManager { 15 | const val TAG: String = "NyaShellManager" 16 | var mService: IShellService? = null 17 | private lateinit var userServiceConnection: ServiceConnection 18 | 19 | fun interface ControlCallback { 20 | fun onResult(exitCode: Int, message: String?) 21 | } 22 | 23 | private val userServiceArgs: UserServiceArgs = UserServiceArgs( 24 | ComponentName( 25 | "github.daisukikaffuchino.rebootnya", 26 | NyaShellService::class.java.name 27 | ) 28 | ) 29 | .daemon(false) 30 | .processNameSuffix("service") 31 | .debuggable(false) 32 | .version(1) 33 | 34 | 35 | fun bindService(shizukuUtil: ShizukuUtil, callback: ControlCallback) { 36 | if (!shizukuUtil.checkShizukuPermission() || mService != null) return 37 | userServiceConnection = object : ServiceConnection { 38 | override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) { 39 | callback.onResult(0, null) 40 | 41 | val res = StringBuilder() 42 | res.append("onServiceConnected: ").append(componentName.className).append('\n') 43 | 44 | if (binder != null && binder.pingBinder()) 45 | mService = IShellService.Stub.asInterface(binder) 46 | else 47 | res.append("invalid binder for ").append(componentName).append(" received") 48 | 49 | Log.i(TAG, res.toString().trim()) 50 | 51 | } 52 | 53 | override fun onServiceDisconnected(componentName: ComponentName) { 54 | Log.i(TAG, "onServiceDisconnected: \n" + componentName.className) 55 | } 56 | } 57 | Shizuku.bindUserService(userServiceArgs, userServiceConnection) 58 | } 59 | 60 | fun executeCommand(cmd: String?, callback: ControlCallback) { 61 | // val serviceVersion = Shizuku.peekUserService(userServiceArgs, userServiceConnection) 62 | // Log.i(TAG, "V$serviceVersion") 63 | mService?.let { service -> 64 | try { 65 | val exitCode = service.exec(cmd) 66 | callback.onResult(exitCode, if (exitCode == 0) "Success" else "Fail: $exitCode") 67 | } catch (e: RemoteException) { 68 | callback.onResult(-1, e.message.toString()) 69 | } 70 | } ?: run { 71 | callback.onResult(-1, "waiting?")//没准备完毕 72 | } 73 | } 74 | 75 | fun unbindService() { 76 | if (Shizuku.pingBinder() && ::userServiceConnection.isInitialized && mService != null) { 77 | Shizuku.unbindUserService(userServiceArgs, userServiceConnection, true) 78 | mService = null 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 确认 3 | 关闭 4 | 设置 5 | 锁屏 6 | 重启 7 | 软重启 8 | 安全模式 9 | 系统界面 10 | 关机 11 | 没有 root 权限 12 | 命令执行失败 13 | 工作中 14 | 未工作 15 | 点击进行授权 16 | 工作模式 17 | 动态取色 18 | 至少需要 Android 12 19 | Shizuku 权限被拒绝 20 | Shizuku 版本过低 21 | Shizuku 未运行 22 | Shizuku 服务连接错误 23 | 不支持 24 | 若你是 KernelSU 用户,请前往管理器手动授权 25 | Shizuku(ADB) 的权限可能不足 26 | 执行自定义 Shell 27 | 你没有获得权限 28 | Shizuku 指令执行模式 29 | UserService 尚未初始化 30 | 启动 31 | 关于 UserService 模式 32 | 高级 33 | 关于 34 | 界面外观 35 | 深色主题 36 | 语言 37 | 以 Apache-2.0 许可证授权 38 | 开发者 39 | Process 模式已弃用,使用的函数计划从 Shizuku API 14 中删除。UserService 类似于绑定服务,是官方推荐的实现功能的最佳方式。\n\n但是,有报告称部分系统(比如MIUI)下可能无法正确启动 UserService,因此 RebootNya 仍默认使用旧模式。\n\n您可以主动尝试启用 UserService 并测试功能是否可用,如果一切正常的话,它将带来更好的性能和稳定性。 40 | 跟随系统 41 | 总是开启 42 | 总是关闭 43 | 隐藏可能无法使用的选项 44 | 由于 ADB 命令的权限不足,部分选项可能无法使用。开启后,Root 模式和使用 Root 激活的 Shizuku 模式不受影响。 45 | 和我联系 46 | 电子邮件 47 | 未找到可打开链接的应用 48 | 在部分系统上,SystemUI 会被系统服务自动拉起,所以结束后很快会重启。 49 | 主界面样式 50 | 经典列表 51 | 质感设计按钮 52 | 请求固定 Shortcuts 到桌面 53 | 杂项 54 | 清除已创建 Shortcuts 55 | 下次启动时会重新创建 56 | 已清除 57 | 参与翻译 58 | 帮助我们将 %s 翻译至您的语言 59 | 执行中… 60 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 確認 3 | 關閉 4 | 設定 5 | 鎖定螢幕 6 | 重新啟動 7 | 軟重啟 8 | 安全模式 9 | 系統介面 10 | 關機 11 | 沒有 root 權限 12 | 指令執行失敗 13 | 運作中 14 | 未運作 15 | 點擊以進行授權 16 | 運作模式 17 | 動態取色 18 | 至少需要 Android 12 19 | Shizuku 權限已被拒絕 20 | Shizuku 版本過舊 21 | Shizuku 未啟動 22 | Shizuku 服務連線錯誤 23 | 不支援 24 | 若你是 KernelSU 使用者,請前往管理器手動授權 25 | Shizuku(ADB) 的許可權可能不足 26 | 執行自定義 Shell 27 | 你沒有取得許可權 28 | Shizuku 指令執行模式 29 | UserService 尚未初始化 30 | 啟動 31 | 關於 UserService 模式 32 | 高階 33 | 關於 34 | 介面外觀 35 | 深色主題 36 | 語言 37 | 以 Apache-2.0 許可證授權 38 | 開發者 39 | Process 模式已棄用,相關函式計劃在 Shizuku API 14 中移除。UserService 類似於綁定服務,是官方推薦的最佳實現方式。\n\n但是,有報告指出在某些 ROM(例如 MIUI)下,UserService 可能無法正確啟動。因此,RebootNya 仍預設使用舊的模式。\n\n您可以手動嘗試啟用 UserService 並測試功能是否可用。若一切正常,將能帶來更佳的效能與穩定性。 40 | 跟隨系統 41 | 總是開啟 42 | 總是關閉 43 | 隱藏可能無法使用的選項 44 | 由於 ADB 指令權限不足,部分選項可能無法使用。啟用後,Root 模式以及以 Root 啟動的 Shizuku 模式將不受影響。 45 | 聯絡我 46 | 電子郵件 47 | 找不到可開啟連結的應用程式 48 | "在部分 ROM 上,SystemUI 會被系統服務自動啟動,所以結束後很快就會重新啟動。" 49 | 主介面風格 50 | 經典列表 51 | Material Design 按鈕 52 | 請求將捷徑固定到主畫面 53 | 雜項 54 | 清除已建立的捷徑 55 | 下次啟動時會重新建立 56 | 已清除 57 | 參與翻譯 58 | 協助我們將 %s 翻譯至您的語言 59 | 执行中… 60 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/shizuku/NyaRemoteProcess.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.shizuku 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Parcel 5 | import android.os.ParcelFileDescriptor 6 | import android.os.Parcelable 7 | import android.os.RemoteException 8 | import android.util.ArraySet 9 | import android.util.Log 10 | import moe.shizuku.server.IRemoteProcess 11 | import java.io.InputStream 12 | import java.io.OutputStream 13 | import java.util.Collections 14 | 15 | @SuppressLint("ParcelCreator") 16 | class NyaRemoteProcess : Process, Parcelable { 17 | private var remote: IRemoteProcess? 18 | private var os: OutputStream? = null 19 | private var `is`: InputStream? = null 20 | 21 | constructor(remote: IRemoteProcess?) { 22 | this.remote = remote 23 | try { 24 | this.remote!!.asBinder().linkToDeath({ 25 | this.remote = null 26 | Log.v(TAG, "remote process is dead") 27 | CACHE.remove(this) 28 | }, 0) 29 | } catch (e: RemoteException) { 30 | Log.e(TAG, "linkToDeath", e) 31 | } 32 | 33 | // The reference to the binder object must be hold 34 | CACHE.add(this) 35 | } 36 | 37 | override fun getOutputStream(): OutputStream { 38 | if (os == null) { 39 | try { 40 | os = ParcelFileDescriptor.AutoCloseOutputStream(remote!!.outputStream) 41 | } catch (e: RemoteException) { 42 | throw RuntimeException(e) 43 | } 44 | } 45 | return os!! 46 | } 47 | 48 | override fun getInputStream(): InputStream { 49 | if (`is` == null) { 50 | try { 51 | `is` = ParcelFileDescriptor.AutoCloseInputStream(remote!!.inputStream) 52 | } catch (e: RemoteException) { 53 | throw RuntimeException(e) 54 | } 55 | } 56 | return `is`!! 57 | } 58 | 59 | override fun getErrorStream(): InputStream { 60 | try { 61 | return ParcelFileDescriptor.AutoCloseInputStream(remote!!.errorStream) 62 | } catch (e: RemoteException) { 63 | throw RuntimeException(e) 64 | } 65 | } 66 | 67 | @Throws(InterruptedException::class) 68 | override fun waitFor(): Int { 69 | try { 70 | return remote!!.waitFor() 71 | } catch (e: RemoteException) { 72 | throw RuntimeException(e) 73 | } 74 | } 75 | 76 | override fun exitValue(): Int { 77 | try { 78 | return remote!!.exitValue() 79 | } catch (e: RemoteException) { 80 | throw RuntimeException(e) 81 | } 82 | } 83 | 84 | override fun destroy() { 85 | try { 86 | remote!!.destroy() 87 | } catch (e: RemoteException) { 88 | throw RuntimeException(e) 89 | } 90 | } 91 | 92 | override fun describeContents(): Int { 93 | return 0 94 | } 95 | 96 | override fun writeToParcel(dest: Parcel, flags: Int) { 97 | dest.writeStrongBinder(remote!!.asBinder()) 98 | } 99 | 100 | companion object { 101 | private val CACHE: MutableSet = 102 | Collections.synchronizedSet( 103 | ArraySet() 104 | ) 105 | 106 | private const val TAG = "NyaRemoteProcess" 107 | 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import androidx.core.content.edit 6 | import androidx.core.graphics.drawable.toDrawable 7 | import github.daisukikaffuchino.rebootnya.databinding.ActivityMainBinding 8 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 9 | import github.daisukikaffuchino.rebootnya.utils.ShortcutHelper 10 | import rikka.shizuku.Shizuku 11 | import kotlin.properties.Delegates 12 | 13 | import android.content.Intent 14 | 15 | import androidx.core.view.WindowCompat 16 | 17 | class MainActivity : BaseActivity() { 18 | companion object { 19 | const val ACTION_LAUNCH = "github.daisukikaffuchino.rebootnya.action.LAUNCH" 20 | const val ACTION_CLOSE = "github.daisukikaffuchino.rebootnya.action.CLOSE" 21 | const val ACTION_TOGGLE = "github.daisukikaffuchino.rebootnya.action.TOGGLE" 22 | 23 | var listFilterStatus by Delegates.notNull() 24 | fun checkListFilterStatus(): Boolean { 25 | return Shizuku.pingBinder() 26 | && Shizuku.getUid() == 2000 27 | && NyaSettings.getWorkMode() == NyaSettings.MODE.SHIZUKU 28 | && NyaSettings.getIsHideUnavailableOptions() 29 | } 30 | } 31 | 32 | var uiStyleChanged by Delegates.notNull() 33 | var workModeChanged by Delegates.notNull() 34 | var isActivityVisible = false 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | listFilterStatus = checkListFilterStatus() 39 | uiStyleChanged = NyaSettings.getMainInterfaceStyle() 40 | workModeChanged = NyaSettings.getWorkMode() 41 | 42 | val window = getWindow() 43 | window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) 44 | if (window != null) { 45 | WindowCompat.setDecorFitsSystemWindows(window, false) 46 | } 47 | 48 | val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater) 49 | setContentView(binding.getRoot()) 50 | 51 | if(!NyaSettings.preferences.getBoolean("isShortcutCreated",false)){ 52 | val shortcutHelper= ShortcutHelper(this) 53 | shortcutHelper.setDynamicShortcuts(shortcutHelper.items) 54 | NyaSettings.preferences.edit { putBoolean("isShortcutCreated", true) } 55 | } 56 | 57 | handleIntent(intent) 58 | } 59 | 60 | override fun onNewIntent(intent: Intent) { 61 | super.onNewIntent(intent) 62 | handleIntent(intent) 63 | } 64 | 65 | private fun handleIntent(intent: Intent?) { 66 | if (intent == null) return 67 | when (intent.action) { 68 | ACTION_LAUNCH -> { 69 | // Already launched, nothing specific needed as activity is brought to front 70 | } 71 | ACTION_CLOSE -> { 72 | finish() 73 | } 74 | ACTION_TOGGLE -> { 75 | if (isActivityVisible) { 76 | finish() 77 | } 78 | } 79 | } 80 | } 81 | 82 | override fun onResume() { 83 | super.onResume() 84 | isActivityVisible = true 85 | if (listFilterStatus != checkListFilterStatus() || 86 | uiStyleChanged != NyaSettings.getMainInterfaceStyle() || 87 | workModeChanged != NyaSettings.getWorkMode() 88 | ) 89 | recreate() 90 | } 91 | 92 | override fun onStop() { 93 | super.onStop() 94 | isActivityVisible = false 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 実行 3 | 閉じる 4 | 設定 5 | ロック画面 6 | 再起動 7 | ソフトリブート 8 | セーフモード 9 | システム UI 10 | 電源を切る 11 | ルート権限なし 12 | コマンドの実行に失敗しました 13 | 稼働中 14 | 停止中 15 | タップして権限を要求する 16 | 動作方法 17 | ダイナミック カラー 18 | Android 12 以上が対象です 19 | Shizukuの権限が付与されていません 20 | Shizukuのバージョンが古いです 21 | Shizukuが動作していません 22 | Shizuku サービスとの接続でエラーが発生しました 23 | サポートされていません 24 | KernelSU を使用している場合は、マネージャーで手動で権限を付与してください 25 | Shizuku(ADB)で付与されている権限が不足している可能性があります 26 | カスタムシェルを実行 27 | 権限を取得していません 28 | Shizuku コマンドの実行方法 29 | UserService は初期化されていません 30 | 起動 31 | UserService モードについて 32 | 上級者向け機能(より高度な操作) 33 | アプリ情報 34 | インターフェースの外観 35 | テーマ 36 | 言語 37 | Apache-2.0 Licensed 38 | 開発者 39 | "Processモードは廃止され、関連する機能はShizuku API 14から削除される予定です。Bound Servicesに似たUserServiceが、機能実装の公式に推奨される方法です。\n\nしかし、一部のROM(例えばMIUI)では、UserServiceが正しく起動しないという報告があります。そのため、RebootNyaではデフォルトで旧モードを使用しています。\n\nUserServiceを手動で有効にして、機能が正常に動作するかどうかをテストすることができます。すべてが正常に動作すれば、パフォーマンスと安定性が向上します。" 40 | システムのテーマ設定に従う 41 | ダークテーマ 42 | ホワイトテーマ 43 | 利用できないオプションを非表示にします 44 | ADB コマンドの権限が不十分なため、一部のオプションが利用できない場合があります。有効にしても、Root モードおよび Root 権限で起動した Shizuku モードには影響しません。 45 | 連絡先 46 | Email 47 | リンクを開くアプリが見つかりません 48 | 一部のROMでは、システムサービスによってSystemUIが自動的に再起動されるため、終了後すぐに再起動されます。 49 | メインインターフェースのスタイル 50 | クラシックリスト 51 | マテリアルデザイン ボタン 52 | ショートカットを作成する 53 | その他 54 | ショートカットを削除する 55 | アプリアイコン長押しで表示されるショートカットを削除する 56 | 削除しました 57 | 翻訳に参加する 58 | %s をあなたの言語に翻訳する手伝いをしてください 59 | 実行中… 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preference_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 18 | 21 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 41 | 46 | 53 | 54 | 55 | 56 | 59 | 63 | 64 | 65 | 66 | 71 | 74 | 75 | 76 | 77 | 81 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 19 | 20 | 30 | 31 | 46 | 47 | 56 | 57 | 62 | 63 | 69 | 70 | 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/adapter/HomeRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.adapter 2 | 3 | import android.graphics.Rect 4 | import android.util.TypedValue 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.google.android.material.card.MaterialCardView 11 | import com.google.android.material.listitem.ListItemViewHolder 12 | import github.daisukikaffuchino.rebootnya.NyaApplication 13 | import github.daisukikaffuchino.rebootnya.R 14 | import github.daisukikaffuchino.rebootnya.data.HomeListItemData 15 | 16 | class HomeRecyclerAdapter( 17 | private val items: MutableList, 18 | private val listener: OnItemSelectedListener? = null 19 | ) : RecyclerView.Adapter() { 20 | 21 | private var selectedPosition = 0 22 | private var recyclerView: RecyclerView? = null 23 | 24 | init { 25 | items.forEachIndexed { index, item -> 26 | if (item.checked) 27 | selectedPosition = index 28 | } 29 | } 30 | 31 | override fun onAttachedToRecyclerView(rv: RecyclerView) { 32 | super.onAttachedToRecyclerView(rv) 33 | recyclerView = rv 34 | } 35 | 36 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 37 | val view = LayoutInflater.from(parent.context) 38 | .inflate(R.layout.item_list_segmented_viewholder, parent, false) 39 | return CustomItemViewHolder(view) 40 | } 41 | 42 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 43 | val data = getItemAt(position) 44 | (holder as CustomItemViewHolder).bind(data) 45 | } 46 | 47 | override fun getItemCount(): Int = items.size 48 | 49 | fun getItemAt(i: Int): HomeListItemData = items[i] 50 | 51 | private inner class CustomItemViewHolder(itemView: View) : 52 | ListItemViewHolder(itemView) { 53 | 54 | private val textView: TextView = itemView.findViewById(R.id.home_list_item_text) 55 | val cardView: MaterialCardView = itemView.findViewById(R.id.home_list_item_card) 56 | 57 | fun bind(data: HomeListItemData) { 58 | super.bind(data.indexInSection, data.sectionCount) 59 | textView.text = data.text 60 | 61 | //Log.i("xxx",data.text+" "+data.checked) 62 | cardView.isChecked = data.checked 63 | 64 | cardView.setOnClickListener { 65 | val old = selectedPosition 66 | val now = bindingAdapterPosition 67 | if (now == RecyclerView.NO_POSITION) return@setOnClickListener 68 | if (old == now) return@setOnClickListener // 点击同一个不处理 69 | 70 | // 1) 更新数据模型 71 | items[old].checked = false 72 | items[now].checked = true 73 | selectedPosition = now 74 | 75 | // 2) 当前点击的项:播放选中动画 76 | cardView.toggle() 77 | 78 | // 3) 旧的选中项:可见时用 toggle() 播放取消动画;否则刷新 79 | recyclerView?.let { rv -> 80 | val oldVH = rv.findViewHolderForAdapterPosition(old) 81 | if (oldVH is CustomItemViewHolder) { 82 | oldVH.cardView.toggle() 83 | } else { 84 | notifyItemChanged(old) 85 | } 86 | } ?: notifyItemChanged(old) 87 | 88 | // 4) 通知外部监听器 89 | listener?.onItemSelected(now, items[now]) 90 | } 91 | } 92 | } 93 | 94 | class MarginItemDecoration() : RecyclerView.ItemDecoration() { 95 | private val itemMargin: Int = TypedValue.applyDimension( 96 | TypedValue.COMPLEX_UNIT_DIP, 97 | 2f, 98 | NyaApplication.context.resources.displayMetrics 99 | ).toInt() 100 | 101 | override fun getItemOffsets( 102 | outRect: Rect, 103 | view: View, 104 | parent: RecyclerView, 105 | state: RecyclerView.State 106 | ) { 107 | val position = parent.getChildAdapterPosition(view) 108 | if (position != state.itemCount - 1) { 109 | outRect.bottom = itemMargin 110 | } 111 | } 112 | } 113 | 114 | fun interface OnItemSelectedListener { 115 | fun onItemSelected(position: Int, item: HomeListItemData) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RebootNya 3 | Confirm 4 | Close 5 | Settings 6 | Lock Screen 7 | Reboot 8 | Soft Reboot 9 | Safe Mode 10 | System UI 11 | Power Off 12 | No root access 13 | Command execution failed 14 | Working 15 | Not working 16 | Tap to request permission 17 | Work Mode 18 | Dynamic Color 19 | Requires at least Android 12 20 | Shizuku permission denied 21 | Shizuku version is too old 22 | Shizuku is not running 23 | Shizuku service connection error 24 | Not supported 25 | If you are using KernelSU, please grant permission manually in the manager 26 | The permissions of Shizuku(ADB) may be insufficient 27 | Execute custom shell 28 | You haven\'t obtained the permissions 29 | Shizuku command execution 30 | UserService is not initialized 31 | Launch 32 | About UserService mode 33 | Advanced 34 | About 35 | Interface Appearance 36 | Dark theme 37 | Language 38 | Apache-2.0 Licensed 39 | Developer 40 | "The Process mode has been deprecated, and the related function is scheduled to be removed from Shizuku API 14. UserService, which is similar to Bound Services, is the officially recommended way to implement features.\n\nHowever, there have been reports that on some ROM (such as MIUI), UserService may fail to start correctly. Therefore, RebootNya still uses the old mode by default.\n\nYou can manually try enabling the UserService and test whether the features work properly. If everything runs good, it will provide better performance and stability." 41 | Follow system 42 | Always on 43 | Always off 44 | Hide unavailable options 45 | Due to insufficient permissions of ADB commands, some options may not be available. When enabled, Root mode and Shizuku mode activated with Root permissions will not be affected. 46 | Contact Me 47 | Email 48 | No app found to open the link 49 | Bilibili 50 | On some ROM, SystemUI will be automatically restarted by system services, so it will relaunch shortly after being terminated. 51 | Main interface style 52 | Classic list 53 | Material Design buttons 54 | Request to pin Shortcuts to launcher 55 | Others 56 | Clear created shortcuts 57 | Will be recreated at next launch 58 | Cleared 59 | Participate in translation 60 | Help us translate %s into your language 61 | Executing… 62 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | 52 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Подтвердить 4 | Закрыть 5 | Настройки 6 | Экран блокировки 7 | Перезагрузка 8 | Мягкая перезагрузка 9 | Безопасный режим 10 | Системная оболочка 11 | Выключение 12 | Нет root-прав 13 | Не удалось выполнить команду 14 | Работает 15 | Не работает 16 | Нажмите, чтобы получить разрешение 17 | Режим работы 18 | Динамический цвет 19 | Требуется как минимум Android 12 20 | Нет разрешения для работы приложения Shizuku 21 | Версия Shizuku устарела 22 | Shizuku не запущен 23 | Ошибка при подключении к сервису Shizuku 24 | Не поддерживается 25 | Если вы используете KernelSU, пожалуйста, дайте разрешение вручную в менеджере 26 | Разрешений Shizuku(ADB) может быть недостаточно 27 | Выполнить пользовательскую команду Shell 28 | Вы не получили разрешения 29 | Режим выполнения команд через Shizuku 30 | UserService не инициализирован 31 | Запуск 32 | О режиме UserService 33 | Дополнительно 34 | О приложении 35 | Внешний вид интерфейса 36 | Тёмная тема 37 | Язык 38 | Лицензия Apache-2.0 39 | Разработчик 40 | "Режим Process устарел, и соответствующая функция будет удалена из Shizuku API 14. UserService, аналогичный Bound Services, является официально рекомендуемым способом реализации функций.\n\nОднако имеются сообщения о том, что в некоторых прошивках (например, MIUI) UserService может запускаться некорректно. Поэтому RebootNya по-прежнему использует старый режим по умолчанию.\n\nВы можете попробовать вручную включить UserService и проверить корректность работы функций. Если всё будет работать корректно, это обеспечит лучшую производительность и стабильность." 41 | По умолчанию 42 | Всегда включён 43 | Всегда выключен 44 | Скрыть недоступные параметры 45 | Из-за недостатка прав доступа для команд ADB некоторые параметры могут быть недоступны. При включении этого параметра режимы Root и Shizuku, активированные с правами Root, не будут затронуты. 46 | Свяжитесь со мной 47 | Электронная почта 48 | Не найдено приложения, чтобы открыть ссылку 49 | В некоторых прошивках SystemUI автоматически перезапускается системными службами, поэтому он перезапустится вскоре после завершения. 50 | Стиль главного интерфейса 51 | Классический список 52 | Кнопки в стиле Material Design 53 | Запрос на закрепление ярлыков на панели запуска 54 | Остальные 55 | Удалить созданные ярлыки 56 | Будет воссоздан при следующем запуске 57 | Очищено 58 | Участвовать в переводе 59 | Помогите перевести %s на ваш язык 60 | Выполнение… 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Confirmar 3 | Cerrar 4 | Ajustes 5 | Bloquear pantalla 6 | Reiniciar 7 | Reinicio suave 8 | Modo seguro 9 | Interfaz del sistema 10 | Apagar 11 | No hay acceso root 12 | Error al ejecutar comando 13 | Funcionando 14 | No está funcionando 15 | Toca para solicitar permiso 16 | Modo de trabajo 17 | Colores dinámicos 18 | Necesitas Android 12 o superior 19 | Permiso de Shizuku denegado 20 | Tu versión de Shizuku es muy vieja 21 | Shizuku no está en ejecución 22 | Error de conexión al servicio Shizuku 23 | No soportado 24 | Si usas KernelSU, concede el permiso manualmente en el gestor 25 | Los permisos de Shizuku (ADB) podrían ser insuficientes 26 | Ejecutar shell personalizado 27 | No tienes los permisos necesarios 28 | Ejecución de comandos con Shizuku 29 | UserService no está inicializado 30 | Lanzar 31 | Acerca del modo UserService 32 | Avanzado 33 | Acerca de 34 | Apariencia de la interfaz 35 | Tema oscuro 36 | Idioma 37 | Licenciado bajo Apache-2.0 38 | Desarrollador 39 | "El modo Process está obsoleto y su función relacionada será eliminada en Shizuku API 14. UserService, que funciona de forma similar a los servicios vinculados (Bound Services), es el método oficialmente recomendado para implementar funciones.\n\nSin embargo, se han reportado casos en algunas ROM (como MIUI) donde UserService puede fallar al iniciarse correctamente. Por ello, RebootNya aún utiliza el modo antiguo por defecto.\n\nPuedes intentar habilitar manualmente UserService y comprobar si las funciones se ejecutan correctamente. Si todo funciona bien, ofrecerá un mejor rendimiento y mayor estabilidad." 40 | Seguir sistema 41 | Siempre encendida 42 | Siempre apagada 43 | Ocultar opciones no disponibles 44 | Algunos comandos ADB no tienen permisos suficientes, por lo que ciertas opciones podrían no estar disponibles. Si se activa esta opción, el modo Root y el modo Shizuku con permisos de superusuario no se verán afectados. 45 | Contáctame 46 | Correo electrónico 47 | No se ha encontrado una aplicación para abrir el link 48 | En algunas ROM, el SystemUI será reiniciado automáticamente por los servicios del sistema, por lo que se volverá a iniciar poco después de ser finalizado. 49 | Estilo de la interfaz principal 50 | Lista clásica 51 | Botones con diseño Material 52 | Solicitar anclar accesos directos al launcher 53 | Otros 54 | Borrar accesos directos creados 55 | Se volverán a crear en el próximo inicio 56 | Borrado 57 | Participar en la traducción 58 | Ayúdanos a traducir %s a tu idioma 59 | Ejecutando… 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/utils/ShizukuUtil.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.IPowerManager 6 | import android.os.RemoteException 7 | import android.util.Log 8 | import android.widget.Toast 9 | import github.daisukikaffuchino.rebootnya.R 10 | import github.daisukikaffuchino.rebootnya.shizuku.NyaRemoteProcess 11 | import github.daisukikaffuchino.rebootnya.shizuku.NyaShellManager 12 | import moe.shizuku.server.IShizukuService 13 | import rikka.shizuku.Shizuku 14 | import rikka.shizuku.ShizukuBinderWrapper 15 | import rikka.shizuku.SystemServiceHelper 16 | import java.util.concurrent.CountDownLatch 17 | import java.util.concurrent.atomic.AtomicInteger 18 | 19 | class ShizukuUtil(private val context: Context) { 20 | fun checkShizukuPermission(): Boolean { 21 | if (!Shizuku.pingBinder()) return false 22 | 23 | if (Shizuku.isPreV11()) { 24 | Toast.makeText(context, R.string.shizuku_too_old, Toast.LENGTH_SHORT) 25 | .show() 26 | return false 27 | } 28 | 29 | if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { 30 | return true 31 | } else if (Shizuku.shouldShowRequestPermissionRationale()) { 32 | Toast.makeText(context, R.string.shizuku_denied, Toast.LENGTH_SHORT) 33 | .show() 34 | return false 35 | } else { 36 | return false 37 | } 38 | } 39 | 40 | fun shizukuReboot(reason: String?) { 41 | val powerManager = IPowerManager.Stub.asInterface( 42 | ShizukuBinderWrapper(SystemServiceHelper.getSystemService("power")) 43 | ) 44 | try { 45 | powerManager.reboot(false, reason, false) 46 | } catch (e: Exception) { 47 | Log.e("ShizukuUtil","reboot", e) 48 | Toast.makeText(context, "Error:" + e.message, Toast.LENGTH_LONG).show() 49 | } 50 | } 51 | 52 | private fun shizukuProcess(cmd: Array?): Int { 53 | try { 54 | val privateField = Shizuku::class.java.getDeclaredField("service") 55 | privateField.isAccessible = true 56 | val value = privateField.get(null) as IShizukuService 57 | try { 58 | checkNotNull(value) 59 | val process: Process = NyaRemoteProcess(value.newProcess(cmd, null, null)) 60 | return process.waitFor() 61 | } catch (e: RemoteException) { 62 | e.fillInStackTrace() 63 | Toast.makeText(context, "Error:" + e.message, Toast.LENGTH_LONG) 64 | .show() 65 | } 66 | } catch (e: Exception) { 67 | e.fillInStackTrace() 68 | Toast.makeText(context, "Error:" + e.message, Toast.LENGTH_LONG).show() 69 | } 70 | return -3 71 | } 72 | 73 | fun runShizukuCommand(cmd: Array, checkUid: Boolean): Int { 74 | val cmdString = cmd.joinToString(" ") 75 | if (checkUid && Shizuku.getUid() == 2000) Toast.makeText( 76 | context, 77 | R.string.shizuku_permission_insufficient, 78 | Toast.LENGTH_SHORT 79 | ).show() 80 | 81 | val mode = NyaSettings.getShizukuShellMode() 82 | when (mode) { 83 | NyaSettings.MODE.PROCESS -> { 84 | val exitCode = shizukuProcess(cmd) 85 | if (exitCode != 0) Toast.makeText( 86 | context, 87 | R.string.exec_fail, 88 | Toast.LENGTH_SHORT 89 | ) 90 | .show() 91 | return exitCode 92 | } 93 | 94 | else -> { 95 | if (NyaShellManager.mService == null) { 96 | Toast.makeText( 97 | context, 98 | R.string.user_service_not_initialized, 99 | Toast.LENGTH_SHORT 100 | ).show() 101 | return -2 102 | } 103 | return executeCommandSync(cmdString) 104 | } 105 | } 106 | 107 | } 108 | 109 | private fun executeCommandSync(cmdString: String): Int { 110 | val latch = CountDownLatch(1) 111 | val result = AtomicInteger(-1) 112 | 113 | NyaShellManager.executeCommand(cmdString) { exitCode, message -> 114 | if (exitCode != 0) Toast.makeText( 115 | context, 116 | "Message: $message", 117 | Toast.LENGTH_SHORT 118 | ) 119 | .show() 120 | result.set(exitCode) 121 | latch.countDown() //完成 122 | } 123 | 124 | try { 125 | latch.await() //阻塞 126 | return result.get() 127 | } catch (e: InterruptedException) { 128 | Thread.currentThread().interrupt() 129 | Log.e("ShizukuUtil","interruptExec", e) 130 | return -2 131 | } 132 | } 133 | } 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.view.MenuItem 8 | import android.widget.Toast 9 | import androidx.activity.enableEdgeToEdge 10 | import com.topjohnwu.superuser.Shell 11 | import github.daisukikaffuchino.rebootnya.databinding.ActivitySettingsBinding 12 | import github.daisukikaffuchino.rebootnya.fragment.SettingsFragment 13 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 14 | import github.daisukikaffuchino.rebootnya.utils.RootUtil 15 | import github.daisukikaffuchino.rebootnya.utils.ShizukuUtil 16 | import github.daisukikaffuchino.rebootnya.utils.getAppVer 17 | import rikka.shizuku.Shizuku 18 | import rikka.shizuku.Shizuku.OnRequestPermissionResultListener 19 | import rikka.sui.Sui 20 | 21 | 22 | class SettingsActivity : BaseActivity() { 23 | lateinit var binding: ActivitySettingsBinding 24 | private val requestPermissionResultListener = 25 | OnRequestPermissionResultListener { requestCode: Int, grantResult: Int -> 26 | this.onRequestPermissionsResult( 27 | requestCode, 28 | grantResult 29 | ) 30 | } 31 | 32 | lateinit var context: Context 33 | 34 | companion object { 35 | const val SHIZUKU_REQUEST_CODE: Int = 1000 36 | } 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | enableEdgeToEdge() 40 | super.onCreate(savedInstanceState) 41 | this.theme.applyStyle( 42 | rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, 43 | true 44 | ) 45 | 46 | context = this 47 | binding = ActivitySettingsBinding.inflate(layoutInflater) 48 | setContentView(binding.root) 49 | setSupportActionBar(binding.settingsToolbar) 50 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 51 | 52 | if (savedInstanceState == null) { 53 | supportFragmentManager.beginTransaction() 54 | .replace(R.id.settings_fragment_container, SettingsFragment()) 55 | .commit() 56 | } 57 | } 58 | 59 | @SuppressLint("SetTextI18n") 60 | override fun onStart() { 61 | super.onStart() 62 | Shizuku.addRequestPermissionResultListener(requestPermissionResultListener) 63 | 64 | val workingMode = NyaSettings.getWorkMode() 65 | val isPermitted = when (workingMode) { 66 | NyaSettings.MODE.ROOT -> Shell.isAppGrantedRoot() != false 67 | NyaSettings.MODE.SHIZUKU -> ShizukuUtil(context).checkShizukuPermission() 68 | else -> false 69 | } 70 | if (isPermitted) 71 | setWorkingStatus(workingMode) 72 | else { 73 | binding.run { 74 | textVerInfo.text = 75 | "RebootNya ${getAppVer(context)}\n${getString(R.string.click_request_pm)}" 76 | textWorkStatus.text = getString(R.string.not_working) 77 | taffy.setImageResource(R.drawable.taffy_no) 78 | 79 | cardStatus.setOnClickListener { 80 | when (workingMode) { 81 | NyaSettings.MODE.ROOT -> RootUtil(context).requestRoot() 82 | NyaSettings.MODE.SHIZUKU -> { 83 | if (!Shizuku.pingBinder()) { 84 | Toast.makeText( 85 | context, 86 | R.string.shizuku_not_run, 87 | Toast.LENGTH_SHORT 88 | ).show() 89 | return@setOnClickListener 90 | } 91 | Shizuku.requestPermission(SHIZUKU_REQUEST_CODE) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | } 99 | 100 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 101 | if (item.itemId == android.R.id.home) { 102 | finish() 103 | return true 104 | } 105 | return super.onOptionsItemSelected(item) 106 | } 107 | 108 | private fun onRequestPermissionsResult(requestCode: Int, grantResult: Int) { 109 | val granted = grantResult == PackageManager.PERMISSION_GRANTED 110 | if (granted && NyaSettings.getWorkMode()== NyaSettings.MODE.SHIZUKU 111 | && requestCode == SHIZUKU_REQUEST_CODE 112 | ) { 113 | setWorkingStatus(NyaSettings.MODE.SHIZUKU) 114 | } 115 | } 116 | 117 | @SuppressLint("SetTextI18n") 118 | private fun setWorkingStatus(workingMode: Int) { 119 | 120 | val statusText = when (workingMode) { 121 | NyaSettings.MODE.SHIZUKU -> { 122 | val shizukuVersion = Shizuku.getVersion() 123 | if (Sui.init(context.packageName)) { 124 | "${getString(R.string.working)} " 125 | } else { 126 | "${getString(R.string.working)} " 127 | } 128 | } 129 | 130 | else -> "${getString(R.string.working)} " 131 | } 132 | 133 | binding.apply { 134 | textWorkStatus.text = statusText 135 | textVerInfo.text = "RebootNya ${getAppVer(context)}" 136 | taffy.setImageResource(R.drawable.taffy_ok) 137 | cardStatus.setOnClickListener(null) 138 | } 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/preference/EditTextPreference.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.preference 2 | 3 | import android.content.Context 4 | import android.os.Parcel 5 | import android.os.Parcelable 6 | import android.text.Editable 7 | import android.text.TextWatcher 8 | import android.util.AttributeSet 9 | import android.widget.Toast 10 | import androidx.preference.Preference 11 | import androidx.preference.PreferenceViewHolder 12 | import com.google.android.material.textfield.TextInputEditText 13 | import com.google.android.material.textfield.TextInputLayout 14 | import com.topjohnwu.superuser.Shell 15 | import github.daisukikaffuchino.rebootnya.R 16 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 17 | import github.daisukikaffuchino.rebootnya.utils.RootUtil 18 | import github.daisukikaffuchino.rebootnya.utils.ShizukuUtil 19 | 20 | class EditTextPreference(private val context: Context, attrs: AttributeSet?) : Preference( 21 | context, attrs 22 | ) { 23 | init { 24 | layoutResource = R.layout.preference_edit_text 25 | } 26 | 27 | private var savedText: String? = null 28 | private var editText: TextInputEditText? = null 29 | 30 | override fun onBindViewHolder(holder: PreferenceViewHolder) { 31 | super.onBindViewHolder(holder) 32 | val inputLayout = 33 | holder.itemView.findViewById(R.id.item_cmd_text_input_layout) 34 | editText = 35 | holder.itemView.findViewById(R.id.item_cmd_text_input_edit) 36 | 37 | inputLayout.setEndIconOnClickListener { 38 | val command = editText?.text.toString() 39 | if (command.isBlank()) { 40 | return@setEndIconOnClickListener 41 | } 42 | 43 | val shizukuUtil = ShizukuUtil(context) 44 | val rootUtil = RootUtil(context) 45 | val workingMode = NyaSettings.getWorkMode() 46 | 47 | when (workingMode) { 48 | NyaSettings.MODE.ROOT -> { 49 | if (Shell.isAppGrantedRoot() != false) { 50 | if (rootUtil.runRootCommandWithResult(command)) { 51 | Toast.makeText(context, "Success!", Toast.LENGTH_SHORT).show() 52 | editText?.text = null 53 | } else { 54 | Toast.makeText(context, R.string.exec_fail, Toast.LENGTH_SHORT).show() 55 | } 56 | } else { 57 | Toast.makeText(context, R.string.no_permission, Toast.LENGTH_SHORT).show() 58 | } 59 | } 60 | 61 | NyaSettings.MODE.SHIZUKU -> { 62 | if (shizukuUtil.checkShizukuPermission()) { 63 | val exitCode = shizukuUtil.runShizukuCommand( 64 | command.split("\\s+".toRegex()).toTypedArray(), false 65 | ) 66 | if (exitCode == 0) { 67 | Toast.makeText(context, "Success!\nExit code: 0", Toast.LENGTH_SHORT) 68 | .show() 69 | editText?.text = null 70 | } else { 71 | val errorMsg = 72 | "${context.getString(R.string.exec_fail)}\nExit code: $exitCode" 73 | Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() 74 | } 75 | } else { 76 | Toast.makeText(context, R.string.no_permission, Toast.LENGTH_SHORT).show() 77 | } 78 | } 79 | 80 | } 81 | } 82 | 83 | inputLayout.setEndIconOnLongClickListener { 84 | editText?.setText(null) 85 | true 86 | } 87 | 88 | editText?.addTextChangedListener(object : TextWatcher { 89 | override fun afterTextChanged(editable: Editable?) { 90 | savedText = editText?.text.toString() 91 | } 92 | 93 | override fun beforeTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {} 94 | override fun onTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {} 95 | }) 96 | 97 | if (savedText != null) 98 | editText?.setText(savedText) 99 | 100 | } 101 | 102 | override fun onSaveInstanceState(): Parcelable { 103 | val superState = super.onSaveInstanceState() 104 | val myState = SavedState(superState) 105 | myState.customValue = savedText 106 | return myState 107 | } 108 | 109 | override fun onRestoreInstanceState(state: Parcelable?) { 110 | if (state !is SavedState) { 111 | super.onRestoreInstanceState(state) 112 | return 113 | } 114 | super.onRestoreInstanceState(state.superState) 115 | savedText = state.customValue 116 | } 117 | 118 | private class SavedState : BaseSavedState { 119 | var customValue: String? = null 120 | 121 | constructor(superState: Parcelable?) : super(superState) 122 | 123 | private constructor(source: Parcel) : super(source) { 124 | customValue = source.readString() 125 | } 126 | 127 | override fun writeToParcel(dest: Parcel, flags: Int) { 128 | super.writeToParcel(dest, flags) 129 | dest.writeString(customValue) 130 | } 131 | 132 | companion object CREATOR : Parcelable.Creator { 133 | override fun createFromParcel(source: Parcel): SavedState = SavedState(source) 134 | override fun newArray(size: Int): Array = arrayOfNulls(size) 135 | } 136 | } 137 | 138 | fun getTextInputEditText(): TextInputEditText? { 139 | return editText 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/utils/NyaSettings.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.UiModeManager 5 | import android.content.Context 6 | import android.content.ContextWrapper 7 | import android.content.SharedPreferences 8 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener 9 | import android.content.res.Configuration 10 | import android.os.Build 11 | import android.text.TextUtils 12 | import androidx.annotation.IntDef 13 | import androidx.appcompat.app.AppCompatDelegate 14 | import java.util.Locale 15 | 16 | object NyaSettings { 17 | const val NAME = "settings" 18 | 19 | private var sPreferences: SharedPreferences? = null 20 | 21 | @JvmStatic 22 | val preferences: SharedPreferences 23 | get() { 24 | if (sPreferences == null) { 25 | throw IllegalStateException("NyaSettings has not been initialized. Call NyaSettings.initialize(context) first.") 26 | } 27 | return sPreferences!! 28 | } 29 | 30 | @JvmStatic 31 | private fun getSettingsStorageContext(context: Context): Context { 32 | var storageContext: Context = context.createDeviceProtectedStorageContext() 33 | 34 | storageContext = object : ContextWrapper(storageContext) { 35 | override fun getSharedPreferences(name: String, mode: Int): SharedPreferences { 36 | return try { 37 | super.getSharedPreferences(name, mode) 38 | } catch (_: IllegalStateException) { 39 | EmptySharedPreferencesImpl() 40 | } 41 | } 42 | } 43 | return storageContext 44 | } 45 | 46 | @JvmStatic 47 | fun initialize(context: Context) { 48 | if (sPreferences == null) { 49 | sPreferences = getSettingsStorageContext(context) 50 | .getSharedPreferences(NAME, Context.MODE_PRIVATE) 51 | } 52 | } 53 | 54 | @SuppressLint("UniqueConstants") 55 | @IntDef( 56 | MODE.SHIZUKU, 57 | MODE.ROOT, 58 | MODE.PROCESS, 59 | MODE.USER_SERVICE 60 | ) 61 | @Retention(AnnotationRetention.SOURCE) 62 | annotation class MODE { 63 | companion object { 64 | const val SHIZUKU = 1 65 | const val ROOT = 2 66 | const val PROCESS = 1 // Note: Same value as SHIZUKU 67 | const val USER_SERVICE = 2 // Note: Same value as ROOT 68 | } 69 | } 70 | 71 | @IntDef( 72 | STYLE.CLASSIC_LIST, 73 | STYLE.MATERIAL_BUTTON 74 | ) 75 | @Retention(AnnotationRetention.SOURCE) 76 | annotation class STYLE { 77 | companion object { 78 | const val CLASSIC_LIST = 1 79 | const val MATERIAL_BUTTON = 2 80 | } 81 | } 82 | 83 | @JvmStatic 84 | @MODE 85 | fun getWorkMode(): Int { 86 | return preferences.getInt("work_mode", MODE.SHIZUKU) 87 | } 88 | 89 | @JvmStatic 90 | @STYLE 91 | fun getMainInterfaceStyle(): Int { 92 | return preferences.getInt("main_interface_style", STYLE.CLASSIC_LIST) 93 | } 94 | 95 | @JvmStatic 96 | @MODE 97 | fun getShizukuShellMode(): Int { 98 | return preferences.getInt("shizuku_shell_mode", MODE.PROCESS) 99 | } 100 | 101 | @JvmStatic 102 | @AppCompatDelegate.NightMode 103 | fun getNightMode(context: Context): Int { 104 | var defValue = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 105 | val uiModeManager = context.getSystemService(UiModeManager::class.java) 106 | if (uiModeManager != null && uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_WATCH) { 107 | defValue = AppCompatDelegate.MODE_NIGHT_YES 108 | } 109 | return preferences.getInt("night_mode", defValue) 110 | } 111 | 112 | @JvmStatic 113 | fun getLocale(): Locale { 114 | val tag = preferences.getString("language", null) 115 | return if (TextUtils.isEmpty(tag) || "SYSTEM" == tag) { 116 | Locale.getDefault() 117 | } else { 118 | Locale.forLanguageTag(tag!!) 119 | } 120 | } 121 | 122 | @JvmStatic 123 | fun getIsHideUnavailableOptions(): Boolean { 124 | return preferences.getBoolean("hide_unavailable_options", true) 125 | } 126 | 127 | @JvmStatic 128 | fun isUsingSystemColor(): Boolean { 129 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && 130 | preferences.getBoolean("dynamic_color", false) 131 | } 132 | 133 | @JvmStatic 134 | fun getLastSelectedOption(): String? { 135 | return preferences.getString("last_selected_option", null) 136 | } 137 | 138 | @SuppressLint("ApplySharedPref") 139 | @JvmStatic 140 | fun setLastSelectedOption(option: String) { 141 | preferences.edit().putString("last_selected_option", option).commit() 142 | } 143 | 144 | private class EmptySharedPreferencesImpl : SharedPreferences { 145 | override fun getAll(): MutableMap { 146 | return HashMap() 147 | } 148 | 149 | override fun getString(key: String?, defValue: String?): String? { 150 | return defValue 151 | } 152 | 153 | override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? { 154 | return defValues 155 | } 156 | 157 | override fun getInt(key: String?, defValue: Int): Int { 158 | return defValue 159 | } 160 | 161 | override fun getLong(key: String?, defValue: Long): Long { 162 | return defValue 163 | } 164 | 165 | override fun getFloat(key: String?, defValue: Float): Float { 166 | return defValue 167 | } 168 | 169 | override fun getBoolean(key: String?, defValue: Boolean): Boolean { 170 | return defValue 171 | } 172 | 173 | override fun contains(key: String?): Boolean { 174 | return false 175 | } 176 | 177 | override fun edit(): SharedPreferences.Editor { 178 | return EditorImpl() 179 | } 180 | 181 | override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener?) { 182 | } 183 | 184 | override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener?) { 185 | } 186 | 187 | private class EditorImpl : SharedPreferences.Editor { 188 | override fun putString(key: String?, value: String?): SharedPreferences.Editor { 189 | return this 190 | } 191 | 192 | override fun putStringSet( 193 | key: String?, 194 | values: MutableSet? 195 | ): SharedPreferences.Editor { 196 | return this 197 | } 198 | 199 | override fun putInt(key: String?, value: Int): SharedPreferences.Editor { 200 | return this 201 | } 202 | 203 | override fun putLong(key: String?, value: Long): SharedPreferences.Editor { 204 | return this 205 | } 206 | 207 | override fun putFloat(key: String?, value: Float): SharedPreferences.Editor { 208 | return this 209 | } 210 | 211 | override fun putBoolean(key: String?, value: Boolean): SharedPreferences.Editor { 212 | return this 213 | } 214 | 215 | override fun remove(key: String?): SharedPreferences.Editor { 216 | return this 217 | } 218 | 219 | override fun clear(): SharedPreferences.Editor { 220 | return this 221 | } 222 | 223 | override fun commit(): Boolean { 224 | return true 225 | } 226 | 227 | override fun apply() { 228 | } 229 | } 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/preference/IntegerSimpleMenuPreference.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.preference 2 | 3 | 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.content.res.TypedArray 7 | import android.os.Parcel 8 | import android.os.Parcelable 9 | import android.util.AttributeSet 10 | import android.view.ContextThemeWrapper 11 | import android.view.View 12 | import androidx.annotation.ArrayRes 13 | import androidx.annotation.StyleableRes 14 | import androidx.core.content.res.TypedArrayUtils 15 | import androidx.preference.Preference 16 | import androidx.preference.PreferenceViewHolder 17 | import rikka.preference.simplemenu.R 18 | import rikka.preference.simplemenu.SimpleMenuPopupWindow 19 | 20 | /** 21 | * a [rikka.preference.SimpleMenuPreference] to implement night mode in user interface settings. 22 | * a [rikka.preference.SimpleMenuPreference] which use integer values array as entryValues. 23 | * 24 | * @author Haruue Icymoon haruue@caoyue.com.cn 25 | */ 26 | @SuppressLint("RestrictedApi", "UseKtx") 27 | class IntegerSimpleMenuPreference @SuppressLint("RestrictedApi") constructor( 28 | context: Context, 29 | attrs: AttributeSet?, 30 | defStyleAttr: Int, 31 | defStyleRes: Int 32 | ) : Preference(context, attrs, defStyleAttr, defStyleRes) { 33 | private val mPopupWindow: SimpleMenuPopupWindow? 34 | private var mAnchor: View? = null 35 | private var mItemView: View? = null 36 | 37 | private var mEntries: Array? 38 | /** 39 | * Returns the array of values to be saved for the preference. 40 | * 41 | * @return The array of values. 42 | */ 43 | /** 44 | * The array to find the value to save for a preference when an entry from 45 | * entries is selected. If a user clicks on the second item in entries, the 46 | * second item in this array will be saved to the preference. 47 | * 48 | * @param entryValues The array to be used as values to save for the preference. 49 | */ 50 | var entryValues: IntArray? 51 | private var mValue = 0 52 | private var mSummary: String? 53 | private var mValueSet = false 54 | 55 | init { 56 | var a = context.obtainStyledAttributes( 57 | attrs, R.styleable.ListPreference, defStyleAttr, defStyleRes 58 | ) 59 | 60 | mEntries = TypedArrayUtils.getTextArray( 61 | a, R.styleable.ListPreference_entries, 62 | R.styleable.ListPreference_android_entries 63 | ) 64 | 65 | this.entryValues = getIntArray( 66 | a, R.styleable.ListPreference_entryValues, 67 | R.styleable.ListPreference_android_entryValues 68 | ) 69 | 70 | a.recycle() 71 | 72 | /* Retrieve the Preference summary attribute since it's private 73 | * in the Preference class. 74 | */ 75 | a = context.obtainStyledAttributes( 76 | attrs, 77 | R.styleable.Preference, defStyleAttr, defStyleRes 78 | ) 79 | 80 | mSummary = TypedArrayUtils.getString( 81 | a, R.styleable.Preference_summary, 82 | R.styleable.Preference_android_summary 83 | ) 84 | 85 | a.recycle() 86 | 87 | a = context.obtainStyledAttributes( 88 | attrs, R.styleable.SimpleMenuPreference, defStyleAttr, defStyleRes 89 | ) 90 | 91 | val popupStyle = a.getResourceId( 92 | R.styleable.SimpleMenuPreference_android_popupMenuStyle, 93 | R.style.Widget_Preference_SimpleMenuPreference_PopupMenu 94 | ) 95 | val popupTheme = a.getResourceId( 96 | R.styleable.SimpleMenuPreference_android_popupTheme, 97 | R.style.ThemeOverlay_Preference_SimpleMenuPreference_PopupMenu 98 | ) 99 | val popupContext = if (popupTheme != 0) { 100 | ContextThemeWrapper(context, popupTheme) 101 | } else { 102 | context 103 | } 104 | mPopupWindow = SimpleMenuPopupWindow( 105 | popupContext, 106 | attrs, 107 | R.styleable.SimpleMenuPreference_android_popupMenuStyle, 108 | popupStyle 109 | ) 110 | mPopupWindow.onItemClickListener = SimpleMenuPopupWindow.OnItemClickListener { i: Int -> 111 | val value = this.entryValues!![i] 112 | if (callChangeListener(value)) this.value = value 113 | } 114 | 115 | a.recycle() 116 | } 117 | 118 | @JvmOverloads 119 | constructor( 120 | context: Context, 121 | attrs: AttributeSet? = null, 122 | defStyleAttr: Int = R.attr.simpleMenuPreferenceStyle 123 | ) : this(context, attrs, defStyleAttr, R.style.Preference_SimpleMenuPreference) 124 | 125 | override fun onClick() { 126 | if (this.entries == null || this.entries!!.isEmpty()) return 127 | 128 | if (mPopupWindow == null) return 129 | 130 | mPopupWindow.setEntries(this.entries) 131 | mPopupWindow.setSelectedIndex(findIndexOfValue(this.value)) 132 | 133 | val container = mItemView!! // itemView 134 | .parent as View? // -> list (RecyclerView) 135 | 136 | mPopupWindow.show(mItemView, container, mAnchor!!.x.toInt()) 137 | } 138 | 139 | /** 140 | * @param entriesResId The entries array as a resource. 141 | * @see .setEntries 142 | */ 143 | fun setEntries(@ArrayRes entriesResId: Int) { 144 | this.entries = context.resources.getTextArray(entriesResId) 145 | } 146 | 147 | var entries: Array? 148 | /** 149 | * The list of entries to be shown in the list in subsequent dialogs. 150 | * 151 | * @return The list as an array. 152 | */ 153 | get() = mEntries 154 | /** 155 | * Sets the human-readable entries to be shown in the list. This will be 156 | * shown in subsequent dialogs. 157 | * 158 | * 159 | * Each entry must have a corresponding index in 160 | * [.setEntryValues]. 161 | * 162 | * @param entries The entries. 163 | * @see .setEntryValues 164 | */ 165 | set(entries) { 166 | mEntries = entries 167 | 168 | mPopupWindow!!.requestMeasure() 169 | } 170 | 171 | /** 172 | * @param entryValuesResId The entry values array as a resource. 173 | * @see .setEntryValues 174 | */ 175 | fun setEntryValues(@ArrayRes entryValuesResId: Int) { 176 | this.entryValues = context.resources.getIntArray(entryValuesResId) 177 | } 178 | 179 | /** 180 | * Returns the summary of this ListPreference. If the summary 181 | * has a [String formatting][java.lang.String.format] 182 | * marker in it (i.e. "%s" or "%1$s"), then the current entry 183 | * value will be substituted in its place. 184 | * 185 | * @return the summary with appropriate string substitution 186 | */ 187 | override fun getSummary(): CharSequence? { 188 | val entry = this.entry 189 | return if (mSummary == null) 190 | super.getSummary() 191 | else 192 | String.format(mSummary!!, entry ?: "") 193 | } 194 | 195 | /** 196 | * Sets the summary for this Preference with a CharSequence. 197 | * If the summary has a 198 | * [String formatting][java.lang.String.format] 199 | * marker in it (i.e. "%s" or "%1$s"), then the current entry 200 | * value will be substituted in its place when it's retrieved. 201 | * 202 | * @param summary The summary for the preference. 203 | */ 204 | override fun setSummary(summary: CharSequence?) { 205 | super.setSummary(summary) 206 | if (summary == null && mSummary != null) 207 | mSummary = null 208 | else if (summary != null && summary != mSummary) 209 | mSummary = summary.toString() 210 | } 211 | 212 | var value: Int 213 | /** 214 | * Returns the value of the key. This should be one of the entries in 215 | * [.getEntryValues]. 216 | * 217 | * @return The value of the key. 218 | */ 219 | get() = mValue 220 | /** 221 | * Sets the value of the key. This should be one of the entries in 222 | * [.getEntryValues]. 223 | * 224 | * @param value The value to set for the key. 225 | */ 226 | set(value) { 227 | // Always persist/notify the first time. 228 | val changed = mValue != value 229 | if (changed || !mValueSet) { 230 | mValue = value 231 | mValueSet = true 232 | persistInt(value) 233 | if (changed) notifyChanged() 234 | } 235 | } 236 | 237 | val entry: CharSequence? 238 | /** 239 | * Returns the entry corresponding to the current value. 240 | * 241 | * @return The entry corresponding to the current value, or null. 242 | */ 243 | get() { 244 | val index = this.valueIndex 245 | return if (index >= 0 && mEntries != null) mEntries!![index] else null 246 | } 247 | 248 | /** 249 | * Returns the index of the given value (in the entry values array). 250 | * 251 | * @param value The value whose index should be returned. 252 | * @return The index of the value, or -1 if not found. 253 | */ 254 | fun findIndexOfValue(value: Int): Int { 255 | if (this.entryValues != null) { 256 | for (i in entryValues!!.indices.reversed()) { 257 | val entryValue = this.entryValues!![i] 258 | if (entryValue == value) return i 259 | } 260 | } 261 | return -1 262 | } 263 | 264 | private var valueIndex: Int 265 | get() = findIndexOfValue(mValue) 266 | set(index) { 267 | if (this.entryValues != null) this.value = this.entryValues!![index] 268 | } 269 | 270 | override fun onGetDefaultValue(a: TypedArray, index: Int): Any? { 271 | return a.getInt(index, 1) 272 | } 273 | 274 | override fun onSetInitialValue(defaultValue: Any?) { 275 | var defaultValue = defaultValue 276 | if (defaultValue == null) { 277 | defaultValue = 0 278 | } 279 | this.value = getPersistedInt(defaultValue as Int) 280 | } 281 | 282 | override fun onSaveInstanceState(): Parcelable? { 283 | val superState = super.onSaveInstanceState() 284 | if (isPersistent) { 285 | // No need to save instance state since it's persistent 286 | return superState 287 | } 288 | 289 | val myState = SavedState(superState) 290 | myState.value = this.value 291 | return myState 292 | } 293 | 294 | override fun onRestoreInstanceState(state: Parcelable?) { 295 | if (state == null || state.javaClass != SavedState::class.java) { 296 | // Didn't save state for us in onSaveInstanceState 297 | super.onRestoreInstanceState(state) 298 | return 299 | } 300 | 301 | val myState = state as SavedState 302 | super.onRestoreInstanceState(myState.superState) 303 | this.value = myState.value 304 | } 305 | 306 | private class SavedState : BaseSavedState { 307 | var value: Int = 0 308 | 309 | override fun writeToParcel(dest: Parcel, flags: Int) { 310 | super.writeToParcel(dest, flags) 311 | dest.writeInt(value) 312 | } 313 | 314 | constructor(superState: Parcelable?) : super(superState) 315 | 316 | } 317 | 318 | override fun onBindViewHolder(holder: PreferenceViewHolder) { 319 | super.onBindViewHolder(holder) 320 | 321 | mItemView = holder.itemView 322 | mAnchor = holder.itemView.findViewById(android.R.id.empty) 323 | 324 | checkNotNull(mAnchor) { 325 | "SimpleMenuPreference item layout must contain" + 326 | "a view id is android.R.id.empty to support iconSpaceReserved" 327 | } 328 | } 329 | 330 | companion object { 331 | @SuppressLint("RestrictedApi") 332 | private fun getIntArray( 333 | a: TypedArray, @StyleableRes index: Int, 334 | @StyleableRes fallbackIndex: Int 335 | ): IntArray { 336 | val resourceId = TypedArrayUtils.getResourceId(a, index, fallbackIndex, 0) 337 | return a.resources.getIntArray(resourceId) 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/fragment/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.fragment 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.text.TextUtils 8 | import android.util.TypedValue 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.Toast 13 | import androidx.appcompat.app.AppCompatDelegate 14 | import androidx.core.content.edit 15 | import androidx.core.content.pm.ShortcutManagerCompat 16 | import androidx.preference.ListPreference 17 | import androidx.preference.Preference 18 | import androidx.preference.PreferenceFragmentCompat 19 | import androidx.preference.TwoStatePreference 20 | import androidx.recyclerview.widget.RecyclerView 21 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 22 | import github.daisukikaffuchino.rebootnya.R 23 | import github.daisukikaffuchino.rebootnya.preference.EditTextPreference 24 | import github.daisukikaffuchino.rebootnya.preference.IntegerSimpleMenuPreference 25 | import github.daisukikaffuchino.rebootnya.data.AppLocales 26 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 27 | import github.daisukikaffuchino.rebootnya.utils.ShortcutHelper 28 | import github.daisukikaffuchino.rebootnya.utils.openUrlLink 29 | import github.daisukikaffuchino.rebootnya.utils.sendEmail 30 | import github.daisukikaffuchino.rebootnya.utils.toHtml 31 | import rikka.material.app.LocaleDelegate 32 | import rikka.recyclerview.addEdgeSpacing 33 | import rikka.recyclerview.fixEdgeEffect 34 | import rikka.widget.borderview.BorderRecyclerView 35 | import java.util.Locale 36 | 37 | 38 | class SettingsFragment : PreferenceFragmentCompat() { 39 | 40 | private lateinit var workModePreference: IntegerSimpleMenuPreference 41 | private lateinit var shellModePreference: IntegerSimpleMenuPreference 42 | private lateinit var userServiceInfoPreference: Preference 43 | private lateinit var hideUnavailableOptionsPreference: TwoStatePreference 44 | private lateinit var dynamicColorPreference: TwoStatePreference 45 | private lateinit var nightModePreference: IntegerSimpleMenuPreference 46 | private lateinit var languagePreference: ListPreference 47 | private lateinit var editTextPreference: EditTextPreference 48 | private lateinit var pinShortcutsPreference: Preference 49 | private lateinit var clearShortcutsPreference: Preference 50 | private lateinit var translationPreference: Preference 51 | private lateinit var developerPreference: Preference 52 | private lateinit var projectInfoPreference: Preference 53 | 54 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 55 | val context = requireContext() 56 | preferenceManager.setStorageDeviceProtected() 57 | preferenceManager.sharedPreferencesName = NyaSettings.NAME 58 | preferenceManager.sharedPreferencesMode = Context.MODE_PRIVATE 59 | setPreferencesFromResource(R.xml.preference_settings, null) 60 | 61 | workModePreference = findPreference("work_mode")!! 62 | shellModePreference = findPreference("shizuku_shell_mode")!! 63 | userServiceInfoPreference = findPreference("user_service_mode_info")!! 64 | hideUnavailableOptionsPreference = findPreference("hide_unavailable_options")!! 65 | dynamicColorPreference = findPreference("dynamic_color")!! 66 | nightModePreference = findPreference("night_mode")!! 67 | languagePreference = findPreference("language")!! 68 | editTextPreference = findPreference("edit_text")!! 69 | pinShortcutsPreference = findPreference("pin_shortcuts")!! 70 | clearShortcutsPreference = findPreference("clear_shortcuts")!! 71 | translationPreference = findPreference("translation")!! 72 | developerPreference = findPreference("developer")!! 73 | projectInfoPreference = findPreference("repo")!! 74 | 75 | val work = NyaSettings.getWorkMode() 76 | workModePreference.onPreferenceChangeListener = 77 | Preference.OnPreferenceChangeListener { _: Preference?, value: Any? -> 78 | if (value is Int && work != value) 79 | activity?.recreate() 80 | true 81 | } 82 | 83 | if (work == NyaSettings.MODE.ROOT) { 84 | shellModePreference.isVisible = false 85 | userServiceInfoPreference.isVisible = false 86 | hideUnavailableOptionsPreference.isVisible = false 87 | } 88 | 89 | userServiceInfoPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 90 | MaterialAlertDialogBuilder(context) 91 | .setTitle(R.string.about_user_service) 92 | .setMessage(R.string.about_user_service_content) 93 | .setPositiveButton(android.R.string.ok, null) 94 | .show() 95 | true 96 | } 97 | 98 | nightModePreference.value = NyaSettings.getNightMode(context) 99 | nightModePreference.onPreferenceChangeListener = 100 | Preference.OnPreferenceChangeListener { _: Preference?, value: Any? -> 101 | if (value is Int && NyaSettings.getNightMode(context) != value) { 102 | AppCompatDelegate.setDefaultNightMode(value) 103 | activity?.recreate() 104 | } 105 | true 106 | } 107 | 108 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 109 | dynamicColorPreference.onPreferenceChangeListener = 110 | Preference.OnPreferenceChangeListener { _: Preference?, value: Any? -> 111 | if (value is Boolean && NyaSettings.isUsingSystemColor() != value) { 112 | activity?.recreate() 113 | } 114 | true 115 | } 116 | } else { 117 | dynamicColorPreference.isEnabled = false 118 | } 119 | 120 | languagePreference.onPreferenceChangeListener = 121 | Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> 122 | if (newValue is String) { 123 | val locale: Locale = if ("SYSTEM" == newValue) 124 | LocaleDelegate.systemLocale 125 | else 126 | Locale.forLanguageTag(newValue) 127 | LocaleDelegate.defaultLocale = locale 128 | activity?.recreate() 129 | } 130 | true 131 | } 132 | setupLocalePreference() 133 | 134 | pinShortcutsPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 135 | MaterialAlertDialogBuilder(context) 136 | .setTitle(R.string.request_pin_shortcuts) 137 | .setItems( 138 | arrayOf( 139 | getString(R.string.lock_screen), 140 | getString(R.string.power_off), 141 | getString(R.string.reboot) 142 | ) 143 | ) { dialogInterface, i -> 144 | val shortcutHelper = ShortcutHelper(context) 145 | shortcutHelper.requestPinShortcut(shortcutHelper.items[i]) 146 | } 147 | .show() 148 | true 149 | } 150 | 151 | clearShortcutsPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 152 | ShortcutManagerCompat.removeAllDynamicShortcuts(context) 153 | NyaSettings.preferences.edit { putBoolean("isShortcutCreated", false) } 154 | Toast.makeText(context, R.string.cleared, Toast.LENGTH_SHORT).show() 155 | true 156 | } 157 | 158 | translationPreference.summary = 159 | context.getString( 160 | R.string.translation_summary, context.getString(R.string.app_name) 161 | ) 162 | translationPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 163 | openUrlLink(context, "https://crowdin.com/project/rebootnya") 164 | true 165 | } 166 | 167 | developerPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 168 | MaterialAlertDialogBuilder(context) 169 | .setTitle(R.string.contact_me) 170 | .setItems( 171 | arrayOf( 172 | getString(R.string.email), 173 | getString(R.string.bilibili) 174 | ) 175 | ) { dialogInterface, i -> 176 | when (i) { 177 | 0 -> sendEmail(context) 178 | 1 -> openUrlLink(context, "https://space.bilibili.com/178423358") 179 | } 180 | } 181 | .show() 182 | true 183 | } 184 | 185 | projectInfoPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { 186 | openUrlLink(context, "https://github.com/daisukiKaffuChino/RebootNya") 187 | true 188 | } 189 | } 190 | 191 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 192 | super.onViewCreated(view, savedInstanceState) 193 | val rootView = requireActivity().window.decorView 194 | rootView.viewTreeObserver.addOnGlobalLayoutListener { 195 | val rect = Rect() 196 | rootView.getWindowVisibleDisplayFrame(rect) 197 | val screenHeight = rootView.rootView.height 198 | val keypadHeight = screenHeight - rect.bottom 199 | if (keypadHeight < screenHeight * 0.15) 200 | editTextPreference.getTextInputEditText()?.clearFocus() 201 | } 202 | } 203 | 204 | override fun onCreateRecyclerView( 205 | inflater: LayoutInflater, 206 | parent: ViewGroup, 207 | savedInstanceState: Bundle? 208 | ): RecyclerView { 209 | val recyclerView = 210 | super.onCreateRecyclerView(inflater, parent, savedInstanceState) as BorderRecyclerView 211 | recyclerView.fixEdgeEffect() 212 | recyclerView.addEdgeSpacing(bottom = 8f, unit = TypedValue.COMPLEX_UNIT_DIP) 213 | 214 | return recyclerView 215 | } 216 | 217 | private fun setupLocalePreference() { 218 | val localeTags = AppLocales.LOCALES 219 | val displayLocaleTags = AppLocales.DISPLAY_LOCALES 220 | 221 | languagePreference.entries = displayLocaleTags 222 | languagePreference.entryValues = localeTags 223 | 224 | val currentLocaleTag = languagePreference.value 225 | val currentLocaleIndex = localeTags.indexOf(currentLocaleTag) 226 | val currentLocale = NyaSettings.getLocale() 227 | val localizedLocales = mutableListOf() 228 | 229 | for ((index, displayLocale) in displayLocaleTags.withIndex()) { 230 | if (index == 0) { 231 | localizedLocales.add(getString(R.string.follow_system)) 232 | continue 233 | } 234 | 235 | val locale = Locale.forLanguageTag(displayLocale.toString()) 236 | val localeName = if (!TextUtils.isEmpty(locale.script)) 237 | locale.getDisplayScript(locale) 238 | else 239 | locale.getDisplayName(locale) 240 | 241 | val localizedLocaleName = if (!TextUtils.isEmpty(locale.script)) 242 | locale.getDisplayScript(currentLocale) 243 | else 244 | locale.getDisplayName(currentLocale) 245 | 246 | localizedLocales.add( 247 | if (index != currentLocaleIndex) 248 | "$localeName
$localizedLocaleName".toHtml() 249 | else 250 | localizedLocaleName 251 | ) 252 | } 253 | 254 | languagePreference.entries = localizedLocales.toTypedArray() 255 | 256 | languagePreference.summary = when { 257 | TextUtils.isEmpty(currentLocaleTag) || "SYSTEM" == currentLocaleTag -> 258 | getString(R.string.follow_system) 259 | 260 | currentLocaleIndex != -1 -> { 261 | val localizedLocale = localizedLocales[currentLocaleIndex] 262 | val newLineIndex = localizedLocale.indexOf('\n') 263 | if (newLineIndex == -1) 264 | localizedLocale.toString() 265 | else 266 | localizedLocale.subSequence(0, newLineIndex).toString() 267 | } 268 | 269 | else -> "Error" 270 | } 271 | } 272 | 273 | } -------------------------------------------------------------------------------- /app/src/main/java/github/daisukikaffuchino/rebootnya/fragment/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package github.daisukikaffuchino.rebootnya.fragment 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.content.DialogInterface 7 | import android.content.Intent 8 | import android.os.Bundle 9 | import android.os.Handler 10 | import android.os.Looper 11 | import android.util.Log 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.Toast 15 | import androidx.appcompat.app.AlertDialog 16 | import androidx.appcompat.widget.ButtonBarLayout 17 | import androidx.fragment.app.DialogFragment 18 | import androidx.recyclerview.widget.LinearLayoutManager 19 | import androidx.recyclerview.widget.RecyclerView 20 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 21 | import github.daisukikaffuchino.rebootnya.MainActivity 22 | import github.daisukikaffuchino.rebootnya.R 23 | import github.daisukikaffuchino.rebootnya.SettingsActivity 24 | import github.daisukikaffuchino.rebootnya.adapter.HomeRecyclerAdapter 25 | import github.daisukikaffuchino.rebootnya.data.HomeListItemData 26 | import github.daisukikaffuchino.rebootnya.data.ListItemEnum 27 | import github.daisukikaffuchino.rebootnya.shizuku.NyaShellManager 28 | import github.daisukikaffuchino.rebootnya.utils.NyaSettings 29 | import github.daisukikaffuchino.rebootnya.utils.RootUtil 30 | import github.daisukikaffuchino.rebootnya.utils.ShizukuUtil 31 | import github.daisukikaffuchino.rebootnya.utils.exclude 32 | import java.io.IOException 33 | import kotlin.system.exitProcess 34 | 35 | 36 | class HomeFragment : DialogFragment() { 37 | private lateinit var mContext: Context 38 | private var checkedItem = 0 39 | private lateinit var rootUtil: RootUtil 40 | private lateinit var shizukuUtil: ShizukuUtil 41 | private lateinit var listMap: LinkedHashMap 42 | 43 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 44 | val intent = requireActivity().intent 45 | if (intent != null && Intent.ACTION_RUN == intent.action) { 46 | return createLoadingDialog(intent) 47 | } 48 | 49 | return if (NyaSettings.getMainInterfaceStyle() == NyaSettings.STYLE.MATERIAL_BUTTON) 50 | createMaterialButtonsDialog() 51 | else 52 | createClassicListDialog() 53 | } 54 | 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | mContext = requireActivity() 58 | rootUtil = RootUtil(mContext) 59 | shizukuUtil = ShizukuUtil(mContext) 60 | listMap = createListItemMap() 61 | } 62 | 63 | override fun onResume() { 64 | super.onResume() 65 | if (NyaSettings.getShizukuShellMode() == NyaSettings.MODE.USER_SERVICE) 66 | NyaShellManager.bindService(shizukuUtil) { exitCode, message -> 67 | Log.d("main", "bind $exitCode") 68 | } 69 | } 70 | 71 | private fun createClassicListDialog(): Dialog { 72 | val builder = MaterialAlertDialogBuilder(mContext) 73 | val items = getDisplayItems() 74 | 75 | builder.setTitle(R.string.app_name) 76 | // Restore last selected item 77 | val lastSelected = NyaSettings.getLastSelectedOption() 78 | if (lastSelected != null) { 79 | val index = items.indexOfFirst { 80 | ListItemEnum.fromLocalizedDisplayName(mContext, it).name == lastSelected 81 | } 82 | if (index != -1) { 83 | checkedItem = index 84 | } else { 85 | // Saved option not found in current list, reset to first item 86 | checkedItem = 0 87 | val firstItemEnum = ListItemEnum.fromLocalizedDisplayName(mContext, items[0]) 88 | NyaSettings.setLastSelectedOption(firstItemEnum.name) 89 | } 90 | } 91 | builder.setSingleChoiceItems(items, checkedItem) { _, which -> checkedItem = which } 92 | 93 | 94 | val dialog = builder.create() 95 | return setupDialogButtons(dialog, items) 96 | } 97 | 98 | @SuppressLint("InflateParams", "StringFormatInvalid") 99 | private fun createMaterialButtonsDialog(): Dialog { 100 | val builder = MaterialAlertDialogBuilder(mContext) 101 | builder.setCustomTitle(layoutInflater.inflate(R.layout.dialog_custom_title, null)) 102 | 103 | val recyclerView = 104 | layoutInflater.inflate( 105 | R.layout.fragment_home_recycler_view, 106 | null, 107 | false 108 | ) as RecyclerView 109 | recyclerView.setLayoutManager(LinearLayoutManager(mContext)) 110 | 111 | val data: MutableList = ArrayList() 112 | val items = getDisplayItems() 113 | 114 | // Restore last selected item 115 | val lastSelected = NyaSettings.getLastSelectedOption() 116 | if (lastSelected != null) { 117 | val index = items.indexOfFirst { 118 | ListItemEnum.fromLocalizedDisplayName(mContext, it).name == lastSelected 119 | } 120 | if (index != -1) { 121 | checkedItem = index 122 | } else { 123 | // Saved option not found in current list, reset to first item 124 | checkedItem = 0 125 | val firstItemEnum = ListItemEnum.fromLocalizedDisplayName(mContext, items[0]) 126 | NyaSettings.setLastSelectedOption(firstItemEnum.name) 127 | } 128 | } 129 | 130 | for (i in 0..items.size - 1) { 131 | val itemData = HomeListItemData( 132 | items[i], 133 | i, 134 | items.size, 135 | i == checkedItem 136 | ) 137 | data.add(itemData) 138 | } 139 | 140 | val adapter = HomeRecyclerAdapter(data) { position, item -> 141 | checkedItem = item.indexInSection 142 | } 143 | recyclerView.setAdapter(adapter) 144 | builder.setView(recyclerView) 145 | recyclerView.addItemDecoration(HomeRecyclerAdapter.MarginItemDecoration()) 146 | 147 | val dialog = builder.create() 148 | return setupDialogButtons(dialog, items) 149 | } 150 | 151 | @SuppressLint("InflateParams") 152 | private fun createLoadingDialog(intent: Intent): Dialog { 153 | val loadingDialog = MaterialAlertDialogBuilder(mContext) 154 | loadingDialog.setTitle(R.string.app_name) 155 | .setView(layoutInflater.inflate(R.layout.dialog_progress, null)) 156 | .setCancelable(false) 157 | 158 | val data = intent.getStringExtra("extra") 159 | if (data != null) 160 | Handler(Looper.getMainLooper()).postDelayed({ 161 | doAction(ListItemEnum.fromDisplayName(data)) 162 | dismiss() 163 | }, 1000) 164 | return loadingDialog.create() 165 | } 166 | 167 | @SuppressLint("RestrictedApi") 168 | private fun setupDialogButtons(dialog: AlertDialog, items: Array): AlertDialog { 169 | dialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.confirm)) 170 | { dialogInterface, i -> } 171 | dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.close)) 172 | { dialogInterface, i -> } 173 | dialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.setting)) 174 | { dialogInterface, i -> } 175 | 176 | dialog.setOnShowListener { dialogInterface -> 177 | val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) 178 | val neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL) 179 | 180 | // Fix: Force selection update for Classic List style to override View State Restoration 181 | if (NyaSettings.getMainInterfaceStyle() == NyaSettings.STYLE.CLASSIC_LIST) { 182 | val listView = dialog.listView 183 | listView.setItemChecked(checkedItem, true) 184 | listView.setSelection(checkedItem) 185 | } 186 | 187 | positiveButton.setOnClickListener { v: View? -> 188 | val itemEnum = ListItemEnum.fromLocalizedDisplayName( 189 | mContext, 190 | items[checkedItem] 191 | ) 192 | NyaSettings.setLastSelectedOption(itemEnum.name) 193 | doAction(itemEnum) 194 | } 195 | neutralButton.setOnClickListener { v: View? -> 196 | val intent = Intent(mContext, SettingsActivity::class.java) 197 | mContext.startActivity(intent) 198 | } 199 | val decor = dialog.window?.decorView 200 | val buttonBar = findButtonBarLayout(decor) 201 | buttonBar?.setAllowStacking(false) 202 | } 203 | return dialog 204 | } 205 | 206 | @SuppressLint("RestrictedApi") 207 | fun findButtonBarLayout(root: View?): ButtonBarLayout? { 208 | if (root == null) return null 209 | if (root is ButtonBarLayout) return root 210 | if (root is ViewGroup) { 211 | for (i in 0 until root.childCount) { 212 | findButtonBarLayout(root.getChildAt(i))?.let { return it } 213 | } 214 | } 215 | return null 216 | } 217 | 218 | private fun getDisplayItems(): Array { 219 | var itemList = ArrayList(listMap.keys) 220 | if (MainActivity.checkListFilterStatus()) { 221 | itemList = exclude( 222 | itemList, 223 | ListItemEnum.SAFE_MODE.getLocalizedDisplayName(mContext), 224 | ListItemEnum.SOFT_REBOOT.getLocalizedDisplayName(mContext) 225 | ) as ArrayList 226 | } 227 | return itemList.toTypedArray() 228 | } 229 | 230 | private fun createListItemMap(): LinkedHashMap { 231 | val map = LinkedHashMap() 232 | for (item in ListItemEnum.entries) { 233 | map[item.getLocalizedDisplayName(mContext)] = item 234 | } 235 | return map 236 | } 237 | 238 | private fun doAction(listItemEnum: ListItemEnum?) { 239 | if (listItemEnum == null) { 240 | Log.e("HomeFragment", "doAction called with null ListItemEnum") 241 | Toast.makeText(mContext, R.string.exec_fail, Toast.LENGTH_SHORT).show() 242 | return 243 | } 244 | if (NyaSettings.getWorkMode() == NyaSettings.MODE.ROOT) { 245 | funcRoot(listItemEnum) 246 | } else { 247 | try { 248 | funcShizuku(listItemEnum) 249 | } catch (e: IOException) { 250 | e.fillInStackTrace() 251 | Toast.makeText(mContext, R.string.exec_fail, Toast.LENGTH_SHORT).show() 252 | } 253 | } 254 | } 255 | 256 | private fun runRootCommand(cmd: String) { 257 | if (rootUtil.runRootCommandWithResult(cmd)) 258 | dismiss() 259 | else 260 | Toast.makeText(mContext, R.string.exec_fail, Toast.LENGTH_SHORT).show() 261 | } 262 | 263 | private fun funcRoot(listItemEnum: ListItemEnum) { 264 | when (listItemEnum) { 265 | ListItemEnum.LOCK_SCREEN -> runRootCommand("input keyevent KEYCODE_POWER") 266 | ListItemEnum.REBOOT -> runRootCommand("svc power reboot") 267 | ListItemEnum.SOFT_REBOOT -> runRootCommand("setprop ctl.restart zygote") 268 | ListItemEnum.SYSTEM_UI -> runRootCommand("pkill -f com.android.systemui") 269 | ListItemEnum.RECOVERY -> runRootCommand("svc power reboot recovery") 270 | ListItemEnum.BOOTLOADER -> runRootCommand("svc power reboot bootloader") 271 | ListItemEnum.SAFE_MODE -> { 272 | if (rootUtil.runRootCommandWithResult("setprop persist.sys.safemode 1")) 273 | runRootCommand("svc power reboot") 274 | } 275 | 276 | ListItemEnum.POWER_OFF -> runRootCommand("reboot -p") 277 | } 278 | } 279 | 280 | private fun funcShizuku(listItemEnum: ListItemEnum) { 281 | if (!shizukuUtil.checkShizukuPermission()) { 282 | Toast.makeText(mContext, R.string.shizuku_denied, Toast.LENGTH_SHORT).show() 283 | return 284 | } 285 | when (listItemEnum) { 286 | ListItemEnum.LOCK_SCREEN -> { 287 | val lockExitCode = shizukuUtil.runShizukuCommand( 288 | arrayOf("input", "keyevent", "KEYCODE_POWER"), 289 | false 290 | ) 291 | if (lockExitCode == 0) dismiss() 292 | } 293 | 294 | ListItemEnum.REBOOT -> shizukuUtil.shizukuReboot(null) 295 | ListItemEnum.SOFT_REBOOT -> shizukuUtil.runShizukuCommand( 296 | arrayOf( 297 | "setprop", 298 | "ctl.restart", 299 | "zygote" 300 | ), true 301 | ) 302 | 303 | ListItemEnum.SYSTEM_UI -> { 304 | val stopUiCode = shizukuUtil.runShizukuCommand( 305 | arrayOf( 306 | "am", 307 | "force-stop", 308 | "com.android.systemui" 309 | ), false 310 | ) 311 | if (stopUiCode == 0) { 312 | Toast.makeText(mContext, R.string.stop_system_ui_tip, Toast.LENGTH_LONG) 313 | .show() 314 | dismiss() 315 | } 316 | } 317 | 318 | ListItemEnum.RECOVERY -> shizukuUtil.runShizukuCommand( 319 | arrayOf("reboot", "recovery"), 320 | false 321 | ) 322 | 323 | ListItemEnum.BOOTLOADER -> shizukuUtil.shizukuReboot("bootloader") 324 | ListItemEnum.SAFE_MODE -> { 325 | val exitCode = shizukuUtil.runShizukuCommand( 326 | arrayOf("setprop", "persist.sys.safemode", "1"), 327 | true 328 | ) 329 | if (exitCode == 0) 330 | shizukuUtil.shizukuReboot(null) 331 | else 332 | Toast.makeText(mContext, R.string.exec_fail, Toast.LENGTH_SHORT).show() 333 | } 334 | 335 | ListItemEnum.POWER_OFF -> shizukuUtil.runShizukuCommand(arrayOf("reboot", "-p"), false) 336 | } 337 | } 338 | 339 | override fun onDismiss(dialog: DialogInterface) { 340 | super.onDismiss(dialog) 341 | /* 342 | 为什么使用 System.exit(0) ? 343 | 因为 KernelSU 授权后需要杀死并重启进程使权限生效。 344 | 通常 activity.finish() 仅限于关闭活动,进程由系统决定回收。 345 | 对于像本项目这样的单线程应用,这种做法是安全的。 346 | */ 347 | NyaShellManager.unbindService() 348 | if (!requireActivity().isChangingConfigurations) 349 | exitProcess(0) 350 | } 351 | 352 | } --------------------------------------------------------------------------------