├── .gitignore
├── Android.bp
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ └── com
│ │ └── github
│ │ └── capntrips
│ │ └── bootcontrol
│ │ ├── BoolResult.aidl
│ │ ├── CommandResult.aidl
│ │ └── IBootControlService.aidl
│ ├── cpp
│ ├── CMakeLists.txt
│ └── libbootcontrol.cpp
│ ├── java
│ └── com
│ │ └── github
│ │ └── capntrips
│ │ └── bootcontrol
│ │ ├── BootControlService.kt
│ │ ├── BootControlVersion.kt
│ │ ├── DeviceState.kt
│ │ ├── ErrorScreen.kt
│ │ ├── MainActivity.kt
│ │ ├── MainContent.kt
│ │ ├── MainListener.kt
│ │ ├── MainViewModel.kt
│ │ ├── RebootContent.kt
│ │ ├── RebootViewModel.kt
│ │ ├── RefreshableScreen.kt
│ │ ├── SlotState.kt
│ │ └── ui
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ ├── jniLibs
│ ├── arm64-v8a
│ │ ├── ld-android.so
│ │ ├── libbase.so
│ │ ├── libbootcontrol.so
│ │ ├── libc++.so
│ │ ├── libcutils.so
│ │ ├── libdl_android.so
│ │ ├── libhidlbase.so
│ │ ├── libutils.so
│ │ └── libvndksupport.so
│ └── armeabi-v7a
│ │ ├── ld-android.so
│ │ ├── libbase.so
│ │ ├── libbootcontrol.so
│ │ ├── libc++.so
│ │ ├── libcutils.so
│ │ ├── libdl_android.so
│ │ ├── libhidlbase.so
│ │ ├── libutils.so
│ │ └── libvndksupport.so
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_monochrome.xml
│ ├── ic_splash_animation.xml
│ └── ic_splash_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── values-night
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ └── data_extraction_rules.xml
├── build.gradle
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/Android.bp:
--------------------------------------------------------------------------------
1 | android_app {
2 | name: "BootControl",
3 | srcs: [
4 | "app/src/main/java/**/*.kt",
5 | // TODO: fix setActiveBootSlot
6 | "app/src/main/java/**/*.java",
7 | "app/src/main/aidl/**/*.aidl",
8 | ],
9 | jni_libs: ["libbootcontrol"],
10 | resource_dirs: ["app/src/main/res"],
11 | manifest: "app/src/main/AndroidManifest.xml",
12 | static_libs: [
13 | "androidx.activity_activity-compose",
14 | "androidx.activity_activity-ktx",
15 | "androidx.appcompat_appcompat",
16 | "androidx.compose.foundation_foundation-layout",
17 | "androidx.compose.material3_material3",
18 | "androidx.compose.material_material-icons-extended",
19 | "androidx.compose.material_material",
20 | "androidx.compose.runtime_runtime",
21 | "androidx.compose.ui_ui",
22 | "androidx.core_core-ktx",
23 | "androidx.core_core-splashscreen",
24 | "androidx.lifecycle_lifecycle-runtime-ktx",
25 | "androidx.lifecycle_lifecycle-viewmodel-compose",
26 | "androidx.navigation_navigation-compose",
27 | "com.google.android.material_material",
28 | "com.github.topjohnwu.libsu_service",
29 | "com.github.topjohnwu.libsu_core",
30 | ],
31 | aidl: {
32 | local_include_dirs: ["app/src/main/aidl"],
33 | },
34 | // TODO: build for api 29
35 | platform_apis: true,
36 | use_embedded_native_libs: true,
37 | }
38 |
39 | cc_library_shared {
40 | name: "libbootcontrol",
41 | srcs: ["app/src/main/cpp/libbootcontrol.cpp"],
42 | static_libs: [
43 | "android.hardware.boot-V1-ndk",
44 | "android.hardware.boot@1.0",
45 | "android.hardware.boot@1.1",
46 | "android.hardware.boot@1.2",
47 | "libboot_control_client",
48 | ],
49 | shared_libs: [
50 | "libhidlbase",
51 | "libbinder_ndk",
52 | "libbase",
53 | "libcutils",
54 | "libutils",
55 | "liblog",
56 | ],
57 | cflags: [
58 | "-Wall",
59 | "-Werror",
60 | ],
61 | header_libs: ["jni_headers"],
62 | }
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 capntrips
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Boot Control
2 |
3 | Boot Control is an Android app that toggles the active boot slot.
4 |
5 | ## Usage
6 |
7 | Pressing the `Activate` button will toggle the active boot slot.
8 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | namespace 'com.github.capntrips.bootcontrol'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "com.github.capntrips.bootcontrol"
12 | minSdk 29
13 | targetSdk 34
14 | versionCode 3
15 | versionName "1.0.0-alpha03"
16 |
17 | vectorDrawables {
18 | useSupportLibrary true
19 | }
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled true
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_17
30 | targetCompatibility JavaVersion.VERSION_17
31 | }
32 | kotlinOptions {
33 | jvmTarget = "17"
34 | }
35 | // externalNativeBuild {
36 | // cmake {
37 | // path file('src/main/cpp/CMakeLists.txt')
38 | // version '3.22.1'
39 | // }
40 | // }
41 | buildFeatures {
42 | compose true
43 | aidl true
44 | }
45 | composeOptions {
46 | kotlinCompilerExtensionVersion libs.versions.compose.compiler.get()
47 | }
48 | packagingOptions {
49 | resources {
50 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
51 | }
52 | }
53 | }
54 |
55 | dependencies {
56 | implementation libs.androidx.activity.compose
57 | implementation libs.androidx.activity.ktx
58 | implementation libs.androidx.appcompat
59 | implementation libs.androidx.compose.foundation.layout
60 | implementation libs.androidx.compose.material3
61 | implementation libs.androidx.compose.material.icons.extended
62 | implementation libs.androidx.compose.material
63 | implementation libs.androidx.compose.ui
64 | implementation libs.androidx.core.ktx
65 | implementation libs.androidx.core.splashscreen
66 | implementation libs.androidx.lifecycle.runtime.ktx
67 | implementation libs.androidx.lifecycle.viewmodel.compose
68 | implementation libs.androidx.navigation.compose
69 | implementation libs.libsu.core
70 | implementation libs.libsu.service
71 | implementation libs.material
72 | }
73 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -dontobfuscate
2 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/aidl/com/github/capntrips/bootcontrol/BoolResult.aidl:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol;
2 |
3 | parcelable BoolResult {
4 | boolean value;
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/aidl/com/github/capntrips/bootcontrol/CommandResult.aidl:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol;
2 |
3 | parcelable CommandResult {
4 | boolean success;
5 | String errMsg;
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/aidl/com/github/capntrips/bootcontrol/IBootControlService.aidl:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol;
2 |
3 | import com.github.capntrips.bootcontrol.BoolResult;
4 | import com.github.capntrips.bootcontrol.CommandResult;
5 |
6 | interface IBootControlService {
7 | String halInfo();
8 | int halVersion();
9 | int getNumberSlots();
10 | int getCurrentSlot();
11 | CommandResult setActiveBootSlot(int slot);
12 | BoolResult isSlotBootable(int slot);
13 | BoolResult isSlotMarkedSuccessful(int slot);
14 | String getSuffix(int slot);
15 | int getActiveBootSlot();
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.22.1)
2 |
3 | project("bootcontrol")
4 |
5 | add_library(bootcontrol SHARED libbootcontrol.cpp)
6 |
7 | target_include_directories(bootcontrol PUBLIC include)
8 |
9 | find_library(log-lib log)
10 | target_link_libraries(bootcontrol ${log-lib})
11 |
--------------------------------------------------------------------------------
/app/src/main/cpp/libbootcontrol.cpp:
--------------------------------------------------------------------------------
1 | #define LOG_TAG "libbootcontrol"
2 |
3 | #include
4 |
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | #define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
11 | #define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
12 | #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
13 | #define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
14 | #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
15 |
16 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=20
17 | #include
18 | #include
19 |
20 | using android::hal::BootControlClient;
21 | using android::hal::BootControlVersion;
22 | using android::hal::CommandResult;
23 |
24 | class BootCtl {
25 | public:
26 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=229
27 | std::unique_ptr client;
28 | BootControlVersion bootVersion = BootControlVersion::BOOTCTL_V1_0;
29 |
30 | BootCtl() {
31 | client = android::hal::BootControlClient::WaitForService();
32 | if (client == nullptr) {
33 | ALOGE("Failed to get bootctl module.");
34 | return;
35 | }
36 | bootVersion = client->GetVersion();
37 | }
38 | };
39 |
40 | jobject commandResult(JNIEnv *env, bool b, std::string s) {
41 | auto cls = env->FindClass("com/github/capntrips/bootcontrol/CommandResult");
42 | auto constructor = env->GetMethodID(cls, "", "()V");
43 | auto success = env->GetFieldID(cls , "success", "Z");
44 | auto errMsg = env->GetFieldID(cls , "errMsg", "Ljava/lang/String;");
45 | auto ret = env->NewObject(cls, constructor);
46 | env->SetBooleanField(ret, success, b);
47 | jstring js = env->NewStringUTF(s.c_str());
48 | env->SetObjectField(ret, errMsg, js);
49 | return ret;
50 | }
51 |
52 | jobject boolResult(JNIEnv *env, bool b) {
53 | auto cls = env->FindClass("com/github/capntrips/bootcontrol/BoolResult");
54 | auto constructor = env->GetMethodID(cls, "", "()V");
55 | auto value = env->GetFieldID(cls , "value", "Z");
56 | auto ret = env->NewObject(cls, constructor);
57 | env->SetBooleanField(ret, value, b);
58 | return ret;
59 | }
60 |
61 | jobject handleReturn(JNIEnv *env, const std::optional& ret) {
62 | if (!ret.has_value()) {
63 | return nullptr;
64 | }
65 | return boolResult(env, ret.value());
66 | }
67 |
68 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=64
69 | static constexpr auto ToString(BootControlVersion ver) {
70 | switch (ver) {
71 | case BootControlVersion::BOOTCTL_V1_0:
72 | return "android.hardware.boot@1.0::IBootControl";
73 | case BootControlVersion::BOOTCTL_V1_1:
74 | return "android.hardware.boot@1.1::IBootControl";
75 | case BootControlVersion::BOOTCTL_V1_2:
76 | return "android.hardware.boot@1.2::IBootControl";
77 | case BootControlVersion::BOOTCTL_AIDL:
78 | return "android.hardware.boot@aidl::IBootControl";
79 | }
80 | }
81 |
82 | static constexpr auto ToInt(BootControlVersion ver) {
83 | switch (ver) {
84 | case BootControlVersion::BOOTCTL_V1_0:
85 | return 0;
86 | case BootControlVersion::BOOTCTL_V1_1:
87 | return 1;
88 | case BootControlVersion::BOOTCTL_V1_2:
89 | return 2;
90 | case BootControlVersion::BOOTCTL_AIDL:
91 | return 3;
92 | }
93 | }
94 |
95 |
96 | extern "C" {
97 |
98 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=77
99 | JNIEXPORT jstring JNICALL
100 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_halInfo(
101 | JNIEnv *env,
102 | jobject /* this */) {
103 | BootCtl bootctl;
104 | if (bootctl.client == nullptr) {
105 | return nullptr;
106 | }
107 | std::string info = ToString(bootctl.client->GetVersion());
108 | return env->NewStringUTF(info.c_str());
109 | }
110 |
111 | JNIEXPORT jint JNICALL
112 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_halVersion(
113 | JNIEnv * /* env */,
114 | jobject /* this */) {
115 | BootCtl bootctl;
116 | return ToInt(bootctl.client->GetVersion());
117 | }
118 |
119 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=82
120 | JNIEXPORT jint JNICALL
121 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_getNumberSlots(
122 | JNIEnv * /* env */,
123 | jobject /* this */) {
124 | BootCtl bootctl;
125 | return bootctl.client->GetNumSlots();
126 | }
127 |
128 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=88
129 | JNIEXPORT jint JNICALL
130 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_getCurrentSlot(
131 | JNIEnv * /* env */,
132 | jobject /* this */) {
133 | BootCtl bootctl;
134 | return bootctl.client->GetCurrentSlot();
135 | }
136 |
137 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=110
138 | JNIEXPORT jint JNICALL
139 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_getActiveBootSlot(
140 | JNIEnv * /* env */,
141 | jobject /* this */) {
142 | BootCtl bootctl;
143 | return bootctl.client->GetActiveBootSlot();
144 | }
145 |
146 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=116
147 | JNIEXPORT jobject JNICALL
148 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_setActiveBootSlot(
149 | JNIEnv *env,
150 | jobject /* this */,
151 | jint slot_number) {
152 | BootCtl bootctl;
153 | const auto cr = bootctl.client->SetActiveBootSlot(slot_number);
154 | return commandResult(env, cr.success, cr.errMsg);
155 | }
156 |
157 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=139
158 | JNIEXPORT jobject JNICALL
159 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_isSlotBootable(
160 | JNIEnv *env,
161 | jobject /* this */,
162 | jint slot_number) {
163 | BootCtl bootctl;
164 | return handleReturn(env, bootctl.client->IsSlotBootable(slot_number));
165 | }
166 |
167 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=144
168 | JNIEXPORT jobject JNICALL
169 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_isSlotMarkedSuccessful(
170 | JNIEnv *env,
171 | jobject /* this */,
172 | jint slot_number) {
173 | BootCtl bootctl;
174 | return handleReturn(env, bootctl.client->IsSlotMarkedSuccessful(slot_number));
175 | }
176 |
177 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:system/extras/bootctl/bootctl.cpp;l=202
178 | JNIEXPORT jstring JNICALL
179 | Java_com_github_capntrips_bootcontrol_BootControlService_00024BootControlIPC_getSuffix(
180 | JNIEnv *env,
181 | jobject /* this */,
182 | jint slot_number) {
183 | BootCtl bootctl;
184 | const auto ret = bootctl.client->GetSuffix(slot_number);
185 | if (ret.empty()) {
186 | return nullptr;
187 | }
188 | return env->NewStringUTF(ret.c_str());
189 | }
190 |
191 | } // extern "C"
192 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/BootControlService.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.content.Intent
4 | import android.os.IBinder
5 | import android.os.Process
6 | import com.topjohnwu.superuser.ipc.RootService
7 |
8 | class BootControlService : RootService() {
9 | inner class BootControlIPC : IBootControlService.Stub() {
10 | init {
11 | if (Process.myUid() == 0) {
12 | System.loadLibrary("bootcontrol")
13 | }
14 | }
15 |
16 | external override fun halInfo(): String
17 | external override fun halVersion(): Int
18 | external override fun getNumberSlots(): Int
19 | external override fun getCurrentSlot(): Int
20 | external override fun setActiveBootSlot(slot: Int): CommandResult
21 | external override fun isSlotBootable(slot: Int): BoolResult
22 | external override fun isSlotMarkedSuccessful(slot: Int): BoolResult
23 | external override fun getSuffix(slot: Int): String?
24 | external override fun getActiveBootSlot(): Int
25 | }
26 |
27 | override fun onBind(intent: Intent): IBinder {
28 | return BootControlIPC()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/BootControlVersion.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | enum class BootControlVersion {
4 | BOOTCTL_V1_0,
5 | BOOTCTL_V1_1,
6 | BOOTCTL_V1_2,
7 | BOOTCTL_AIDL
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/DeviceState.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.widget.Toast
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.launch
13 |
14 |
15 | class DeviceState(
16 | context: Context,
17 | private val bootctl: IBootControlService,
18 | private val _isRefreshing : MutableStateFlow
19 | ) : ViewModel() {
20 | companion object {
21 | const val TAG: String = "BootControl/BootControl"
22 | }
23 |
24 | private var _slotA: MutableStateFlow
25 | private var _slotB: MutableStateFlow
26 | val halInfo: String
27 | val halVersion: BootControlVersion
28 | val slotSuffix: String
29 | var initialized: Boolean = false
30 | private set
31 |
32 | val slotA: StateFlow
33 | get() = _slotA.asStateFlow()
34 | val slotB: StateFlow
35 | get() = _slotB.asStateFlow()
36 |
37 | private fun _refresh() {
38 | slotA.value.unbootable = !isSlotBootable(0)
39 | slotA.value.successful = isSlotMarkedSuccessful(0)
40 | slotB.value.unbootable = !isSlotBootable(1)
41 | slotB.value.successful = isSlotMarkedSuccessful(1)
42 | if (halVersion >= BootControlVersion.BOOTCTL_V1_2) {
43 | val activeSlot = getActiveBootSlot()
44 | slotA.value.active = activeSlot == 0
45 | slotB.value.active = activeSlot == 1
46 | }
47 | }
48 |
49 | fun refresh() {
50 | launch {
51 | _refresh()
52 | }
53 | }
54 |
55 | init {
56 | _slotA = MutableStateFlow(SlotState())
57 | _slotB = MutableStateFlow(SlotState())
58 | halInfo = halInfo()
59 | halVersion = halVersion()
60 | slotSuffix = getSuffix(getCurrentSlot())
61 | _refresh()
62 | initialized = true
63 | }
64 |
65 | private fun launch(block: suspend () -> Unit) {
66 | viewModelScope.launch(Dispatchers.IO) {
67 | _isRefreshing.emit(true)
68 | block()
69 | _isRefreshing.emit(false)
70 | }
71 | }
72 |
73 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
74 | viewModelScope.launch(Dispatchers.Main) {
75 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
76 | }
77 | Log.d(TAG, message)
78 | if (shouldThrow) {
79 | throw Exception(message)
80 | }
81 | }
82 |
83 | private fun halInfo(): String {
84 | return bootctl.halInfo()
85 | }
86 |
87 | private fun halVersion(): BootControlVersion {
88 | return BootControlVersion.entries[bootctl.halVersion()]
89 | }
90 |
91 | private fun getCurrentSlot(): Int {
92 | return bootctl.getCurrentSlot()
93 | }
94 |
95 | fun setActiveBootSlot(slot: Int): Boolean {
96 | return bootctl.setActiveBootSlot(slot).success
97 | }
98 |
99 | private fun isSlotBootable(slot: Int): Boolean {
100 | return bootctl.isSlotBootable(slot).value
101 | }
102 |
103 | private fun isSlotMarkedSuccessful(slot: Int): Boolean {
104 | return bootctl.isSlotMarkedSuccessful(slot).value
105 | }
106 |
107 | private fun getSuffix(slot: Int): String {
108 | return bootctl.getSuffix(slot)
109 | }
110 |
111 | private fun getActiveBootSlot(): Int {
112 | return bootctl.getActiveBootSlot()
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/ErrorScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Warning
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Scaffold
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.unit.dp
22 | import com.github.capntrips.bootcontrol.ui.theme.Orange500
23 |
24 | @ExperimentalMaterial3Api
25 | @Composable
26 | fun ErrorScreen(message: String) {
27 | Scaffold { contentPadding ->
28 | Box(
29 | contentAlignment = Alignment.Center,
30 | modifier = Modifier
31 | .padding(contentPadding)
32 | .fillMaxSize()
33 | ) {
34 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
35 | Icon(
36 | modifier = Modifier
37 | .width(36.dp)
38 | .height(36.dp),
39 | imageVector = Icons.Filled.Warning,
40 | tint = Orange500,
41 | contentDescription = message
42 | )
43 | Spacer(Modifier.height(8.dp))
44 | Text(
45 | message,
46 | modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp),
47 | textAlign = TextAlign.Center,
48 | style = MaterialTheme.typography.titleLarge
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.animation.ObjectAnimator
4 | import android.animation.PropertyValuesHolder
5 | import android.content.ComponentName
6 | import android.content.Intent
7 | import android.content.ServiceConnection
8 | import android.os.Bundle
9 | import android.os.IBinder
10 | import android.view.View
11 | import android.view.ViewTreeObserver
12 | import android.view.animation.AccelerateInterpolator
13 | import androidx.activity.ComponentActivity
14 | import androidx.activity.compose.BackHandler
15 | import androidx.activity.compose.setContent
16 | import androidx.compose.animation.ExperimentalAnimationApi
17 | import androidx.compose.material.ExperimentalMaterialApi
18 | import androidx.compose.material3.ExperimentalMaterial3Api
19 | import androidx.compose.runtime.collectAsState
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.core.animation.doOnEnd
23 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
24 | import androidx.core.view.WindowCompat
25 | import androidx.navigation.compose.NavHost
26 | import androidx.navigation.compose.composable
27 | import androidx.navigation.compose.rememberNavController
28 | import com.github.capntrips.bootcontrol.ui.theme.BootControlTheme
29 | import com.topjohnwu.superuser.Shell
30 | import com.topjohnwu.superuser.ipc.RootService
31 |
32 |
33 | @ExperimentalAnimationApi
34 | @ExperimentalMaterialApi
35 | class MainActivity : ComponentActivity() {
36 | private var viewModel: MainViewModel? = null
37 | private lateinit var mainListener: MainListener
38 | private var bootctlFailure = false;
39 |
40 | inner class BootControlConnection : ServiceConnection {
41 | @ExperimentalMaterial3Api
42 | override fun onServiceConnected(name: ComponentName, service: IBinder) {
43 | onBootControlServiceConnected(IBootControlService.Stub.asInterface(service))
44 | }
45 |
46 | @ExperimentalMaterial3Api
47 | override fun onServiceDisconnected(name: ComponentName) {
48 | setContent {
49 | BootControlTheme {
50 | ErrorScreen(stringResource(R.string.root_service_disconnected))
51 | }
52 | }
53 | }
54 | }
55 |
56 | @ExperimentalMaterial3Api
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | WindowCompat.setDecorFitsSystemWindows(window, false)
59 | val splashScreen = installSplashScreen()
60 | super.onCreate(savedInstanceState)
61 |
62 | splashScreen.setOnExitAnimationListener { splashScreenView ->
63 | val scale = ObjectAnimator.ofPropertyValuesHolder(
64 | splashScreenView.view,
65 | PropertyValuesHolder.ofFloat(
66 | View.SCALE_X,
67 | 1f,
68 | 0f
69 | ),
70 | PropertyValuesHolder.ofFloat(
71 | View.SCALE_Y,
72 | 1f,
73 | 0f
74 | )
75 | )
76 | scale.interpolator = AccelerateInterpolator()
77 | scale.duration = 250L
78 | scale.doOnEnd { splashScreenView.remove() }
79 | scale.start()
80 | }
81 |
82 | val content: View = findViewById(android.R.id.content)
83 | content.viewTreeObserver.addOnPreDrawListener(
84 | object : ViewTreeObserver.OnPreDrawListener {
85 | override fun onPreDraw(): Boolean {
86 | return if (viewModel?.isRefreshing?.value == false || bootctlFailure || Shell.isAppGrantedRoot() == false) {
87 | content.viewTreeObserver.removeOnPreDrawListener(this)
88 | true
89 | } else {
90 | false
91 | }
92 | }
93 | }
94 | )
95 |
96 | Shell.getShell()
97 | if (Shell.isAppGrantedRoot()!!) {
98 | val intent = Intent(this, BootControlService::class.java)
99 | RootService.bind(intent, BootControlConnection())
100 | } else {
101 | setContent {
102 | BootControlTheme {
103 | ErrorScreen(stringResource(R.string.root_required))
104 | }
105 | }
106 | }
107 | }
108 |
109 | @ExperimentalMaterial3Api
110 | fun onBootControlServiceConnected(bootctl: IBootControlService) {
111 | setContent {
112 | BootControlTheme {
113 | if (bootctl.halInfo() != null) {
114 | val navController = rememberNavController()
115 | viewModel = MainViewModel(this, bootctl, navController)
116 | if (!viewModel!!.hasError) {
117 | mainListener = MainListener {
118 | viewModel!!.refresh()
119 | }
120 | val rebootViewModel = viewModel!!.reboot
121 | val isRefreshing by viewModel!!.isRefreshing.collectAsState()
122 | BackHandler(enabled = isRefreshing, onBack = {})
123 | NavHost(navController = navController, startDestination = "main") {
124 | composable("main") {
125 | RefreshableScreen(viewModel!!, navController, swipeEnabled = true) {
126 | MainContent(viewModel!!, navController)
127 | }
128 | }
129 | composable("reboot") {
130 | RefreshableScreen(viewModel!!, navController) {
131 | RebootContent(rebootViewModel)
132 | }
133 | }
134 | composable("error/{error}") { backStackEntry ->
135 | val error = backStackEntry.arguments?.getString("error")
136 | ErrorScreen(error!!)
137 | }
138 | }
139 | } else {
140 | ErrorScreen(viewModel!!.error)
141 | }
142 | } else {
143 | bootctlFailure = true
144 | ErrorScreen(stringResource(R.string.bootctl_failed))
145 | }
146 | }
147 | }
148 | }
149 |
150 | public override fun onResume() {
151 | super.onResume()
152 | if (this::mainListener.isInitialized) {
153 | mainListener.resume()
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/MainContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.os.Build
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.foundation.BorderStroke
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.ColumnScope
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.layout.width
17 | import androidx.compose.foundation.shape.RoundedCornerShape
18 | import androidx.compose.foundation.text.selection.SelectionContainer
19 | import androidx.compose.material3.Button
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.OutlinedButton
22 | import androidx.compose.material3.Surface
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.graphics.Shape
31 | import androidx.compose.ui.platform.LocalContext
32 | import androidx.compose.ui.res.stringResource
33 | import androidx.compose.ui.text.TextStyle
34 | import androidx.compose.ui.unit.Dp
35 | import androidx.compose.ui.unit.dp
36 | import androidx.navigation.NavController
37 | import kotlinx.coroutines.flow.StateFlow
38 |
39 | @Composable
40 | fun ColumnScope.MainContent(
41 | viewModel: MainViewModel,
42 | navController: NavController,
43 | ) {
44 | val isRefreshing by viewModel.isRefreshing.collectAsState()
45 | val uiState by viewModel.uiState.collectAsState()
46 | DataCard (title = stringResource(R.string.device)) {
47 | DataRow(
48 | label = stringResource(R.string.model),
49 | value = "${Build.MODEL} (${Build.DEVICE})"
50 | )
51 | DataRow(
52 | label = stringResource(R.string.build_number),
53 | value = Build.ID
54 | )
55 | DataRow(
56 | label = stringResource(R.string.hal_version),
57 | value = uiState.halInfo
58 | )
59 | DataRow(
60 | label = stringResource(R.string.slot_suffix),
61 | value = uiState.slotSuffix
62 | )
63 | }
64 | Spacer(Modifier.height(16.dp))
65 | SlotCard(
66 | title = stringResource(R.string.slot_a),
67 | viewModel=viewModel,
68 | navController=navController,
69 | slotStateFlow = uiState.slotA,
70 | isActive = uiState.slotSuffix == "_a",
71 | initialized = uiState.initialized,
72 | halVersion = uiState.halVersion,
73 | )
74 | Spacer(Modifier.height(16.dp))
75 | SlotCard(
76 | title = stringResource(R.string.slot_b),
77 | viewModel=viewModel,
78 | navController=navController,
79 | slotStateFlow = uiState.slotB,
80 | isActive = uiState.slotSuffix == "_b",
81 | initialized = uiState.initialized,
82 | halVersion = uiState.halVersion,
83 | )
84 | Spacer(Modifier.height(16.dp))
85 | AnimatedVisibility(
86 | !isRefreshing,
87 | enter = fadeIn(),
88 | exit = fadeOut(),
89 | ) {
90 | OutlinedButton(
91 | modifier = Modifier
92 | .fillMaxWidth(),
93 | shape = RoundedCornerShape(4.dp),
94 | onClick = { navController.navigate("reboot") }
95 | ) {
96 | Text(stringResource(R.string.reboot))
97 | }
98 | }
99 | }
100 |
101 | @Composable
102 | fun SlotCard(
103 | title: String,
104 | viewModel: MainViewModel,
105 | navController: NavController,
106 | slotStateFlow: StateFlow,
107 | isActive: Boolean,
108 | initialized: Boolean,
109 | halVersion: BootControlVersion,
110 | ) {
111 | // TODO: hoist state?
112 | val slot by slotStateFlow.collectAsState()
113 | val isRefreshing by viewModel.isRefreshing.collectAsState()
114 | DataCard (
115 | title = title,
116 | button = {
117 | AnimatedVisibility(
118 | initialized && !isRefreshing && if (halVersion >= BootControlVersion.BOOTCTL_V1_2) !slot.active else !isActive,
119 | enter = fadeIn(),
120 | exit = fadeOut(),
121 | ) {
122 | ActivateButton(viewModel, slot, navController)
123 | }
124 | }
125 | ) {
126 | AnimatedVisibility(initialized) {
127 | Column {
128 | HasStatusDataRow(
129 | label = stringResource(R.string.unbootable),
130 | value = stringResource(if (slot.unbootable) R.string.yes else R.string.no),
131 | hasStatus = !slot.unbootable
132 | )
133 | HasStatusDataRow(
134 | label = stringResource(R.string.successful),
135 | value = stringResource(if (slot.successful) R.string.yes else R.string.no),
136 | hasStatus = slot.successful
137 | )
138 | if (halVersion >= BootControlVersion.BOOTCTL_V1_2) {
139 | HasStatusDataRow(
140 | label = stringResource(R.string.active),
141 | value = stringResource(if (slot.active) R.string.yes else R.string.no),
142 | hasStatus = if (isActive) slot.active else !slot.active
143 | )
144 | }
145 | }
146 | }
147 | }
148 | }
149 |
150 | @Composable
151 | fun ActivateButton(
152 | viewModel: MainViewModel,
153 | slot: SlotState,
154 | navController: NavController,
155 | ) {
156 | val context = LocalContext.current
157 | Button(
158 | modifier = Modifier.padding(0.dp),
159 | shape = RoundedCornerShape(4.0.dp),
160 | onClick = { viewModel.activate(context, slot) { navController.navigate("reboot") } }
161 | ) {
162 | Text(stringResource(R.string.activate))
163 | }
164 | }
165 |
166 | @Composable
167 | fun DataCard(
168 | title: String,
169 | button: @Composable (() -> Unit)? = null,
170 | content: @Composable (ColumnScope.() -> Unit)
171 | ) {
172 | Card {
173 | Row(
174 | modifier = Modifier
175 | .fillMaxWidth()
176 | .padding(0.dp),
177 | horizontalArrangement = Arrangement.SpaceBetween,
178 | verticalAlignment = Alignment.CenterVertically
179 | ) {
180 | Text(
181 | modifier = Modifier.padding(0.dp, 9.dp, 8.dp, 9.dp),
182 | text = title,
183 | color = MaterialTheme.colorScheme.primary,
184 | style = MaterialTheme.typography.titleLarge
185 | )
186 | if (button != null) {
187 | button()
188 | }
189 | }
190 | Spacer(Modifier.height(10.dp))
191 | content()
192 | }
193 | }
194 |
195 | // TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android
196 | @Composable
197 | fun Card(
198 | shape: Shape = RoundedCornerShape(4.dp),
199 | backgroundColor: Color = MaterialTheme.colorScheme.surface,
200 | contentColor: Color = MaterialTheme.colorScheme.onSurface,
201 | border: BorderStroke? = null,
202 | tonalElevation: Dp = 2.dp,
203 | shadowElevation: Dp = 1.dp,
204 | content: @Composable ColumnScope.() -> Unit
205 | ) {
206 | Surface(
207 | shape = shape,
208 | color = backgroundColor,
209 | contentColor = contentColor,
210 | tonalElevation = tonalElevation,
211 | shadowElevation = shadowElevation,
212 | border = border
213 | ) {
214 | Column(
215 | modifier = Modifier
216 | .fillMaxWidth()
217 | .padding(18.dp, (13.788).dp, 18.dp, 18.dp),
218 | content = content
219 | )
220 | }
221 | }
222 |
223 | @Composable
224 | fun DataRow(
225 | label: String,
226 | value: String,
227 | valueColor: Color = Color.Unspecified,
228 | valueStyle: TextStyle = MaterialTheme.typography.titleSmall
229 | ) {
230 | Row {
231 | Text(
232 | modifier = Modifier.alignByBaseline(),
233 | text = label,
234 | style = MaterialTheme.typography.labelMedium
235 | )
236 | Spacer(Modifier.width(8.dp))
237 | SelectionContainer(Modifier.alignByBaseline()) {
238 | Text(
239 | modifier = Modifier.alignByBaseline(),
240 | text = value,
241 | color = valueColor,
242 | style = valueStyle
243 | )
244 | }
245 | }
246 | }
247 |
248 | @Composable
249 | fun HasStatusDataRow(
250 | label: String,
251 | value: String,
252 | hasStatus: Boolean
253 | ) {
254 | DataRow(
255 | label = label,
256 | value = value,
257 | valueColor = if (hasStatus) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
258 | )
259 | }
260 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/MainListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | internal class MainListener(private val callback: () -> Unit) {
4 | fun resume() {
5 | callback.invoke()
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.widget.Toast
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.navigation.NavHostController
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.withContext
15 |
16 | class MainViewModel(
17 | context: Context,
18 | bootctl: IBootControlService,
19 | navController: NavHostController,
20 | ) : ViewModel() {
21 | companion object {
22 | const val TAG: String = "BootControl/MainViewModel"
23 | }
24 |
25 | val reboot: RebootViewModel
26 |
27 | private val _isRefreshing: MutableStateFlow = MutableStateFlow(false)
28 | private lateinit var _uiState: MutableStateFlow
29 | private var _error: String? = null
30 |
31 | val isRefreshing: StateFlow
32 | get() = _isRefreshing.asStateFlow()
33 | val uiState: StateFlow
34 | get() = _uiState.asStateFlow()
35 | val hasError: Boolean
36 | get() = _error != null
37 | val error: String
38 | get() = _error ?: "Unknown Error"
39 |
40 | init {
41 | try {
42 | _uiState = MutableStateFlow(DeviceState(context, bootctl, _isRefreshing))
43 | } catch (e: Exception) {
44 | Log.e(TAG, e.message, e)
45 | _error = e.message
46 | }
47 | reboot = RebootViewModel(navController, _isRefreshing)
48 | }
49 |
50 | private fun launch(block: suspend () -> Unit) {
51 | viewModelScope.launch(Dispatchers.IO) {
52 | _isRefreshing.emit(true)
53 | try {
54 | block()
55 | } catch (e: Exception) {
56 | Log.e(TAG, e.message, e)
57 | _error = e.message
58 | }
59 | _isRefreshing.emit(false)
60 | }
61 | }
62 |
63 | fun refresh() {
64 | launch {
65 | uiState.value.refresh()
66 | }
67 | }
68 |
69 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
70 | viewModelScope.launch(Dispatchers.Main) {
71 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
72 | }
73 | Log.d(TAG, message)
74 | if (shouldThrow) {
75 | throw Exception(message)
76 | }
77 | }
78 |
79 | fun activate(context: Context, slot: SlotState, callback: () -> Unit) {
80 | launch {
81 | val slotA = uiState.value.slotA.value
82 | val slotB = uiState.value.slotB.value
83 | if (slotA == slot) {
84 | uiState.value.setActiveBootSlot(0)
85 | } else if (slotB == slot) {
86 | uiState.value.setActiveBootSlot(1)
87 | } else {
88 | log(context, "Invalid slot", shouldThrow = true)
89 | }
90 | log(context, "slot activated")
91 | uiState.value.refresh()
92 | withContext(Dispatchers.Main) {
93 | callback.invoke()
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/RebootContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.os.Build
4 | import android.os.PowerManager
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.OutlinedButton
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 |
16 | @Suppress("UnusedReceiverParameter")
17 | @Composable
18 | fun ColumnScope.RebootContent(
19 | viewModel: RebootViewModel
20 | ) {
21 | val context = LocalContext.current
22 | OutlinedButton(
23 | modifier = Modifier
24 | .fillMaxWidth(),
25 | shape = RoundedCornerShape(4.dp),
26 | onClick = { viewModel.rebootSystem() }
27 | ) {
28 | Text(stringResource(R.string.reboot))
29 | }
30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) {
31 | OutlinedButton(
32 | modifier = Modifier
33 | .fillMaxWidth(),
34 | shape = RoundedCornerShape(4.dp),
35 | onClick = { viewModel.rebootUserspace() }
36 | ) {
37 | Text(stringResource(R.string.reboot_userspace))
38 | }
39 | }
40 | OutlinedButton(
41 | modifier = Modifier
42 | .fillMaxWidth(),
43 | shape = RoundedCornerShape(4.dp),
44 | onClick = { viewModel.rebootRecovery() }
45 | ) {
46 | Text(stringResource(R.string.reboot_recovery))
47 | }
48 | OutlinedButton(
49 | modifier = Modifier
50 | .fillMaxWidth(),
51 | shape = RoundedCornerShape(4.dp),
52 | onClick = { viewModel.rebootBootloader() }
53 | ) {
54 | Text(stringResource(R.string.reboot_bootloader))
55 | }
56 | OutlinedButton(
57 | modifier = Modifier
58 | .fillMaxWidth(),
59 | shape = RoundedCornerShape(4.dp),
60 | onClick = { viewModel.rebootDownload() }
61 | ) {
62 | Text(stringResource(R.string.reboot_download))
63 | }
64 | OutlinedButton(
65 | modifier = Modifier
66 | .fillMaxWidth(),
67 | shape = RoundedCornerShape(4.dp),
68 | onClick = { viewModel.rebootEdl() }
69 | ) {
70 | Text(stringResource(R.string.reboot_edl))
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/RebootViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.navigation.NavController
7 | import com.topjohnwu.superuser.Shell
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 |
13 | class RebootViewModel(
14 | private val navController: NavController,
15 | private val _isRefreshing : MutableStateFlow
16 | ) : ViewModel() {
17 | companion object {
18 | const val TAG: String = "KernelFlasher/RebootState"
19 | }
20 |
21 | private fun launch(block: suspend () -> Unit) {
22 | viewModelScope.launch(Dispatchers.IO) {
23 | _isRefreshing.value = true
24 | try {
25 | block()
26 | } catch (e: Exception) {
27 | withContext (Dispatchers.Main) {
28 | Log.e(TAG, e.message, e)
29 | navController.navigate("error/${e.message}") {
30 | popUpTo("main")
31 | }
32 | }
33 | }
34 | _isRefreshing.value = false
35 | }
36 | }
37 |
38 | private fun reboot(destination: String = "") {
39 | launch {
40 | // https://github.com/topjohnwu/Magisk/blob/v25.2/app/src/main/java/com/topjohnwu/magisk/ktx/XSU.kt#L11-L15
41 | if (destination == "recovery") {
42 | // https://github.com/topjohnwu/Magisk/pull/5637
43 | Shell.cmd("/system/bin/input keyevent 26").submit()
44 | }
45 | Shell.cmd("/system/bin/svc power reboot $destination || /system/bin/reboot $destination").submit()
46 | }
47 | }
48 |
49 | fun rebootSystem() {
50 | reboot()
51 | }
52 |
53 | fun rebootUserspace() {
54 | reboot("userspace")
55 | }
56 |
57 | fun rebootRecovery() {
58 | reboot("recovery")
59 | }
60 |
61 | fun rebootBootloader() {
62 | reboot("bootloader")
63 | }
64 |
65 | fun rebootDownload() {
66 | reboot("download")
67 | }
68 |
69 | fun rebootEdl() {
70 | reboot("edl")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/RefreshableScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.ColumnScope
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.WindowInsets
12 | import androidx.compose.foundation.layout.asPaddingValues
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.navigationBars
17 | import androidx.compose.foundation.layout.padding
18 | import androidx.compose.foundation.rememberScrollState
19 | import androidx.compose.foundation.verticalScroll
20 | import androidx.compose.material.ExperimentalMaterialApi
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
23 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
24 | import androidx.compose.material.pullrefresh.pullRefresh
25 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
26 | import androidx.compose.material3.CenterAlignedTopAppBar
27 | import androidx.compose.material3.ExperimentalMaterial3Api
28 | import androidx.compose.material3.Icon
29 | import androidx.compose.material3.IconButton
30 | import androidx.compose.material3.MaterialTheme
31 | import androidx.compose.material3.Scaffold
32 | import androidx.compose.material3.Text
33 | import androidx.compose.runtime.Composable
34 | import androidx.compose.runtime.collectAsState
35 | import androidx.compose.runtime.getValue
36 | import androidx.compose.ui.Alignment
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.res.stringResource
39 | import androidx.compose.ui.unit.dp
40 | import androidx.navigation.NavController
41 |
42 | @ExperimentalMaterialApi
43 | @ExperimentalAnimationApi
44 | @ExperimentalMaterial3Api
45 | @Composable
46 | fun RefreshableScreen(
47 | viewModel: MainViewModel,
48 | navController: NavController,
49 | swipeEnabled: Boolean = false,
50 | content: @Composable ColumnScope.() -> Unit
51 | ) {
52 | val isRefreshing by viewModel.isRefreshing.collectAsState()
53 | val state = rememberPullRefreshState(isRefreshing, onRefresh = {
54 | viewModel.refresh()
55 | })
56 | // TODO: Is WindowInsets.statusBars automatically applied to Scaffold?
57 | // val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues()
58 | val navigationBars = WindowInsets.navigationBars.asPaddingValues()
59 | Scaffold(
60 | topBar = {
61 | CenterAlignedTopAppBar(
62 | // modifier = Modifier.padding(statusBar),
63 | title = {
64 | Text(
65 | text = stringResource(R.string.app_name),
66 | style = MaterialTheme.typography.headlineSmall
67 | )
68 | },
69 | navigationIcon = {
70 | if (navController.previousBackStackEntry != null) {
71 | AnimatedVisibility(
72 | !isRefreshing,
73 | enter = fadeIn(),
74 | exit = fadeOut()
75 | ) {
76 | IconButton(
77 | onClick = { navController.popBackStack() },
78 | ) {
79 | Icon(
80 | Icons.AutoMirrored.Filled.ArrowBack,
81 | contentDescription = stringResource(R.string.back),
82 | tint = MaterialTheme.colorScheme.onSurface
83 | )
84 | }
85 | }
86 | }
87 | },
88 | )
89 | },
90 | bottomBar = {
91 | Spacer(
92 | Modifier
93 | .height(navigationBars.calculateBottomPadding())
94 | .fillMaxWidth())
95 | },
96 |
97 | ) { contentPadding ->
98 | Box(
99 | modifier = Modifier
100 | .padding(contentPadding)
101 | .pullRefresh(state, swipeEnabled)
102 | .fillMaxSize(),
103 | ) {
104 | Column(
105 | modifier = Modifier
106 | .padding(16.dp, 0.dp, 16.dp, 16.dp)
107 | .fillMaxSize()
108 | .verticalScroll(rememberScrollState()),
109 | content = content
110 | )
111 | PullRefreshIndicator(
112 | isRefreshing,
113 | state,
114 | Modifier.align(Alignment.TopCenter),
115 | MaterialTheme.colorScheme.background,
116 | MaterialTheme.colorScheme.primaryContainer,
117 | true
118 | )
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/SlotState.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol
2 |
3 | class SlotState(
4 | var unbootable: Boolean = false,
5 | var successful: Boolean = true,
6 | var active: Boolean = false
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Orange500 = Color(0xFFFF9800)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol.ui.theme
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | @SuppressLint("ObsoleteSdkInt")
15 | @Composable
16 | fun BootControlTheme(
17 | isDarkTheme: Boolean = isSystemInDarkTheme(),
18 | isDynamicColor: Boolean = true,
19 | content: @Composable () -> Unit
20 | ) {
21 | val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
22 | val colorScheme = if (dynamicColor) {
23 | if (isDarkTheme) {
24 | dynamicDarkColorScheme(LocalContext.current)
25 | } else {
26 | dynamicLightColorScheme(LocalContext.current)
27 | }
28 | } else {
29 | if (isDarkTheme) {
30 | darkColorScheme()
31 | } else {
32 | lightColorScheme()
33 | }
34 | }
35 |
36 | MaterialTheme(
37 | colorScheme = colorScheme,
38 | typography = typography(colorScheme),
39 | content = content
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/bootcontrol/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.bootcontrol.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.material3.Typography
5 | import androidx.compose.runtime.Composable
6 |
7 | @Composable
8 | fun typography(colorScheme: ColorScheme) : Typography {
9 | return Typography(
10 | labelMedium = Typography().labelMedium.copy(
11 | color = colorScheme.onSurface.copy(alpha = 0.667f)
12 | )
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/ld-android.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/ld-android.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libbase.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libbase.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libbootcontrol.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libbootcontrol.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libc++.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libc++.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libcutils.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libcutils.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libdl_android.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libdl_android.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libhidlbase.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libhidlbase.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libutils.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libutils.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libvndksupport.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/arm64-v8a/libvndksupport.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/ld-android.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/ld-android.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libbase.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libbase.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libbootcontrol.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libbootcontrol.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libc++.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libc++.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libcutils.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libcutils.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libdl_android.so:
--------------------------------------------------------------------------------
1 | /apex/com.android.runtime/lib/bionic/libdl_android.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libhidlbase.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libhidlbase.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libutils.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libutils.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libvndksupport.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/app/src/main/jniLibs/armeabi-v7a/libvndksupport.so
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_splash_animation.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
17 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_splash_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Boot Control
4 | Root is required
5 | Root service disconnected
6 | Failed to get bootctl module.
7 | Device
8 | Model
9 | Build Number
10 | HAL Version
11 | Slot Suffix
12 | Slot A
13 | Slot B
14 | Unbootable
15 | Successful
16 | Active
17 | Yes
18 | No
19 | Activate
20 | Back
21 | Reboot
22 | Soft Reboot
23 | Reboot to Recovery
24 | Reboot to Bootloader
25 | Reboot to Download
26 | Reboot to EDL
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
29 |
30 |
36 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | dependencies {
7 | classpath libs.gradle
8 | classpath libs.kotlin.gradle.plugin
9 | }
10 | }
11 |
12 | tasks.register('clean', Delete) {
13 | delete rootProject.layout.buildDirectory
14 | }
15 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 | kotlin.code.style=official
5 | android.nonTransitiveRClass=false
6 | android.nonFinalResIds=false
7 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | activityCompose = "1.8.2"
3 | appcompat = "1.6.1"
4 | compose = "1.6.4"
5 | compose-compiler = "1.5.11"
6 | coreKtx = "1.12.0"
7 | coreSplashscreen = "1.0.1"
8 | foundation = "1.6.4"
9 | gradle = "8.3.1"
10 | kotlinGradlePlugin = "1.9.23"
11 | libsu = "5.2.2"
12 | lifecycleRuntimeKtx = "2.7.0"
13 | material = "1.11.0"
14 | material3 = "1.2.1"
15 | navigationCompose = "2.7.7"
16 |
17 | [libraries]
18 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
19 | androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityCompose" }
20 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
21 | androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "foundation" }
22 | androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
23 | androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
24 | androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
25 | androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
26 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
27 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
28 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
29 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
30 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
31 | gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
32 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" }
33 | libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
34 | libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }
35 | material = { module = "com.google.android.material:material", version.ref = "material" }
36 |
37 | [plugins]
38 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capntrips/BootControl/127984a5a3c747d8c0c5b948bfd6082bb78f7ced/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | maven { url 'https://jitpack.io' }
7 | }
8 | }
9 | rootProject.name = "Boot Control"
10 | include ':app'
11 |
--------------------------------------------------------------------------------