├── 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 |
4 |
5 | # RebootNya
6 |
7 | [
](https://github.com/daisukiKaffuChino/RebootNya/releases)
11 |
12 | [](https://github.com/daisukiKaffuChino/RebootNya/releases/latest)
13 | [](https://crowdin.com/project/rebootnya)
14 | [](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 | 
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 |

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 |
4 |
5 | # RebootNya
6 |
7 | [
](https://github.com/daisukiKaffuChino/RebootNya/releases)
11 |
12 | [](https://github.com/daisukiKaffuChino/RebootNya/releases/latest)
13 | [](https://crowdin.com/project/rebootnya)
14 | [](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 | 
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 |

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 | }
--------------------------------------------------------------------------------