├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── yangfentuozi │ │ └── runner │ │ └── service │ │ ├── IService.aidl │ │ ├── callback │ │ ├── IExecResultCallback.aidl │ │ └── IInstallTermExtCallback.aidl │ │ └── data │ │ ├── CommandInfo.aidl │ │ ├── EnvInfo.aidl │ │ ├── ProcessInfo.aidl │ │ └── TermExtVersion.aidl │ ├── cpp │ ├── CMakeLists.txt │ ├── processutils.c │ └── starter.c │ ├── java │ └── yangfentuozi │ │ └── runner │ │ ├── App.kt │ │ ├── Runner.kt │ │ ├── appservice │ │ └── ExecOnBootService.kt │ │ ├── base │ │ ├── BaseActivity.kt │ │ ├── BaseDialogBuilder.kt │ │ └── BaseFragment.kt │ │ ├── receiver │ │ └── BootCompleteReceiver.kt │ │ ├── service │ │ ├── ServiceImpl.kt │ │ ├── callback │ │ │ ├── ExecResultCallback.kt │ │ │ └── InstallTermExtCallback.kt │ │ ├── data │ │ │ ├── CommandInfo.kt │ │ │ ├── EnvInfo.kt │ │ │ ├── ProcessInfo.kt │ │ │ └── TermExtVersion.kt │ │ ├── database │ │ │ ├── CommandDao.kt │ │ │ ├── DataDbHelper.kt │ │ │ └── EnvironmentDao.kt │ │ ├── fakecontext │ │ │ ├── FakeContext.kt │ │ │ └── Workarounds.kt │ │ └── util │ │ │ ├── JniUtilsBase.kt │ │ │ └── ProcessUtils.kt │ │ ├── ui │ │ ├── activity │ │ │ ├── CrashReportActivity.kt │ │ │ ├── InstallTermExtActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PackActivity.kt │ │ │ └── envmanage │ │ │ │ ├── EnvAdapter.kt │ │ │ │ ├── EnvManageActivity.kt │ │ │ │ └── ItemAdapter.kt │ │ ├── dialog │ │ │ ├── BlurBehindDialogBuilder.kt │ │ │ └── ExecDialogBuilder.kt │ │ └── fragment │ │ │ ├── home │ │ │ ├── GrantShizukuPermViewHolder.kt │ │ │ ├── HomeAdapter.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── ServiceStatusViewHolder.kt │ │ │ ├── ShizukuStatusViewHolder.kt │ │ │ └── TermExtStatusViewHolder.kt │ │ │ ├── proc │ │ │ ├── ProcAdapter.kt │ │ │ └── ProcFragment.kt │ │ │ ├── runner │ │ │ ├── CommandAdapter.kt │ │ │ └── RunnerFragment.kt │ │ │ ├── settings │ │ │ └── SettingsFragment.kt │ │ │ └── terminal │ │ │ └── TerminalFragment.kt │ │ └── util │ │ ├── ThemeUtil.kt │ │ ├── ThrowableUtil.kt │ │ └── UpdateUtil.kt │ └── res │ ├── color-night │ └── home_card_background_color.xml │ ├── color │ └── home_card_background_color.xml │ ├── drawable │ ├── ic_add_24.xml │ ├── ic_backup_restore_24.xml │ ├── ic_check_circle_outline_24.xml │ ├── ic_close_24.xml │ ├── ic_configs_24.xml │ ├── ic_dark_mode_24.xml │ ├── ic_delete_outline_24.xml │ ├── ic_empty_icon_24.xml │ ├── ic_error_outline_24.xml │ ├── ic_file_24.xml │ ├── ic_format_color_fill_24.xml │ ├── ic_help_24.xml │ ├── ic_home.xml │ ├── ic_home_baseline_24.xml │ ├── ic_home_outline_24.xml │ ├── ic_import_export_24.xml │ ├── ic_info_24.xml │ ├── ic_invert_colors_24.xml │ ├── ic_language_24.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_round.xml │ ├── ic_layers.xml │ ├── ic_layers_baseline_24.xml │ ├── ic_layers_outline_24.xml │ ├── ic_merge_type_24.xml │ ├── ic_palette_outline_24.xml │ ├── ic_play_arrow.xml │ ├── ic_play_arrow_baseline_24.xml │ ├── ic_play_arrow_outline_24.xml │ ├── ic_restore_24.xml │ ├── ic_run_24.xml │ ├── ic_settings.xml │ ├── ic_settings_applications_outline_24.xml │ ├── ic_settings_baseline_24.xml │ ├── ic_settings_outline_24.xml │ ├── ic_stop_circle_outline_24.xml │ ├── ic_terminal_24.xml │ └── shape_circle_icon_background.xml │ ├── layout │ ├── activity_crash_report.xml │ ├── activity_env_manage.xml │ ├── activity_main.xml │ ├── activity_pack.xml │ ├── activity_stream_activity.xml │ ├── dialog_about.xml │ ├── dialog_add.xml │ ├── dialog_edit.xml │ ├── dialog_edit_env.xml │ ├── dialog_edit_env_e.xml │ ├── dialog_edit_env_e2.xml │ ├── dialog_exec.xml │ ├── dialog_import.xml │ ├── dialog_pick_backup.xml │ ├── fragment_home.xml │ ├── fragment_proc.xml │ ├── fragment_runner.xml │ ├── fragment_settings.xml │ ├── fragment_terminal.xml │ ├── home_item_container.xml │ ├── home_service_status.xml │ ├── home_shizuku_perm_request.xml │ ├── home_shizuku_status.xml │ ├── home_term_ext_status.xml │ ├── item_cmd.xml │ ├── item_env.xml │ ├── item_env_item.xml │ └── item_proc.xml │ ├── menu │ ├── bottom_nav_menu.xml │ └── menu_home.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── navigation │ └── mobile_navigation.xml │ ├── resources.properties │ ├── values-land │ └── dimens.xml │ ├── values-night │ └── styles.xml │ ├── values-sw600dp │ ├── dimens.xml │ └── values.xml │ ├── values-w1240dp │ └── dimens.xml │ ├── values-w600dp │ └── dimens.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── attrs.xml │ ├── dimens.xml │ ├── strings.xml │ ├── styles.xml │ ├── theme.xml │ ├── themes_custom.xml │ ├── themes_overlay.xml │ ├── themes_override.xml │ └── values.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ └── preference_setting.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: App 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '.github/ISSUE_TEMPLATE' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-24.04 13 | if: ${{ !startsWith(github.event.head_commit.message, '[skip ci]') && github.repository_owner == 'yangFenTuoZi' }} 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: 'recursive' 20 | fetch-depth: 0 21 | 22 | - name: Setup Java 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'temurin' 26 | java-version: '21' 27 | cache: 'gradle' 28 | 29 | - name: Write key 30 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' 31 | run: | 32 | touch signing.properties 33 | echo KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }} >> signing.properties 34 | echo KEYSTORE_ALIAS=${{ secrets.KEYSTORE_ALIAS }} >> signing.properties 35 | echo KEYSTORE_ALIAS_PASSWORD='${{ secrets.KEYSTORE_ALIAS_PASSWORD }}' >> signing.properties 36 | echo KEYSTORE_FILE=../key.jks >> signing.properties 37 | echo '${{ secrets.KEYSTORE }}' | base64 --decode > key.jks 38 | 39 | - name: Write temporary key 40 | if: hashFiles('key.jks') == '' && !steps.vars.outputs.HAS_SECRET 41 | run: | 42 | keytool -genkey -alias a -dname CN=_ -storepass passwd -keypass passwd -keystore key.jks 43 | echo KEYSTORE_PASSWORD=passwd >> signing.properties 44 | echo KEYSTORE_ALIAS=a >> signing.properties 45 | echo KEYSTORE_ALIAS_PASSWORD=passwd >> signing.properties 46 | echo KEYSTORE_FILE=../key.jks >> signing.properties 47 | 48 | - name: Cache Gradle Dependencies 49 | uses: actions/cache@v4 50 | with: 51 | path: | 52 | ~/.gradle/caches 53 | ~/.gradle/wrapper 54 | !~/.gradle/caches/build-cache-* 55 | key: gradle-deps-app-${{ hashFiles('**/build.gradle') }} 56 | restore-keys: | 57 | gradle-deps 58 | 59 | - name: Cache Gradle Build 60 | uses: actions/cache@v4 61 | with: 62 | path: | 63 | ~/.gradle/caches/build-cache-* 64 | key: gradle-builds-app-${{ github.sha }} 65 | restore-keys: | 66 | gradle-builds 67 | 68 | - name: Build with Gradle 69 | id: buildWithGradle 70 | run: | 71 | echo 'org.gradle.caching=true' >> gradle.properties 72 | echo 'org.gradle.parallel=true' >> gradle.properties 73 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 74 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 75 | chmod +x gradlew 76 | ./gradlew :app:assembleRelease :app:assembleDebug 77 | releaseName=`ls app/build/outputs/apk/release/runner*-v*-release.apk | awk -F '(/|.apk)' '{print $6}'` && echo "releaseName=$releaseName" >> $GITHUB_OUTPUT 78 | debugName=`ls app/build/outputs/apk/debug/runner*-v*-debug.apk | awk -F '(/|.apk)' '{print $6}'` && echo "debugName=$debugName" >> $GITHUB_OUTPUT 79 | 80 | - name: Upload release 81 | if: success() 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: ${{ steps.buildWithGradle.outputs.releaseName }} 85 | path: "app/build/outputs/apk/release/*.apk" 86 | 87 | - name: Upload debug 88 | if: success() 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: ${{ steps.buildWithGradle.outputs.debugName }} 92 | path: "app/build/outputs/apk/debug/*.apk" 93 | 94 | - name: Upload mappings 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: mappings 98 | path: "app/build/outputs/mapping/release" 99 | 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 项目排除路径 2 | # Gradle files 3 | .gradle/ 4 | build/ 5 | 6 | # Local configuration file (sdk path, etc) 7 | local.properties 8 | 9 | # Log/OS Files 10 | *.log 11 | 12 | # Android Studio generated files and folders 13 | captures/ 14 | .externalNativeBuild/ 15 | .cxx/ 16 | *.apk 17 | output.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 | /signing.properties 37 | /out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Runner 2 | 3 | 本APP受到[ShizukuRunner](https://github.com/WuDi-ZhanShen/ShizukuRunner)的启发而写,引用~~部分~~ 4 | 极小部分[ShizukuRunner](https://github.com/WuDi-ZhanShen/ShizukuRunner)的代码 5 | 目前仍在开发,大部分功能不全,请等后续 6 | 如果喜欢这个项目的话请多多为我Star吧 7 | 8 | ## 下载 9 | 10 | [Github](https://github.com/yangFenTuoZi/Runner/releases) 11 | 12 | ## 计划 13 | 14 | **开发大致按以下顺序进行 此表随时变动 仅供参考** 15 | 16 | **暂并不考虑支持其他语言 如有需要 请提 [PR](https://github.com/yangFenTuoZi/Runner/pulls)** 17 | 18 | ### 功能 19 | 20 | - [x] 命令执行实时输出 21 | - [x] 分离终端扩展包 22 | - [x] 终端bash, busybox 23 | - [x] 允许进程后台运行 24 | - [x] 允许执行前降低权限(可能仅 Root 可用) 25 | - [x] 支持强杀进程 26 | - [x] 支持杀所有子进程 27 | - [x] 增加进程管理 28 | - [x] 支持自定义环境变量 29 | - [x] 开机自启动执行命令 30 | - [x] App数据备份与还原 31 | - [ ] 将命令打包为独立 APK 32 | - [ ] 终端 33 | - [ ] 终端侧载模块(类似 dpkg ) 34 | - [ ] 外部CLI 35 | 36 | ### 语言 37 | 38 | - [x] 中文 39 | - [x] 英文 40 | 41 | ## 反馈注意事项 42 | 43 | 1. 反馈请说明设备信息(系统版本、UI版本、权限等) 44 | 2. 反馈请附日志 45 | 3. 请使用**中文标题**反馈 46 | 47 | ## 注意 48 | 49 | 2024/10/04: **不再使用Shizuku** 50 | 51 | 2025/04/21: **重新使用Shizuku** 52 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # 项目排除路径 2 | # Gradle files 3 | .gradle/ 4 | build/ 5 | 6 | # Local configuration file (sdk path, etc) 7 | local.properties 8 | 9 | # Log/OS Files 10 | *.log 11 | 12 | # Android Studio generated files and folders 13 | captures/ 14 | .externalNativeBuild/ 15 | .cxx/ 16 | *.apk 17 | output.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 | release 37 | debug 38 | -------------------------------------------------------------------------------- /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.kts. 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 | -dontobfuscate 23 | -dontwarn org.jetbrains.annotations.NotNull 24 | -dontwarn org.jetbrains.annotations.Nullable 25 | -keep class yangfentuozi.runner.service.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 36 | 37 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/IService.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service; 2 | 3 | import yangfentuozi.runner.service.data.CommandInfo; 4 | import yangfentuozi.runner.service.data.TermExtVersion; 5 | import yangfentuozi.runner.service.data.ProcessInfo; 6 | import yangfentuozi.runner.service.data.EnvInfo; 7 | 8 | import yangfentuozi.runner.service.callback.IExecResultCallback; 9 | import yangfentuozi.runner.service.callback.IInstallTermExtCallback; 10 | 11 | interface IService { 12 | void destroy() = 16777114; 13 | void exit() = 1; 14 | int version() = 2; 15 | 16 | void exec(String command, String ids, String procName, in IExecResultCallback callback) = 100; 17 | 18 | int size() = 200; 19 | CommandInfo read(int position) = 201; 20 | CommandInfo[] readAll() = 202; 21 | void delete(int position) = 203; 22 | void edit(in CommandInfo cmdInfo, int position) = 204; 23 | void insert(in CommandInfo cmdInfo) = 205; 24 | void move(int position, int afterPosition) = 206; 25 | void insertInto(in CommandInfo cmdInfo, int position) = 207; 26 | 27 | void deleteEnv(String key) = 300; 28 | boolean insertEnv(String key, String value) = 301; 29 | boolean updateEnv(in EnvInfo from, in EnvInfo to) = 302; 30 | String getEnv(String key) = 303; 31 | EnvInfo[] getAllEnv() = 304; 32 | 33 | ProcessInfo[] getProcesses() = 400; 34 | boolean[] sendSignal(in int[] pid, int signal) = 401; 35 | 36 | void backupData(String output, boolean data, boolean termHome, boolean termUsr) = 500; 37 | void restoreData(String input) = 501; 38 | 39 | void installTermExt(String termExtZip, in IInstallTermExtCallback callback) = 1000; 40 | void removeTermExt() = 1001; 41 | TermExtVersion getTermExtVersion() = 1002; 42 | } -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/callback/IExecResultCallback.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.callback; 2 | 3 | interface IExecResultCallback { 4 | void onOutput(String outputs); 5 | void onExit(int exitValue); 6 | } -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/callback/IInstallTermExtCallback.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.callback; 2 | 3 | interface IInstallTermExtCallback { 4 | void onMessage(String message); 5 | void onExit(boolean isSuccessful); 6 | } -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/data/CommandInfo.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data; 2 | parcelable CommandInfo; -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/data/EnvInfo.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data; 2 | parcelable EnvInfo; -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/data/ProcessInfo.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data; 2 | parcelable ProcessInfo; -------------------------------------------------------------------------------- /app/src/main/aidl/yangfentuozi/runner/service/data/TermExtVersion.aidl: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data; 2 | parcelable TermExtVersion; -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22.1) 2 | 3 | project("runner") 4 | 5 | add_executable(starter 6 | starter.c) 7 | 8 | add_library(processutils SHARED 9 | processutils.c) 10 | 11 | set_target_properties(starter PROPERTIES PREFIX "lib") 12 | set_target_properties(starter PROPERTIES SUFFIX ".so") 13 | 14 | set_target_properties(processutils PROPERTIES PREFIX "lib") 15 | set_target_properties(processutils PROPERTIES SUFFIX ".so") 16 | -------------------------------------------------------------------------------- /app/src/main/cpp/starter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | typedef struct { 14 | uid_t uid; 15 | gid_t gid; 16 | gid_t *groups; 17 | size_t groups_count; 18 | } UserInfo; 19 | 20 | // Constants for fixed command execution 21 | static const char *BASH_PATH = "/data/local/tmp/runner/usr/bin/bash"; 22 | 23 | void parse_uid_gid(const char *arg, UserInfo *info) { 24 | // Initialize structure 25 | memset(info, 0, sizeof(UserInfo)); 26 | 27 | if (!arg || !*arg) return; 28 | 29 | char *str = strdup(arg); 30 | if (!str) { 31 | perror("strdup failed"); 32 | return; 33 | } 34 | 35 | // First token - UID 36 | char *token = strtok(str, ","); 37 | if (token) info->uid = (uid_t) atoi(token); 38 | 39 | // Second token - GID 40 | token = strtok(NULL, ","); 41 | if (token) info->gid = (gid_t) atoi(token); 42 | 43 | // Count additional groups 44 | size_t count = 0; 45 | char *tmp = str; 46 | while ((tmp = strchr(tmp, ','))) { 47 | count++; 48 | tmp++; 49 | } 50 | count = count > 2 ? count - 2 : 0; 51 | 52 | if (count > 0) { 53 | info->groups = calloc(count, sizeof(gid_t)); 54 | if (!info->groups) { 55 | perror("malloc failed"); 56 | free(str); 57 | return; 58 | } 59 | 60 | // Parse groups 61 | for (size_t i = 0; i < count; i++) { 62 | token = strtok(NULL, ","); 63 | if (token) info->groups[i] = (gid_t) atoi(token); 64 | } 65 | info->groups_count = count; 66 | } 67 | 68 | free(str); 69 | } 70 | 71 | int set_user_groups(const UserInfo *info) { 72 | if (info->gid != 0 && setgid(info->gid) != 0) { 73 | perror("setgid failed"); 74 | return 0; 75 | } 76 | 77 | if (info->groups_count > 0 && setgroups(info->groups_count, info->groups) != 0) { 78 | perror("setgroups failed"); 79 | return 0; 80 | } 81 | 82 | if (info->uid != 0 && setuid(info->uid) != 0) { 83 | perror("setuid failed"); 84 | return 0; 85 | } 86 | 87 | return 1; 88 | } 89 | 90 | void free_user_info(UserInfo *info) { 91 | if (info) { 92 | free(info->groups); 93 | info->groups = NULL; 94 | info->groups_count = 0; 95 | } 96 | } 97 | 98 | int main(int argc, char *argv[]) { 99 | if (argc != 3) { 100 | fprintf(stderr, "Usage: %s \n", argv[0]); 101 | return EXIT_FAILURE; 102 | } 103 | UserInfo user_info = {0}; 104 | 105 | // Parse UID/GID argument 106 | if (isdigit(argv[1][0]) && strcmp(argv[1], "-1") != 0) { 107 | parse_uid_gid(argv[1], &user_info); 108 | } 109 | 110 | // Set user and group IDs if specified 111 | if ((user_info.uid != 0 || user_info.gid != 0 || user_info.groups_count > 0) && 112 | !set_user_groups(&user_info)) { 113 | free_user_info(&user_info); 114 | return EXIT_FAILURE; 115 | } 116 | 117 | free_user_info(&user_info); 118 | 119 | char *const args[] = {"bash", "--nice-name", argv[2], NULL}; 120 | return execvp(BASH_PATH, args); 121 | } 122 | 123 | -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/App.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.os.Environment 8 | import android.os.Looper 9 | import androidx.appcompat.app.AppCompatDelegate 10 | import androidx.preference.PreferenceManager 11 | import yangfentuozi.runner.ui.activity.CrashReportActivity 12 | import yangfentuozi.runner.util.ThemeUtil 13 | import java.io.File 14 | import java.util.LinkedList 15 | 16 | 17 | class App : Application(), Thread.UncaughtExceptionHandler { 18 | private val activities: MutableList = LinkedList() 19 | private lateinit var pref: SharedPreferences 20 | 21 | override fun onCreate() { 22 | super.onCreate() 23 | instance = this 24 | pref = PreferenceManager.getDefaultSharedPreferences(this) 25 | AppCompatDelegate.setDefaultNightMode(ThemeUtil.darkTheme) 26 | 27 | Runner.init() 28 | Thread.setDefaultUncaughtExceptionHandler(this) 29 | } 30 | 31 | fun addActivity(activity: Activity?) { 32 | activities.add(activity!!) 33 | } 34 | 35 | fun removeActivity(activity: Activity?) { 36 | activities.remove(activity) 37 | } 38 | 39 | fun finishApp() { 40 | for (activity in activities) activity.finish() 41 | activities.clear() 42 | } 43 | 44 | override fun onTerminate() { 45 | super.onTerminate() 46 | Runner.remove() 47 | } 48 | 49 | private fun crashHandler(t: Thread, e: Throwable) { 50 | val fileName = "runnerCrash-" + System.currentTimeMillis() + ".log" 51 | val file: File? 52 | if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { 53 | val dir = 54 | File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS) 55 | if (!dir.exists()) { 56 | dir.mkdirs() 57 | } 58 | file = File(dir, fileName) 59 | } else { 60 | file = getExternalFilesDir(fileName) 61 | } 62 | checkNotNull(file) 63 | 64 | startActivity( 65 | Intent(this, CrashReportActivity::class.java) 66 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 67 | .putExtra("crash_info", e.stackTraceToString()) 68 | .putExtra("crash_file", file.absolutePath) 69 | ) 70 | } 71 | 72 | override fun uncaughtException(t: Thread, e: Throwable) { 73 | Thread { 74 | Looper.prepare() 75 | crashHandler(t, e) 76 | Looper.loop() 77 | }.start() 78 | } 79 | 80 | companion object { 81 | lateinit var instance: App 82 | private set 83 | 84 | val preferences: SharedPreferences 85 | get() = instance.pref 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/appservice/ExecOnBootService.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.appservice 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.Handler 6 | import android.os.IBinder 7 | import android.os.Looper 8 | import android.util.Log 9 | import yangfentuozi.runner.App 10 | import yangfentuozi.runner.Runner 11 | import yangfentuozi.runner.service.callback.IExecResultCallback 12 | import java.util.concurrent.ExecutorService 13 | import java.util.concurrent.Executors 14 | 15 | class ExecOnBootService : Service() { 16 | 17 | companion object { 18 | const val TAG = "ExecOnBootService" 19 | var isRunning = false 20 | } 21 | 22 | private lateinit var executor: ExecutorService 23 | 24 | val mHandler = Handler(Looper.getMainLooper()) 25 | 26 | val timeout: Long = 20_000L 27 | 28 | override fun onCreate() { 29 | super.onCreate() 30 | Log.i(TAG, "service created") 31 | executor = Executors.newFixedThreadPool(1) 32 | } 33 | 34 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 35 | Log.i(TAG, "service starting command") 36 | isRunning = true 37 | 38 | executor.execute { 39 | Runner.refreshStatus() 40 | if (Runner.waitShizuku(timeout)) { 41 | Log.i(TAG, "Shizuku is ready") 42 | Runner.refreshStatus() 43 | Runner.tryBindService() 44 | if (Runner.waitService((timeout * 3).toLong())) { 45 | Log.i(TAG, "Service is ready") 46 | exec() 47 | } else { 48 | Log.i(TAG, "Service is not ready") 49 | mHandler.post { stopSelf() } 50 | } 51 | } else { 52 | Log.i(TAG, "Shizuku is not ready") 53 | mHandler.post { stopSelf() } 54 | } 55 | } 56 | 57 | return START_STICKY 58 | } 59 | 60 | fun exec() { 61 | val sharedPreferences = App.instance.getSharedPreferences("startup_script", MODE_PRIVATE) 62 | val command = sharedPreferences.getString("startup_script_command", "") 63 | val targetPerm = if (sharedPreferences.getBoolean("startup_script_reduce_perm", false)) sharedPreferences.getString("startup_script_target_perm", null) else null 64 | if (command.isNullOrEmpty()) { 65 | Log.i(TAG, "exec: command is empty") 66 | return 67 | } 68 | Log.i(TAG, "exec: $command") 69 | Runner.service?.exec( 70 | command, 71 | targetPerm ?: "", 72 | "ExecOnBootTask", 73 | object : IExecResultCallback.Stub() { 74 | override fun onOutput(outputs: String?) { 75 | Log.i(TAG, "onOutput: $outputs") 76 | } 77 | 78 | override fun onExit(exitValue: Int) { 79 | Log.i(TAG, "exit with $exitValue") 80 | stopSelf() 81 | } 82 | }) 83 | } 84 | 85 | override fun onDestroy() { 86 | super.onDestroy() 87 | isRunning = false 88 | executor.shutdownNow() 89 | super.onDestroy() 90 | } 91 | 92 | override fun onBind(intent: Intent?): IBinder? = null 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.base 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.os.Handler 7 | import android.os.Looper 8 | import rikka.material.app.MaterialActivity 9 | import yangfentuozi.runner.App 10 | import yangfentuozi.runner.R 11 | import yangfentuozi.runner.util.ThemeUtil 12 | 13 | 14 | open class BaseActivity : MaterialActivity() { 15 | 16 | private val mHandler: Handler = Handler(Looper.getMainLooper()) 17 | private val mUiThread: Thread = Thread.currentThread() 18 | var isDialogShowing: Boolean = false 19 | 20 | lateinit var mApp: App 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | mApp = application as App 24 | mApp.addActivity(this) 25 | setTheme(R.style.AppTheme); 26 | super.onCreate(savedInstanceState) 27 | } 28 | 29 | override fun onDestroy() { 30 | mApp.removeActivity(this) 31 | super.onDestroy() 32 | } 33 | 34 | override fun onApplyUserThemeResource(theme: Resources.Theme, isDecorView: Boolean) { 35 | if (!ThemeUtil.isSystemAccent) { 36 | theme.applyStyle(ThemeUtil.colorThemeStyleRes, true); 37 | } 38 | theme.applyStyle(ThemeUtil.getNightThemeStyleRes(this), true); 39 | theme.applyStyle(rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, true) 40 | } 41 | 42 | override fun computeUserThemeKey(): String? { 43 | return ThemeUtil.colorTheme + ThemeUtil.getNightTheme(this) 44 | } 45 | 46 | override fun onApplyTranslucentSystemBars() { 47 | super.onApplyTranslucentSystemBars() 48 | 49 | // 设置状态栏导航栏透明 50 | val window = window 51 | window.statusBarColor = Color.TRANSPARENT 52 | window.navigationBarColor = Color.TRANSPARENT 53 | } 54 | 55 | fun runOnMainThread(action: Runnable) { 56 | if (Thread.currentThread() !== mUiThread) { 57 | mHandler.post(action) 58 | } else { 59 | action.run() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/base/BaseDialogBuilder.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.base 2 | 3 | import android.content.DialogInterface 4 | import androidx.annotation.StringRes 5 | import androidx.appcompat.app.AlertDialog 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | 8 | open class BaseDialogBuilder @Throws(DialogShowingException::class) constructor(context: BaseActivity) : MaterialAlertDialogBuilder(context) { 9 | class DialogShowingException : Exception() 10 | 11 | private val mBaseActivity: BaseActivity = context 12 | private var mAlertDialog: AlertDialog? = null 13 | private var mOnDismissListener: DialogInterface.OnDismissListener? = null 14 | 15 | init { 16 | if (mBaseActivity.isDialogShowing) throw DialogShowingException() 17 | mBaseActivity.isDialogShowing = true 18 | super.setOnDismissListener { dialogInterface -> 19 | mBaseActivity.isDialogShowing = false 20 | mOnDismissListener?.onDismiss(dialogInterface) 21 | } 22 | } 23 | 24 | fun getAlertDialog(): AlertDialog? = mAlertDialog 25 | 26 | 27 | override fun create(): AlertDialog { 28 | return super.create().also { mAlertDialog = it } 29 | } 30 | 31 | 32 | override fun setOnDismissListener(onDismissListener: DialogInterface.OnDismissListener?): MaterialAlertDialogBuilder { 33 | mOnDismissListener = onDismissListener 34 | return this 35 | } 36 | 37 | fun runOnMainThread(action: Runnable) { 38 | mBaseActivity.runOnMainThread(action) 39 | } 40 | 41 | 42 | fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { 43 | return mBaseActivity.getString(resId, *formatArgs) 44 | } 45 | 46 | 47 | fun getString(@StringRes resId: Int): String { 48 | return mBaseActivity.getString(resId) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.base 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import yangfentuozi.runner.ui.activity.MainActivity 6 | 7 | open class BaseFragment : Fragment() { 8 | protected lateinit var mMainActivity: MainActivity 9 | val appBar get() = mMainActivity.appBar 10 | val toolbar get() = mMainActivity.toolbar 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | mMainActivity = (activity as? MainActivity) 15 | ?: throw RuntimeException("父Activity非MainActivity") 16 | } 17 | 18 | override fun onStart() { 19 | super.onStart() 20 | mMainActivity.toolbar.subtitle = null 21 | } 22 | 23 | fun runOnMainThread(action: Runnable) { 24 | mMainActivity.runOnMainThread(action) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/receiver/BootCompleteReceiver.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Process 7 | import android.util.Log 8 | import yangfentuozi.runner.App 9 | import yangfentuozi.runner.appservice.ExecOnBootService 10 | 11 | 12 | class BootCompleteReceiver : BroadcastReceiver() { 13 | companion object { 14 | const val TAG = "BootCompleteReceiver" 15 | } 16 | 17 | override fun onReceive(context: Context, intent: Intent) { 18 | if (Intent.ACTION_LOCKED_BOOT_COMPLETED != intent.action && Intent.ACTION_BOOT_COMPLETED != intent.action) { 19 | return 20 | } 21 | 22 | if (!App.preferences.getBoolean("auto_start_exec", false)) return 23 | if (Process.myUid() / 100000 > 0) return 24 | 25 | Log.i(TAG, "receive $intent") 26 | 27 | if (ExecOnBootService.isRunning) { 28 | Log.i(TAG, "ExecOnBootService is already running") 29 | return 30 | } 31 | Log.i(TAG, "start ExecOnBootService") 32 | context.startService(Intent(context, ExecOnBootService::class.java)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/callback/ExecResultCallback.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.callback 2 | 3 | import android.os.RemoteException 4 | 5 | class ExecResultCallback(private val mCallback: IExecResultCallback?) { 6 | fun onOutput(outputs: String?) { 7 | try { 8 | mCallback?.onOutput(outputs) 9 | } catch (_: RemoteException) { 10 | } 11 | } 12 | 13 | fun onExit(exitValue: Int) { 14 | try { 15 | mCallback?.onExit(exitValue) 16 | } catch (_: RemoteException) { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/callback/InstallTermExtCallback.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.callback 2 | 3 | import android.os.RemoteException 4 | 5 | class InstallTermExtCallback(private val mCallback: IInstallTermExtCallback?) { 6 | fun onMessage(message: String?) { 7 | try { 8 | mCallback?.onMessage(message) 9 | } catch (_: RemoteException) { 10 | } 11 | } 12 | 13 | fun onExit(isSuccessful: Boolean) { 14 | try { 15 | mCallback?.onExit(isSuccessful) 16 | } catch (_: RemoteException) { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/data/CommandInfo.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | open class CommandInfo : Parcelable { 7 | var name: String? = null 8 | var command: String? = null 9 | var keepAlive: Boolean = false 10 | var reducePerm: Boolean = false 11 | var targetPerm: String? = null 12 | 13 | constructor() 14 | 15 | constructor(source: Parcel) : super() { 16 | name = source.readString() 17 | command = source.readString() 18 | keepAlive = source.readInt() == 1 19 | reducePerm = source.readInt() == 1 20 | targetPerm = source.readString() 21 | } 22 | 23 | override fun describeContents(): Int { 24 | return 0 25 | } 26 | 27 | override fun writeToParcel(dest: Parcel, flags: Int) { 28 | dest.writeString(name) 29 | dest.writeString(command) 30 | dest.writeInt(if (keepAlive) 1 else 0) 31 | dest.writeInt(if (reducePerm) 1 else 0) 32 | dest.writeString(targetPerm) 33 | } 34 | 35 | companion object { 36 | @JvmField 37 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 38 | override fun createFromParcel(source: Parcel): CommandInfo { 39 | return CommandInfo(source) 40 | } 41 | 42 | override fun newArray(size: Int): Array { 43 | return arrayOfNulls(size) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/data/EnvInfo.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | open class EnvInfo : Parcelable { 7 | var key: String? = null 8 | var value: String? = null 9 | 10 | constructor() 11 | 12 | constructor(source: Parcel) : super() { 13 | key = source.readString() 14 | value = source.readString() 15 | } 16 | 17 | override fun describeContents(): Int { 18 | return 0 19 | } 20 | 21 | override fun writeToParcel(dest: Parcel, flags: Int) { 22 | dest.writeString(key) 23 | dest.writeString(value) 24 | } 25 | 26 | companion object { 27 | @JvmField 28 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 29 | override fun createFromParcel(source: Parcel): EnvInfo { 30 | return EnvInfo(source) 31 | } 32 | 33 | override fun newArray(size: Int): Array { 34 | return arrayOfNulls(size) 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/data/ProcessInfo.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import java.util.Objects 6 | 7 | open class ProcessInfo : Parcelable { 8 | var pid: Int = 0 9 | var ppid: Int = 0 10 | var exe: String? = null 11 | var args: Array? = null 12 | 13 | constructor() 14 | 15 | constructor(source: Parcel) : super() { 16 | pid = source.readInt() 17 | ppid = source.readInt() 18 | exe = source.readString() 19 | val argsLength = source.readInt() 20 | args = arrayOfNulls(argsLength) 21 | source.readStringArray(args!!) 22 | } 23 | 24 | override fun describeContents(): Int { 25 | return 0 26 | } 27 | 28 | override fun writeToParcel(dest: Parcel, flags: Int) { 29 | dest.writeInt(pid) 30 | dest.writeInt(ppid) 31 | dest.writeString(exe) 32 | dest.writeInt(args?.size ?: 0) 33 | dest.writeStringArray(args) 34 | } 35 | 36 | override fun toString(): String { 37 | return "ProcessInfo{" + 38 | "pid=" + pid + 39 | ", ppid=" + ppid + 40 | ", name='" + exe + '\'' + 41 | ", args=" + args.contentToString() + 42 | '}' 43 | } 44 | 45 | override fun equals(o: Any?): Boolean { 46 | if (o !is ProcessInfo) return false 47 | return pid == o.pid 48 | } 49 | 50 | override fun hashCode(): Int { 51 | return Objects.hashCode(pid) 52 | } 53 | 54 | companion object { 55 | @JvmField 56 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 57 | override fun createFromParcel(source: Parcel): ProcessInfo { 58 | return ProcessInfo(source) 59 | } 60 | 61 | override fun newArray(size: Int): Array { 62 | return arrayOfNulls(size) 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/data/TermExtVersion.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.data 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import java.io.InputStream 6 | import java.util.Properties 7 | 8 | open class TermExtVersion : Parcelable { 9 | val versionName: String? 10 | val versionCode: Int 11 | val abi: String? 12 | 13 | constructor(versionName: String?, versionCode: Int, abi: String?) { 14 | this.versionName = versionName 15 | this.versionCode = versionCode 16 | this.abi = abi 17 | } 18 | 19 | constructor(`in`: InputStream?) { 20 | var versionCode1: Int 21 | val buildProp = Properties() 22 | buildProp.load(`in`) 23 | versionName = buildProp.getProperty("version.name") 24 | try { 25 | versionCode1 = buildProp.getProperty("version.code").toInt() 26 | } catch (e: NumberFormatException) { 27 | versionCode1 = -1 28 | } 29 | versionCode = versionCode1 30 | abi = buildProp.getProperty("build.abi") 31 | } 32 | 33 | override fun describeContents(): Int { 34 | return 0 35 | } 36 | 37 | override fun writeToParcel(dest: Parcel, flags: Int) { 38 | dest.writeString(this.versionName) 39 | dest.writeInt(this.versionCode) 40 | dest.writeString(this.abi) 41 | } 42 | 43 | protected constructor(`in`: Parcel) { 44 | this.versionName = `in`.readString() 45 | this.versionCode = `in`.readInt() 46 | this.abi = `in`.readString() 47 | } 48 | 49 | companion object { 50 | @JvmField 51 | val CREATOR: Parcelable.Creator = 52 | object : Parcelable.Creator { 53 | override fun createFromParcel(source: Parcel): TermExtVersion { 54 | return TermExtVersion(source) 55 | } 56 | 57 | override fun newArray(size: Int): Array { 58 | return arrayOfNulls(size) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/database/DataDbHelper.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.database 2 | 3 | import android.content.Context 4 | import android.database.sqlite.SQLiteDatabase 5 | import android.database.sqlite.SQLiteOpenHelper 6 | import yangfentuozi.runner.service.ServiceImpl 7 | 8 | class DataDbHelper(context: Context?) : 9 | SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { 10 | override fun onCreate(db: SQLiteDatabase) { 11 | db.execSQL(TABLE_COMMANDS_CREATE) 12 | db.execSQL(TABLE_ENVIRONMENT_CREATE) 13 | } 14 | 15 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 16 | db.execSQL("DROP TABLE IF EXISTS $TABLE_COMMANDS") 17 | db.execSQL("DROP TABLE IF EXISTS $TABLE_ENVIRONMENT") 18 | onCreate(db) 19 | } 20 | 21 | val database: SQLiteDatabase = writableDatabase 22 | 23 | companion object { 24 | const val DATABASE_NAME: String = ServiceImpl.DATA_PATH + "/data.db" 25 | const val DATABASE_VERSION: Int = 1 26 | 27 | // commands 表 28 | const val TABLE_COMMANDS: String = "commands" 29 | const val COLUMN_ID: String = "_id" 30 | const val COLUMN_POSITION: String = "position" 31 | const val COLUMN_NAME: String = "name" 32 | const val COLUMN_COMMAND: String = "command" 33 | const val COLUMN_KEEP_ALIVE: String = "keep_alive" 34 | const val COLUMN_REDUCE_PERM: String = "reduce_perm" 35 | const val COLUMN_TARGET_PERM: String = "target_perm" 36 | 37 | private const val TABLE_COMMANDS_CREATE = "CREATE TABLE " + TABLE_COMMANDS + " (" + 38 | COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + 39 | COLUMN_POSITION + " INTEGER NOT NULL, " + 40 | COLUMN_NAME + " TEXT NOT NULL, " + 41 | COLUMN_COMMAND + " TEXT NOT NULL, " + 42 | COLUMN_KEEP_ALIVE + " INTEGER NOT NULL DEFAULT 0, " + 43 | COLUMN_REDUCE_PERM + " INTEGER NOT NULL DEFAULT 0, " + 44 | COLUMN_TARGET_PERM + " TEXT);" 45 | 46 | // environment 表 47 | const val TABLE_ENVIRONMENT: String = "environment" 48 | const val COLUMN_KEY: String = "env_key" 49 | const val COLUMN_VALUE: String = "env_value" 50 | 51 | private const val TABLE_ENVIRONMENT_CREATE = "CREATE TABLE " + TABLE_ENVIRONMENT + " (" + 52 | COLUMN_KEY + " TEXT NOT NULL, " + 53 | COLUMN_VALUE + " TEXT NOT NULL, " + 54 | "PRIMARY KEY (" + COLUMN_KEY + "));" 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/database/EnvironmentDao.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.database 2 | 3 | import android.content.ContentValues 4 | import android.database.sqlite.SQLiteDatabase 5 | import yangfentuozi.runner.service.data.EnvInfo 6 | 7 | class EnvironmentDao(private val db: SQLiteDatabase) { 8 | // 插入键值对 9 | fun insert(key: String?, value: String?): Boolean { 10 | val values = ContentValues() 11 | values.put(DataDbHelper.COLUMN_KEY, key) 12 | values.put(DataDbHelper.COLUMN_VALUE, value) 13 | val result = db.insert(DataDbHelper.TABLE_ENVIRONMENT, null, values) 14 | return result != -1L // 返回是否插入成功 15 | } 16 | 17 | // 更新键值对 18 | fun update(key: String?, value: String?): Boolean { 19 | val values = ContentValues() 20 | values.put(DataDbHelper.COLUMN_VALUE, value) 21 | val rowsAffected = db.update( 22 | DataDbHelper.TABLE_ENVIRONMENT, 23 | values, 24 | DataDbHelper.COLUMN_KEY + " = ?", 25 | arrayOf(key) 26 | ) 27 | return rowsAffected > 0 // 返回是否更新成功 28 | } 29 | 30 | fun update(fromKey: String?, fromValue: String?, toKey: String?, toValue: String?): Boolean { 31 | val values = ContentValues() 32 | values.put(DataDbHelper.COLUMN_KEY, toKey) 33 | values.put(DataDbHelper.COLUMN_VALUE, toValue) 34 | 35 | val rowsAffected = db.update( 36 | DataDbHelper.TABLE_ENVIRONMENT, 37 | values, 38 | DataDbHelper.COLUMN_KEY + " = ? AND " + DataDbHelper.COLUMN_VALUE + " = ?", 39 | arrayOf(fromKey, fromValue) 40 | ) 41 | return rowsAffected > 0 // 返回是否更新成功 42 | } 43 | 44 | // 根据键获取值 45 | fun getValue(key: String?): String? { 46 | val cursor = db.query( 47 | DataDbHelper.TABLE_ENVIRONMENT, 48 | arrayOf(DataDbHelper.COLUMN_VALUE), 49 | DataDbHelper.COLUMN_KEY + " = ?", 50 | arrayOf(key), 51 | null, 52 | null, 53 | null 54 | ) 55 | 56 | var value: String? = null 57 | if (cursor.moveToFirst()) { 58 | value = cursor.getString(cursor.getColumnIndexOrThrow(DataDbHelper.COLUMN_VALUE)) 59 | } 60 | cursor.close() 61 | return value 62 | } 63 | 64 | // 删除键值对 65 | fun delete(key: String?) { 66 | db.delete( 67 | DataDbHelper.TABLE_ENVIRONMENT, 68 | DataDbHelper.COLUMN_KEY + " = ?", 69 | arrayOf(key) 70 | ) 71 | } 72 | 73 | val all: ArrayList? 74 | // 获取所有键值对 75 | get() { 76 | val cursor = db.query( 77 | DataDbHelper.TABLE_ENVIRONMENT, 78 | arrayOf(DataDbHelper.COLUMN_KEY, DataDbHelper.COLUMN_VALUE), 79 | null, 80 | null, 81 | null, 82 | null, 83 | null 84 | ) 85 | 86 | val arrayList = ArrayList() 87 | while (cursor.moveToNext()) { 88 | val key = cursor.getString(cursor.getColumnIndexOrThrow(DataDbHelper.COLUMN_KEY)) 89 | val value = 90 | cursor.getString(cursor.getColumnIndexOrThrow(DataDbHelper.COLUMN_VALUE)) 91 | val envInfo = EnvInfo() 92 | envInfo.key = key 93 | envInfo.value = value 94 | arrayList.add(envInfo) 95 | } 96 | cursor.close() 97 | return arrayList 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/fakecontext/FakeContext.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.fakecontext 2 | 3 | import android.content.AttributionSource 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.os.Build 7 | import android.system.Os 8 | import androidx.annotation.RequiresApi 9 | 10 | class FakeContext private constructor() : ContextWrapper(Workarounds.systemContext) { 11 | override fun getPackageName(): String { 12 | return PACKAGE_NAME 13 | } 14 | 15 | override fun getOpPackageName(): String { 16 | return PACKAGE_NAME 17 | } 18 | 19 | @RequiresApi(Build.VERSION_CODES.S) 20 | override fun getAttributionSource(): AttributionSource { 21 | val builder = AttributionSource.Builder(Os.getuid()) 22 | builder.setPackageName(PACKAGE_NAME) 23 | return builder.build() 24 | } 25 | 26 | // @Override to be added on SDK upgrade for Android 14 27 | @Suppress("unused") 28 | override fun getDeviceId(): Int { 29 | return 0 30 | } 31 | 32 | override fun getApplicationContext(): Context { 33 | return this 34 | } 35 | 36 | companion object { 37 | var PACKAGE_NAME: String = if (Os.getuid() == 0) "root" else "com.android.shell" 38 | private val INSTANCE = FakeContext() 39 | 40 | fun get(): FakeContext { 41 | return INSTANCE 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/fakecontext/Workarounds.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.fakecontext 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import yangfentuozi.runner.service.ServiceImpl 7 | import java.lang.reflect.Constructor 8 | 9 | @SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") 10 | object Workarounds { 11 | private var ACTIVITY_THREAD_CLASS: Class<*>? = null 12 | private var ACTIVITY_THREAD: Any? = null 13 | 14 | init { 15 | try { 16 | ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread") 17 | val activityThreadConstructor: Constructor<*>? = 18 | ACTIVITY_THREAD_CLASS?.getDeclaredConstructor() 19 | activityThreadConstructor?.isAccessible = true 20 | ACTIVITY_THREAD = activityThreadConstructor?.newInstance() 21 | 22 | val sCurrentActivityThreadField = 23 | ACTIVITY_THREAD_CLASS?.getDeclaredField("sCurrentActivityThread") 24 | sCurrentActivityThreadField?.isAccessible = true 25 | sCurrentActivityThreadField?.set(null, ACTIVITY_THREAD) 26 | } catch (e: Exception) { 27 | throw AssertionError(e) 28 | } 29 | } 30 | 31 | val systemContext: Context? 32 | get() { 33 | try { 34 | val getSystemContextMethod = 35 | ACTIVITY_THREAD_CLASS!!.getDeclaredMethod("getSystemContext") 36 | return getSystemContextMethod.invoke(ACTIVITY_THREAD) as Context? 37 | } catch (throwable: Throwable) { 38 | Log.e( 39 | ServiceImpl.TAG, 40 | "Workarounds: Failed to get system context: ${throwable.stackTraceToString()}", 41 | throwable 42 | ) 43 | return null 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/util/JniUtilsBase.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.util.Log 5 | 6 | @SuppressLint("UnsafeDynamicallyLoadedCode") 7 | abstract class JniUtilsBase { 8 | var isLibraryLoaded: Boolean = false 9 | private set 10 | 11 | @Synchronized 12 | fun loadLibrary() { 13 | if (!this.isLibraryLoaded) { 14 | try { 15 | System.load(this.jniPath) 16 | this.isLibraryLoaded = true 17 | } catch (e: UnsatisfiedLinkError) { 18 | Log.e("ProcessUtils", "Failed to load native library: ", e) 19 | this.isLibraryLoaded = false 20 | } 21 | } 22 | } 23 | 24 | abstract val jniPath: String 25 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/service/util/ProcessUtils.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.service.util 2 | 3 | import yangfentuozi.runner.service.ServiceImpl 4 | import yangfentuozi.runner.service.data.ProcessInfo 5 | 6 | class ProcessUtils : JniUtilsBase() { 7 | external fun sendSignal(pid: Int, signal: Int): Boolean 8 | 9 | external fun getProcesses(): Array? 10 | 11 | override val jniPath: String 12 | get() = ServiceImpl.JNI_PROCESS_UTILS 13 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/CrashReportActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity 2 | 3 | import android.graphics.Typeface 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.os.SystemProperties 7 | import android.text.SpannableString 8 | import android.text.Spanned 9 | import android.text.style.StyleSpan 10 | import android.view.MenuItem 11 | import yangfentuozi.runner.base.BaseActivity 12 | import yangfentuozi.runner.databinding.ActivityCrashReportBinding 13 | import yangfentuozi.runner.util.ThrowableUtil.toErrorDialog 14 | import java.io.FileOutputStream 15 | import java.io.IOException 16 | 17 | class CrashReportActivity : BaseActivity() { 18 | 19 | private lateinit var binding: ActivityCrashReportBinding 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | binding = ActivityCrashReportBinding.inflate(layoutInflater) 24 | setContentView(binding.root) 25 | 26 | binding.appBar.setLiftable(true) 27 | setSupportActionBar(binding.toolbar) 28 | supportActionBar?.apply { 29 | setDisplayHomeAsUpEnabled(true) 30 | } 31 | 32 | val crashFile = intent.getStringExtra("crash_file") 33 | val crashInfo = intent.getStringExtra("crash_info") 34 | 35 | binding.crashFile.text = crashFile 36 | 37 | val crashInfoTextView = binding.crashInfo.apply { 38 | append(getBoldText("VERSION.RELEASE: ")) 39 | append(Build.VERSION.RELEASE) 40 | append("\n") 41 | 42 | append(getBoldText("VERSION.SDK_INT: ")) 43 | append(Build.VERSION.SDK_INT.toString()) 44 | append("\n") 45 | 46 | append(getBoldText("BUILD_TYPE: ")) 47 | append(Build.TYPE) 48 | append("\n") 49 | 50 | append(getBoldText("CPU_ABI: ")) 51 | append(SystemProperties.get("ro.product.cpu.abi")) 52 | append("\n") 53 | 54 | append(getBoldText("CPU_SUPPORTED_ABIS: ")) 55 | append(Build.SUPPORTED_ABIS.contentToString()) 56 | append("\n\n$crashInfo") 57 | } 58 | 59 | try { 60 | FileOutputStream(crashFile).use { out -> 61 | out.write(crashInfoTextView.text.toString().toByteArray()) 62 | } 63 | } catch (e: IOException) { 64 | e.toErrorDialog(this) 65 | } 66 | } 67 | 68 | private fun getBoldText(text: String): CharSequence { 69 | return SpannableString(text).apply { 70 | setSpan(StyleSpan(Typeface.BOLD), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) 71 | } 72 | } 73 | 74 | override fun onDestroy() { 75 | super.onDestroy() 76 | mApp.finishApp() 77 | } 78 | 79 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 80 | return if (item.itemId == android.R.id.home) { 81 | finish() 82 | true 83 | } else { 84 | super.onOptionsItemSelected(item) 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/InstallTermExtActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity 2 | 3 | import android.content.Intent 4 | import android.graphics.Typeface 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.view.MenuItem 8 | import android.widget.ScrollView 9 | import android.widget.Toast 10 | import androidx.annotation.StringRes 11 | import yangfentuozi.runner.R 12 | import yangfentuozi.runner.Runner 13 | import yangfentuozi.runner.base.BaseActivity 14 | import yangfentuozi.runner.databinding.ActivityStreamActivityBinding 15 | import yangfentuozi.runner.service.ServiceImpl 16 | import yangfentuozi.runner.service.callback.IInstallTermExtCallback 17 | import java.io.File 18 | import java.io.FileNotFoundException 19 | import java.io.FileOutputStream 20 | import java.io.IOException 21 | import java.io.InputStream 22 | 23 | 24 | class InstallTermExtActivity : BaseActivity() { 25 | private lateinit var binding: ActivityStreamActivityBinding 26 | private var callback: IInstallTermExtCallback? = null 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | binding = ActivityStreamActivityBinding.inflate(layoutInflater) 31 | setContentView(binding.root) 32 | binding.appBar.setLiftable(true) 33 | setSupportActionBar(binding.toolbar) 34 | supportActionBar?.apply { 35 | setDisplayHomeAsUpEnabled(true) 36 | } 37 | binding.text1.typeface = Typeface.MONOSPACE 38 | 39 | val action = intent.action 40 | val type = intent.type 41 | 42 | if (Intent.ACTION_VIEW == action && type != null) { 43 | val uri = intent.data 44 | if (uri != null) 45 | handleReceivedFile(uri) 46 | else { 47 | onMessage("! Invalid file") 48 | return 49 | } 50 | } else if (Intent.ACTION_SEND == action && type != null) { 51 | val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) 52 | if (uri != null) 53 | handleReceivedFile(uri) 54 | else { 55 | onMessage("! Invalid file") 56 | return 57 | } 58 | } else { 59 | onMessage("! Invalid intent") 60 | return 61 | } 62 | } 63 | 64 | private fun handleReceivedFile(uri: Uri) { 65 | var input: InputStream? = null 66 | try { 67 | input = contentResolver.openInputStream(uri) 68 | } catch (e: FileNotFoundException) { 69 | onMessage("! File not found:\n" + e.stackTraceToString()) 70 | return 71 | } 72 | if (input == null) { 73 | onMessage("! Failed to open file") 74 | return 75 | } 76 | val termExtCacheDir = File(externalCacheDir, "termExtCache") 77 | ServiceImpl.rmRF(termExtCacheDir) 78 | termExtCacheDir.mkdirs() 79 | val file = File(termExtCacheDir, "termux_ext.zip") 80 | try { 81 | if (!file.exists()) { 82 | file.createNewFile() 83 | } 84 | val output = FileOutputStream(file) 85 | input.copyTo(output, bufferSize = ServiceImpl.PAGE_SIZE) 86 | input.close() 87 | output.close() 88 | } catch (_: IOException) { 89 | onMessage("! Failed to copy file") 90 | return 91 | } 92 | 93 | if (!Runner.pingServer()) { 94 | showErrAndFinish(R.string.service_not_running) 95 | finish() 96 | } 97 | 98 | callback = object : IInstallTermExtCallback.Stub() { 99 | override fun onMessage(message: String?) { 100 | this@InstallTermExtActivity.onMessage(message) 101 | } 102 | 103 | override fun onExit(isSuccessful: Boolean) { 104 | onMessage(if (isSuccessful) "- Installation successful" else "! Installation failed") 105 | onMessage("\n- Cleanup temp: ${termExtCacheDir.absolutePath}") 106 | ServiceImpl.rmRF(termExtCacheDir) 107 | callback = null 108 | } 109 | } 110 | 111 | Runner.service?.installTermExt(file.absolutePath, callback) 112 | } 113 | 114 | private fun onMessage(message: String?) { 115 | runOnMainThread { 116 | binding.text1.append(message + "\n") 117 | binding.scrollView.post { 118 | binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN) 119 | } 120 | } 121 | } 122 | 123 | private fun showErrAndFinish(@StringRes resId: Int) { 124 | runOnMainThread { 125 | Toast.makeText(this, resId, Toast.LENGTH_SHORT).show() 126 | finish() 127 | } 128 | } 129 | 130 | override fun onDestroy() { 131 | super.onDestroy() 132 | callback = null 133 | } 134 | 135 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 136 | return if (item.itemId == android.R.id.home) { 137 | finish() 138 | true 139 | } else { 140 | super.onOptionsItemSelected(item) 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity 2 | 3 | import android.os.Bundle 4 | import android.text.method.LinkMovementMethod 5 | import androidx.core.text.HtmlCompat 6 | import androidx.navigation.fragment.NavHostFragment 7 | import androidx.navigation.ui.AppBarConfiguration 8 | import androidx.navigation.ui.NavigationUI 9 | import com.google.android.material.appbar.AppBarLayout 10 | import com.google.android.material.appbar.MaterialToolbar 11 | import yangfentuozi.runner.BuildConfig 12 | import yangfentuozi.runner.R 13 | import yangfentuozi.runner.Runner 14 | import yangfentuozi.runner.base.BaseActivity 15 | import yangfentuozi.runner.base.BaseDialogBuilder 16 | import yangfentuozi.runner.databinding.ActivityMainBinding 17 | import yangfentuozi.runner.databinding.DialogAboutBinding 18 | import yangfentuozi.runner.ui.dialog.BlurBehindDialogBuilder 19 | import yangfentuozi.runner.util.ThrowableUtil.toErrorDialog 20 | import java.util.Locale 21 | 22 | class MainActivity : BaseActivity() { 23 | 24 | lateinit var appBar: AppBarLayout 25 | lateinit var toolbar: MaterialToolbar 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | val binding = ActivityMainBinding.inflate(layoutInflater) 31 | setContentView(binding.root) 32 | 33 | val appBarConfiguration = AppBarConfiguration.Builder( 34 | R.id.navigation_home, 35 | R.id.navigation_runner, 36 | R.id.navigation_terminal, 37 | R.id.navigation_proc, 38 | R.id.navigation_settings 39 | ).build() 40 | 41 | val fragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) 42 | val navController = (fragment as NavHostFragment).navController 43 | NavigationUI.setupWithNavController(binding.toolbar, navController, appBarConfiguration) 44 | NavigationUI.setupWithNavController(binding.navView, navController) 45 | binding.navView.menu.findItem(R.id.navigation_terminal).isEnabled = false 46 | binding.navView.menu.findItem(R.id.navigation_terminal).isVisible = false 47 | 48 | binding.appBar.setLiftable(true) 49 | binding.toolbar.inflateMenu(R.menu.menu_home) 50 | 51 | binding.toolbar.setOnMenuItemClickListener { item -> 52 | when (item.itemId) { 53 | R.id.menu_stop_server -> { 54 | if (!Runner.pingServer()) return@setOnMenuItemClickListener true 55 | try { 56 | BaseDialogBuilder(this) 57 | .setTitle(R.string.warning) 58 | .setMessage(R.string.confirm_stop_server) 59 | .setPositiveButton(android.R.string.ok) { _, _ -> 60 | Thread { 61 | try { 62 | Runner.tryUnbindService(true) 63 | } catch (e: Exception) { 64 | e.toErrorDialog(this) 65 | } 66 | }.start() 67 | } 68 | .setNegativeButton(android.R.string.cancel, null) 69 | .show() 70 | } catch (_: BaseDialogBuilder.DialogShowingException) { 71 | } 72 | true 73 | } 74 | 75 | R.id.menu_about -> { 76 | showAbout() 77 | true 78 | } 79 | 80 | else -> true 81 | } 82 | } 83 | 84 | toolbar = binding.toolbar 85 | appBar = binding.appBar 86 | } 87 | 88 | private fun showAbout() { 89 | val binding = DialogAboutBinding.inflate(layoutInflater, null, false) 90 | binding.designAboutTitle.setText(R.string.app_name) 91 | binding.designAboutInfo.movementMethod = LinkMovementMethod.getInstance() 92 | binding.designAboutInfo.text = HtmlCompat.fromHtml( 93 | getString( 94 | R.string.about_view_source_code, 95 | "GitHub" 96 | ), HtmlCompat.FROM_HTML_MODE_LEGACY 97 | ) 98 | binding.designAboutVersion.text = String.format( 99 | Locale.getDefault(), 100 | "%s (%d)", 101 | BuildConfig.VERSION_NAME, 102 | BuildConfig.VERSION_CODE 103 | ) 104 | 105 | try { 106 | BlurBehindDialogBuilder(this) 107 | .setView(binding.root) 108 | .show() 109 | } catch (_: BaseDialogBuilder.DialogShowingException) { 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/PackActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.MenuItem 6 | import android.widget.Toast 7 | import androidx.appcompat.app.ActionBar 8 | import yangfentuozi.runner.R 9 | import yangfentuozi.runner.base.BaseActivity 10 | import yangfentuozi.runner.databinding.ActivityPackBinding 11 | 12 | class PackActivity : BaseActivity() { 13 | private var binding: ActivityPackBinding? = null 14 | 15 | @SuppressLint("SetTextI18n") 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | binding = ActivityPackBinding.inflate(layoutInflater) 19 | setContentView(binding!!.getRoot()) 20 | 21 | binding!!.appBar.setLiftable(true) 22 | setSupportActionBar(binding!!.toolbar) 23 | val actionBar: ActionBar? 24 | if ((supportActionBar.also { 25 | actionBar = it 26 | }) != null) actionBar!!.setDisplayHomeAsUpEnabled(true) 27 | 28 | val id = intent.getIntExtra("id", -1) 29 | if (id == -1) { 30 | Toast.makeText(this, R.string.finish, Toast.LENGTH_LONG).show() 31 | finish() 32 | } 33 | 34 | binding!!.packName.setText(intent.getStringExtra("name")) 35 | binding!!.packPackageName.setText("runner.app." + intent.getStringExtra("name")) 36 | binding!!.packVersionName.setText("1.0") 37 | binding!!.packVersionCode.setText("1") 38 | } 39 | 40 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 41 | return if (item.itemId == android.R.id.home) { 42 | finish() 43 | true 44 | } else { 45 | super.onOptionsItemSelected(item) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/envmanage/EnvManageActivity.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity.envmanage 2 | 3 | import android.os.Bundle 4 | import android.util.TypedValue 5 | import android.view.LayoutInflater 6 | import android.view.MenuItem 7 | import android.view.View 8 | import android.widget.Toast 9 | import androidx.appcompat.app.ActionBar 10 | import rikka.recyclerview.addEdgeSpacing 11 | import rikka.recyclerview.addItemSpacing 12 | import rikka.recyclerview.fixEdgeEffect 13 | import yangfentuozi.runner.R 14 | import yangfentuozi.runner.Runner 15 | import yangfentuozi.runner.base.BaseActivity 16 | import yangfentuozi.runner.base.BaseDialogBuilder 17 | import yangfentuozi.runner.databinding.ActivityEnvManageBinding 18 | import yangfentuozi.runner.databinding.DialogEditEnvBinding 19 | import yangfentuozi.runner.service.data.EnvInfo 20 | 21 | class EnvManageActivity : BaseActivity() { 22 | private lateinit var mBinding: ActivityEnvManageBinding 23 | private val mAdapter: EnvAdapter = EnvAdapter(this) 24 | val binding get() = mBinding 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | mBinding = ActivityEnvManageBinding.inflate(layoutInflater) 29 | setContentView(mBinding.getRoot()) 30 | 31 | mBinding.recyclerView.apply { 32 | adapter = mAdapter 33 | fixEdgeEffect(true, true) 34 | addItemSpacing(top = 4f, bottom = 4f, unit = TypedValue.COMPLEX_UNIT_DIP) 35 | addEdgeSpacing( 36 | top = 4f, 37 | bottom = 4f, 38 | left = 16f, 39 | right = 16f, 40 | unit = TypedValue.COMPLEX_UNIT_DIP 41 | ) 42 | } 43 | mBinding.appBar.setLiftable(true) 44 | setSupportActionBar(mBinding.toolbar) 45 | val actionBar: ActionBar? 46 | if ((supportActionBar.also { 47 | actionBar = it 48 | }) != null) actionBar!!.setDisplayHomeAsUpEnabled(true) 49 | 50 | mBinding.add.setOnClickListener { 51 | showAddDialog() 52 | } 53 | 54 | mBinding.toolbar.setOnClickListener { v: View? -> 55 | mBinding.recyclerView.smoothScrollToPosition( 56 | 0 57 | ) 58 | } 59 | } 60 | 61 | fun showAddDialog() { 62 | val dialogBinding = DialogEditEnvBinding.inflate(LayoutInflater.from(this)) 63 | 64 | try { 65 | BaseDialogBuilder(this) 66 | .setTitle(R.string.edit) 67 | .setView(dialogBinding.root) 68 | .setPositiveButton(android.R.string.ok) { _, _ -> 69 | if (!Runner.pingServer()) { 70 | Toast.makeText( 71 | this, 72 | R.string.service_not_running, 73 | Toast.LENGTH_SHORT 74 | ).show() 75 | return@setPositiveButton 76 | } 77 | 78 | mAdapter.add(EnvInfo().apply { 79 | key = dialogBinding.key.text.toString() 80 | value = dialogBinding.value.text.toString() 81 | }) 82 | } 83 | .show() 84 | } catch (_: BaseDialogBuilder.DialogShowingException) { 85 | } 86 | } 87 | 88 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 89 | return if (item.itemId == android.R.id.home) { 90 | finish() 91 | true 92 | } else { 93 | super.onOptionsItemSelected(item) 94 | } 95 | } 96 | 97 | override fun onContextItemSelected(item: MenuItem): Boolean { 98 | return mAdapter.onContextItemSelected(item) 99 | } 100 | 101 | override fun onStart() { 102 | super.onStart() 103 | Runner.refreshStatus() 104 | mAdapter.updateData() 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/activity/envmanage/ItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.activity.envmanage 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import androidx.core.view.size 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 8 | import yangfentuozi.runner.R 9 | import yangfentuozi.runner.databinding.DialogEditEnvE2Binding 10 | import yangfentuozi.runner.databinding.ItemEnvItemBinding 11 | 12 | 13 | class ItemAdapter(private val mContext: EnvManageActivity, value: String) : 14 | RecyclerView.Adapter() { 15 | var data: String = "" 16 | var dataList: ArrayList = ArrayList() 17 | 18 | init { 19 | data = value 20 | dataList = ArrayList(value.split(":")) 21 | 22 | } 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 25 | return ViewHolder(ItemEnvItemBinding.inflate(mContext.layoutInflater, parent, false)) 26 | } 27 | 28 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 29 | var position = holder.bindingAdapterPosition 30 | if (position == itemCount - 1) { 31 | holder.binding.value.visibility = ViewGroup.GONE 32 | holder.binding.remove.apply { 33 | contentDescription = mContext.getString(R.string.add) 34 | icon = mContext.getDrawable(R.drawable.ic_add_24) 35 | setOnClickListener { 36 | dataList.add("") 37 | notifyItemInserted(holder.bindingAdapterPosition) 38 | mContext.binding.recyclerView.scrollToPosition(dataList.size - 1) 39 | } 40 | } 41 | return 42 | } 43 | val info = dataList[position] 44 | holder.binding.value.apply { 45 | setText(info) 46 | keyListener = null 47 | setOnClickListener { 48 | val dialogBinding = DialogEditEnvE2Binding.inflate(mContext.layoutInflater) 49 | MaterialAlertDialogBuilder(mContext) 50 | .setTitle(R.string.edit) 51 | .setView(dialogBinding.root) 52 | .setNegativeButton(android.R.string.ok) { _, _ -> 53 | dataList[position] = dialogBinding.value.text.toString() 54 | text = dialogBinding.value.text 55 | } 56 | .show() 57 | } 58 | } 59 | holder.binding.remove.setOnClickListener { 60 | holder.binding.root.apply { 61 | setFocusable(false) 62 | setFocusableInTouchMode(false) 63 | clearFocus() 64 | 65 | for (i in 0..(root) { 13 | init { 14 | binding.button1.setOnClickListener { v -> 15 | Runner.requestPermission() 16 | } 17 | } 18 | 19 | companion object { 20 | val CREATOR: Creator = Creator { inflater: LayoutInflater?, parent: ViewGroup? -> 21 | val outer = HomeItemContainerBinding.inflate( 22 | inflater!!, parent, false 23 | ) 24 | val inner = HomeShizukuPermRequestBinding.inflate(inflater, outer.getRoot(), true) 25 | GrantShizukuPermViewHolder(inner, outer.getRoot()) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/home/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.home 2 | 3 | import android.annotation.SuppressLint 4 | import rikka.recyclerview.IdBasedRecyclerViewAdapter 5 | import rikka.recyclerview.IndexCreatorPool 6 | import yangfentuozi.runner.Runner 7 | 8 | class HomeAdapter(private val fragment: HomeFragment) : IdBasedRecyclerViewAdapter(ArrayList()) { 9 | 10 | private val shizukuPermissionListener = Runner.ShizukuPermissionListener { 11 | var position = findPositionById(ID_GRANT_SHIZUKU_PERM) 12 | if (position == -1) { 13 | if (it) return@ShizukuPermissionListener 14 | position = 2 15 | addItemAt(position, GrantShizukuPermViewHolder.CREATOR, null, ID_GRANT_SHIZUKU_PERM) 16 | notifyItemInserted(position) 17 | } else { 18 | if (!it) return@ShizukuPermissionListener 19 | removeItemAt(position) 20 | notifyItemRemoved(position) 21 | } 22 | } 23 | 24 | private val shizukuStatusListener = Runner.ShizukuStatusListener { 25 | val position = 1 26 | removeItemAt(position) 27 | addItemAt(position, ShizukuStatusViewHolder.CREATOR, null, ID_SHIZUKU_STATUS) 28 | notifyItemChanged(position) 29 | } 30 | 31 | private val serviceStatusListener = Runner.ServiceStatusListener { 32 | var position = 0 33 | removeItemAt(position) 34 | addItemAt(position, ServiceStatusViewHolder.CREATOR, null, ID_SERVICE_STATUS) 35 | notifyItemChanged(position) 36 | 37 | position = findPositionById(ID_TERM_EXT_STATUS) 38 | if (position == -1) { 39 | if (!it) return@ServiceStatusListener 40 | position = if (findPositionById(ID_GRANT_SHIZUKU_PERM) == -1) 2 else 3 41 | addItemAt(position, TermExtStatusViewHolder.CREATOR, fragment, ID_TERM_EXT_STATUS) 42 | notifyItemInserted(position) 43 | } else { 44 | if (it) return@ServiceStatusListener 45 | removeItemAt(position) 46 | notifyItemRemoved(position) 47 | } 48 | } 49 | 50 | init { 51 | updateData() 52 | setHasStableIds(true) 53 | } 54 | 55 | companion object { 56 | private const val ID_SERVICE_STATUS = 0L 57 | private const val ID_SHIZUKU_STATUS = 1L 58 | private const val ID_GRANT_SHIZUKU_PERM = 2L 59 | private const val ID_TERM_EXT_STATUS = 3L 60 | } 61 | 62 | override fun onCreateCreatorPool(): IndexCreatorPool { 63 | return IndexCreatorPool() 64 | } 65 | 66 | fun findPositionById(id: Long): Int { 67 | for (i in 0 until ids.size) 68 | if (ids[i] == id) 69 | return i 70 | return -1 71 | } 72 | 73 | @SuppressLint("NotifyDataSetChanged") 74 | fun updateData() { 75 | clear() 76 | addItem(ServiceStatusViewHolder.CREATOR, null, ID_SERVICE_STATUS) 77 | addItem(ShizukuStatusViewHolder.CREATOR, null, ID_SHIZUKU_STATUS) 78 | 79 | if (!Runner.shizukuPermission) { 80 | addItem(GrantShizukuPermViewHolder.CREATOR, null, ID_GRANT_SHIZUKU_PERM) 81 | } 82 | 83 | if (Runner.pingServer()) { 84 | addItem(TermExtStatusViewHolder.CREATOR, fragment, ID_TERM_EXT_STATUS) 85 | } 86 | 87 | notifyDataSetChanged() 88 | } 89 | 90 | fun registerListeners() { 91 | Runner.addShizukuPermissionListener(shizukuPermissionListener) 92 | Runner.addShizukuStatusListener(shizukuStatusListener) 93 | Runner.addServiceStatusListener(serviceStatusListener) 94 | } 95 | 96 | fun unregisterListeners() { 97 | Runner.removeShizukuPermissionListener(shizukuPermissionListener) 98 | Runner.removeShizukuStatusListener(shizukuStatusListener) 99 | Runner.removeServiceStatusListener(serviceStatusListener) 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.home 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.TypedValue 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import rikka.recyclerview.addEdgeSpacing 12 | import rikka.recyclerview.addItemSpacing 13 | import rikka.recyclerview.fixEdgeEffect 14 | import rikka.widget.borderview.BorderRecyclerView 15 | import yangfentuozi.runner.R 16 | import yangfentuozi.runner.Runner 17 | import yangfentuozi.runner.base.BaseFragment 18 | import yangfentuozi.runner.databinding.FragmentHomeBinding 19 | import yangfentuozi.runner.ui.activity.InstallTermExtActivity 20 | 21 | class HomeFragment : BaseFragment() { 22 | var binding: FragmentHomeBinding? = null 23 | private set 24 | private var recyclerView: BorderRecyclerView? = null 25 | private val mAdapter = HomeAdapter(this) 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, 29 | container: ViewGroup?, savedInstanceState: Bundle? 30 | ): View? { 31 | binding = FragmentHomeBinding.inflate(inflater, container, false) 32 | 33 | recyclerView = binding!!.list.apply { 34 | adapter = this@HomeFragment.mAdapter 35 | fixEdgeEffect(true, true) 36 | addItemSpacing(0f, 4f, 0f, 4f, TypedValue.COMPLEX_UNIT_DIP) 37 | addEdgeSpacing(16f, 4f, 16f, 4f, TypedValue.COMPLEX_UNIT_DIP) 38 | } 39 | 40 | return binding!!.getRoot() 41 | } 42 | 43 | override fun onDestroyView() { 44 | super.onDestroyView() 45 | binding = null 46 | } 47 | 48 | override fun onStart() { 49 | super.onStart() 50 | val l = View.OnClickListener { v: View? -> recyclerView!!.smoothScrollToPosition(0) } 51 | toolbar.setOnClickListener(l) 52 | Runner.refreshStatus() 53 | mAdapter.updateData() 54 | mAdapter.registerListeners() 55 | } 56 | 57 | override fun onStop() { 58 | super.onStop() 59 | mAdapter.unregisterListeners() 60 | } 61 | 62 | fun installTermExt() { 63 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 64 | addCategory(Intent.CATEGORY_OPENABLE) 65 | type = "application/zip" 66 | } 67 | pickFileLauncher.launch(Intent.createChooser(intent, getString(R.string.pick_term_ext))) 68 | } 69 | 70 | private var pickFileLauncher = 71 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 72 | if (it.resultCode == Activity.RESULT_OK) { 73 | it.data?.data?.let { uri -> 74 | val mimeType = mMainActivity.contentResolver.getType(uri) 75 | if (mimeType != "application/zip") { 76 | return@let 77 | } 78 | 79 | val installIntent = Intent(mMainActivity, InstallTermExtActivity::class.java).apply { 80 | action = Intent.ACTION_VIEW 81 | setDataAndType(uri, "application/zip") 82 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 83 | } 84 | 85 | startActivity(installIntent) 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/home/ServiceStatusViewHolder.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.core.content.ContextCompat 9 | import rikka.recyclerview.BaseViewHolder 10 | import yangfentuozi.runner.R 11 | import yangfentuozi.runner.Runner 12 | import yangfentuozi.runner.databinding.HomeItemContainerBinding 13 | import yangfentuozi.runner.databinding.HomeServiceStatusBinding 14 | 15 | class ServiceStatusViewHolder(binding: HomeServiceStatusBinding, root: View) : 16 | BaseViewHolder(root) { 17 | private val textView: TextView = binding.text1 18 | private val summaryView: TextView = binding.text2 19 | private val iconView: ImageView = binding.icon 20 | 21 | init { 22 | root.setOnClickListener { 23 | if (Runner.pingServer()) return@setOnClickListener 24 | Thread { 25 | Runner.tryBindService() 26 | } 27 | } 28 | } 29 | 30 | override fun onBind() { 31 | val context = this@ServiceStatusViewHolder.itemView.context 32 | val ok = Runner.pingServer() 33 | val version = Runner.serviceVersion 34 | 35 | iconView.setImageDrawable( 36 | ContextCompat.getDrawable( 37 | context, 38 | if (ok) R.drawable.ic_check_circle_outline_24 else R.drawable.ic_error_outline_24 39 | ) 40 | ) 41 | 42 | val title: String? 43 | val summary: String? 44 | 45 | if (ok) { 46 | title = context.getString(R.string.service_is_running) 47 | summary = context.getString(R.string.service_version, version) 48 | } else { 49 | title = context.getString(R.string.service_not_running) 50 | summary = "" 51 | } 52 | 53 | textView.text = title 54 | summaryView.text = summary 55 | 56 | if (summaryView.getText().isEmpty()) { 57 | summaryView.visibility = View.GONE 58 | } else { 59 | summaryView.visibility = View.VISIBLE 60 | } 61 | } 62 | 63 | companion object { 64 | val CREATOR: Creator = Creator { inflater: LayoutInflater?, parent: ViewGroup? -> 65 | val outer = HomeItemContainerBinding.inflate( 66 | inflater!!, parent, false 67 | ) 68 | val inner = HomeServiceStatusBinding.inflate(inflater, outer.getRoot(), true) 69 | ServiceStatusViewHolder(inner, outer.getRoot()) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/home/ShizukuStatusViewHolder.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.home 2 | 3 | import android.text.TextUtils 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.core.content.ContextCompat 10 | import rikka.recyclerview.BaseViewHolder 11 | import yangfentuozi.runner.R 12 | import yangfentuozi.runner.Runner 13 | import yangfentuozi.runner.databinding.HomeItemContainerBinding 14 | import yangfentuozi.runner.databinding.HomeShizukuStatusBinding 15 | 16 | class ShizukuStatusViewHolder(binding: HomeShizukuStatusBinding, root: View) : 17 | BaseViewHolder(root) { 18 | private val textView: TextView = binding.text1 19 | private val summaryView: TextView = binding.text2 20 | private val iconView: ImageView = binding.icon 21 | 22 | override fun onBind() { 23 | val context = this@ShizukuStatusViewHolder.itemView.context 24 | val ok = Runner.shizukuStatus 25 | val isRoot = Runner.shizukuUid == 0 26 | val apiVersion = Runner.shizukuApiVersion 27 | val patchVersion = Runner.shizukuPatchVersion 28 | 29 | iconView.setImageDrawable( 30 | ContextCompat.getDrawable( 31 | context, 32 | if (ok) R.drawable.ic_check_circle_outline_24 else R.drawable.ic_error_outline_24 33 | ) 34 | ) 35 | 36 | 37 | val user = if (isRoot) "root" else "adb" 38 | val title: String? 39 | val summary: String? 40 | 41 | if (ok) { 42 | title = context.getString(R.string.shizuku_is_running) 43 | summary = context.getString( 44 | R.string.shizuku_version, 45 | user, 46 | "$apiVersion.$patchVersion" 47 | ) 48 | } else { 49 | title = context.getString(R.string.shizuku_not_running) 50 | summary = "" 51 | } 52 | 53 | textView.text = title 54 | summaryView.text = summary 55 | 56 | if (TextUtils.isEmpty(summaryView.getText())) { 57 | summaryView.visibility = View.GONE 58 | } else { 59 | summaryView.visibility = View.VISIBLE 60 | } 61 | } 62 | 63 | companion object { 64 | val CREATOR: Creator = Creator { inflater: LayoutInflater?, parent: ViewGroup? -> 65 | val outer = HomeItemContainerBinding.inflate( 66 | inflater!!, parent, false 67 | ) 68 | val inner = HomeShizukuStatusBinding.inflate(inflater, outer.getRoot(), true) 69 | ShizukuStatusViewHolder(inner, outer.getRoot()) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/home/TermExtStatusViewHolder.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.home 2 | 3 | import android.os.RemoteException 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.Button 9 | import android.widget.TextView 10 | import rikka.recyclerview.BaseViewHolder 11 | import yangfentuozi.runner.R 12 | import yangfentuozi.runner.Runner 13 | import yangfentuozi.runner.base.BaseActivity 14 | import yangfentuozi.runner.base.BaseDialogBuilder 15 | import yangfentuozi.runner.databinding.HomeItemContainerBinding 16 | import yangfentuozi.runner.databinding.HomeTermExtStatusBinding 17 | 18 | class TermExtStatusViewHolder(binding: HomeTermExtStatusBinding, root: View) : 19 | BaseViewHolder(root) { 20 | private val title: TextView = binding.text1 21 | private val summaryView: TextView = binding.text2 22 | private val install: Button = binding.button1 23 | private val remove: Button = binding.button2 24 | 25 | override fun onBind() { 26 | install.setOnClickListener { 27 | data.installTermExt() 28 | } 29 | 30 | remove.setOnClickListener { 31 | if (!Runner.pingServer()) return@setOnClickListener 32 | try { 33 | BaseDialogBuilder(context as BaseActivity) 34 | .setTitle(R.string.will_remove_term_ext) 35 | 36 | } catch (_: BaseDialogBuilder.DialogShowingException){} 37 | Thread { 38 | try { 39 | Runner.service?.removeTermExt() 40 | data.runOnMainThread { onBind() } 41 | } catch (e: RemoteException) { 42 | Log.e("TermExtStatusViewHolder", "uninstall term ext error", e) 43 | } 44 | }.start() 45 | } 46 | 47 | if (!Runner.pingServer()) return 48 | 49 | try { 50 | val version = Runner.service?.getTermExtVersion() 51 | if (version == null || version.versionCode == -1) { 52 | // not install 53 | title.setText(R.string.term_ext_title) 54 | summaryView.visibility = View.GONE 55 | install.setText(R.string.install) 56 | remove.visibility = View.GONE 57 | } else { 58 | // installed 59 | title.setText(R.string.term_ext_title_installed) 60 | summaryView.visibility = View.VISIBLE 61 | summaryView.text = context.getString( 62 | R.string.term_ext_version, 63 | version.versionName, 64 | version.versionCode, 65 | version.abi 66 | ) 67 | install.setText(R.string.reinstall) 68 | remove.visibility = View.VISIBLE 69 | } 70 | } catch (e: RemoteException) { 71 | Log.e("TermExtStatusViewHolder", "get term ext version error", e) 72 | } 73 | } 74 | 75 | companion object { 76 | val CREATOR: Creator = Creator { inflater: LayoutInflater?, parent: ViewGroup? -> 77 | val outer = HomeItemContainerBinding.inflate( 78 | inflater!!, parent, false 79 | ) 80 | val inner = HomeTermExtStatusBinding.inflate(inflater, outer.getRoot(), true) 81 | TermExtStatusViewHolder(inner, outer.getRoot()) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/proc/ProcFragment.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.proc 2 | 3 | import android.content.DialogInterface 4 | import android.os.Bundle 5 | import android.os.RemoteException 6 | import android.util.TypedValue 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Toast 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import rikka.recyclerview.addEdgeSpacing 14 | import rikka.recyclerview.addItemSpacing 15 | import rikka.recyclerview.fixEdgeEffect 16 | import yangfentuozi.runner.R 17 | import yangfentuozi.runner.Runner 18 | import yangfentuozi.runner.base.BaseDialogBuilder 19 | import yangfentuozi.runner.base.BaseFragment 20 | import yangfentuozi.runner.databinding.FragmentProcBinding 21 | import yangfentuozi.runner.util.ThrowableUtil.toErrorDialog 22 | 23 | class ProcFragment : BaseFragment() { 24 | private lateinit var mBinding: FragmentProcBinding 25 | val binding get() = mBinding 26 | private var recyclerView: RecyclerView? = null 27 | private lateinit var adapter: ProcAdapter 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, 31 | container: ViewGroup?, savedInstanceState: Bundle? 32 | ): View { 33 | mBinding = FragmentProcBinding.inflate(inflater, container, false) 34 | recyclerView = mBinding.recyclerView 35 | recyclerView!!.setLayoutManager(LinearLayoutManager(mMainActivity)) 36 | recyclerView!!.fixEdgeEffect(false, true) 37 | 38 | adapter = ProcAdapter(mMainActivity, this) 39 | 40 | mBinding.recyclerView.apply { 41 | layoutManager = LinearLayoutManager(mMainActivity) 42 | fixEdgeEffect(true, true) 43 | addItemSpacing(0f, 4f, 0f, 4f, TypedValue.COMPLEX_UNIT_DIP) 44 | addEdgeSpacing(16f, 4f, 16f, 4f, TypedValue.COMPLEX_UNIT_DIP) 45 | adapter = this@ProcFragment.adapter 46 | } 47 | mBinding.swipeRefreshLayout.setOnRefreshListener { 48 | adapter.updateData() 49 | } 50 | 51 | mBinding.procKillAll.setOnClickListener { v -> 52 | if (Runner.pingServer()) { 53 | try { 54 | if (mBinding.recyclerView.adapter 55 | ?.itemCount == 0 56 | ) { 57 | return@setOnClickListener 58 | } 59 | } catch (_: NullPointerException) { 60 | } 61 | } else { 62 | Toast.makeText( 63 | mMainActivity, 64 | R.string.service_not_running, 65 | Toast.LENGTH_SHORT 66 | ).show() 67 | return@setOnClickListener 68 | } 69 | try { 70 | BaseDialogBuilder(mMainActivity) 71 | .setTitle(R.string.kill_all_processes) 72 | .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> 73 | Thread { 74 | if (Runner.pingServer()) { 75 | try { 76 | adapter.killAll() 77 | } catch (e: RemoteException) { 78 | e.toErrorDialog(mMainActivity) 79 | } 80 | } else { 81 | runOnMainThread { 82 | Toast.makeText( 83 | mMainActivity, 84 | R.string.service_not_running, 85 | Toast.LENGTH_SHORT 86 | ).show() 87 | } 88 | } 89 | }.start() 90 | } 91 | .setNegativeButton(android.R.string.cancel, null) 92 | .show() 93 | } catch (_: BaseDialogBuilder.DialogShowingException) { 94 | } 95 | } 96 | return mBinding.getRoot() 97 | } 98 | 99 | override fun onStart() { 100 | super.onStart() 101 | val l = View.OnClickListener { v: View? -> recyclerView!!.smoothScrollToPosition(0) } 102 | toolbar.setOnClickListener(l) 103 | adapter.updateData() 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/runner/RunnerFragment.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.runner 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.util.TypedValue 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.inputmethod.InputMethodManager 10 | import android.widget.Toast 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import rikka.recyclerview.addEdgeSpacing 13 | import rikka.recyclerview.addItemSpacing 14 | import rikka.recyclerview.fixEdgeEffect 15 | import yangfentuozi.runner.R 16 | import yangfentuozi.runner.Runner 17 | import yangfentuozi.runner.base.BaseDialogBuilder 18 | import yangfentuozi.runner.base.BaseFragment 19 | import yangfentuozi.runner.databinding.DialogEditBinding 20 | import yangfentuozi.runner.databinding.FragmentRunnerBinding 21 | import yangfentuozi.runner.service.data.CommandInfo 22 | 23 | class RunnerFragment : BaseFragment() { 24 | private lateinit var mBinding: FragmentRunnerBinding 25 | private lateinit var adapter: CommandAdapter 26 | val binding get() = mBinding 27 | 28 | override fun onCreateView( 29 | inflater: LayoutInflater, 30 | container: ViewGroup?, 31 | savedInstanceState: Bundle? 32 | ): View { 33 | mBinding = FragmentRunnerBinding.inflate(inflater, container, false) 34 | return mBinding.root 35 | } 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | 40 | adapter = CommandAdapter(mMainActivity, this) 41 | 42 | mBinding.recyclerView.apply { 43 | layoutManager = LinearLayoutManager(mMainActivity) 44 | fixEdgeEffect(true, true) 45 | addItemSpacing(0f, 4f, 0f, 4f, TypedValue.COMPLEX_UNIT_DIP) 46 | addEdgeSpacing(16f, 4f, 16f, 4f, TypedValue.COMPLEX_UNIT_DIP) 47 | adapter = this@RunnerFragment.adapter 48 | } 49 | 50 | mBinding.swipeRefreshLayout.setOnRefreshListener { adapter.updateData() } 51 | 52 | mBinding.add.setOnClickListener { 53 | if (mMainActivity.isDialogShowing) return@setOnClickListener 54 | showAddCommandDialog(-1) 55 | } 56 | adapter.updateData() 57 | } 58 | 59 | fun showAddCommandDialog(toPosition: Int) { 60 | val dialogBinding = DialogEditBinding.inflate(LayoutInflater.from(mMainActivity)) 61 | 62 | dialogBinding.apply { 63 | targetPermParent.visibility = View.GONE 64 | reducePerm.setOnCheckedChangeListener { _, isChecked -> 65 | targetPermParent.visibility = if (isChecked) View.VISIBLE else View.GONE 66 | } 67 | 68 | name.requestFocus() 69 | name.postDelayed({ 70 | (mMainActivity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) 71 | .showSoftInput(name, InputMethodManager.SHOW_IMPLICIT) 72 | }, 200) 73 | } 74 | 75 | try { 76 | BaseDialogBuilder(mMainActivity) 77 | .setTitle(R.string.edit) 78 | .setView(dialogBinding.root) 79 | .setPositiveButton(android.R.string.ok) { _, _ -> 80 | if (!Runner.pingServer()) { 81 | Toast.makeText( 82 | mMainActivity, 83 | R.string.service_not_running, 84 | Toast.LENGTH_SHORT 85 | ).show() 86 | return@setPositiveButton 87 | } 88 | 89 | val newCommand = CommandInfo().apply { 90 | command = dialogBinding.command.text.toString() 91 | name = dialogBinding.name.text.toString() 92 | keepAlive = dialogBinding.keepAlive.isChecked 93 | reducePerm = dialogBinding.reducePerm.isChecked 94 | targetPerm = 95 | if (dialogBinding.reducePerm.isChecked) dialogBinding.targetPerm.text.toString() else null 96 | } 97 | 98 | if (toPosition == -1) 99 | adapter.add(newCommand) 100 | else 101 | adapter.addUnderOne(toPosition + 1, newCommand) 102 | } 103 | .show() 104 | } catch (_: BaseDialogBuilder.DialogShowingException) { 105 | } 106 | } 107 | 108 | override fun onContextItemSelected(item: android.view.MenuItem): Boolean { 109 | return adapter.onContextItemSelected(item) 110 | } 111 | 112 | override fun onStart() { 113 | super.onStart() 114 | toolbar.setOnClickListener { 115 | mBinding.recyclerView.smoothScrollToPosition(0) 116 | } 117 | } 118 | 119 | override fun onDestroyView() { 120 | super.onDestroyView() 121 | adapter.close() 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/ui/fragment/terminal/TerminalFragment.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.ui.fragment.terminal 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import yangfentuozi.runner.base.BaseFragment 8 | import yangfentuozi.runner.databinding.FragmentTerminalBinding 9 | 10 | class TerminalFragment : BaseFragment() { 11 | private var binding: FragmentTerminalBinding? = null 12 | 13 | override fun onCreateView( 14 | inflater: LayoutInflater, 15 | container: ViewGroup?, savedInstanceState: Bundle? 16 | ): View? { 17 | binding = FragmentTerminalBinding.inflate(inflater, container, false) 18 | val root: View? = binding!!.getRoot() 19 | return root 20 | } 21 | 22 | override fun onDestroyView() { 23 | super.onDestroyView() 24 | binding = null 25 | } 26 | 27 | 28 | override fun onStart() { 29 | super.onStart() 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/util/ThemeUtil.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.util 2 | 3 | import android.content.Context 4 | import androidx.annotation.StyleRes 5 | import androidx.appcompat.app.AppCompatDelegate 6 | import com.google.android.material.color.DynamicColors 7 | import rikka.core.util.ResourceUtils 8 | import yangfentuozi.runner.App 9 | import yangfentuozi.runner.R 10 | 11 | 12 | object ThemeUtil { 13 | private val colorThemeMap: MutableMap = HashMap() 14 | 15 | const val MODE_NIGHT_FOLLOW_SYSTEM: String = "MODE_NIGHT_FOLLOW_SYSTEM" 16 | const val MODE_NIGHT_NO: String = "MODE_NIGHT_NO" 17 | const val MODE_NIGHT_YES: String = "MODE_NIGHT_YES" 18 | 19 | init { 20 | colorThemeMap.put("MATERIAL_RED", R.style.ThemeOverlay_MaterialRed) 21 | colorThemeMap.put("MATERIAL_PINK", R.style.ThemeOverlay_MaterialPink) 22 | colorThemeMap.put("MATERIAL_PURPLE", R.style.ThemeOverlay_MaterialPurple) 23 | colorThemeMap.put("MATERIAL_DEEP_PURPLE", R.style.ThemeOverlay_MaterialDeepPurple) 24 | colorThemeMap.put("MATERIAL_INDIGO", R.style.ThemeOverlay_MaterialIndigo) 25 | colorThemeMap.put("MATERIAL_BLUE", R.style.ThemeOverlay_MaterialBlue) 26 | colorThemeMap.put("MATERIAL_LIGHT_BLUE", R.style.ThemeOverlay_MaterialLightBlue) 27 | colorThemeMap.put("MATERIAL_CYAN", R.style.ThemeOverlay_MaterialCyan) 28 | colorThemeMap.put("MATERIAL_TEAL", R.style.ThemeOverlay_MaterialTeal) 29 | colorThemeMap.put("MATERIAL_GREEN", R.style.ThemeOverlay_MaterialGreen) 30 | colorThemeMap.put("MATERIAL_LIGHT_GREEN", R.style.ThemeOverlay_MaterialLightGreen) 31 | colorThemeMap.put("MATERIAL_LIME", R.style.ThemeOverlay_MaterialLime) 32 | colorThemeMap.put("MATERIAL_YELLOW", R.style.ThemeOverlay_MaterialYellow) 33 | colorThemeMap.put("MATERIAL_AMBER", R.style.ThemeOverlay_MaterialAmber) 34 | colorThemeMap.put("MATERIAL_ORANGE", R.style.ThemeOverlay_MaterialOrange) 35 | colorThemeMap.put("MATERIAL_DEEP_ORANGE", R.style.ThemeOverlay_MaterialDeepOrange) 36 | colorThemeMap.put("MATERIAL_BROWN", R.style.ThemeOverlay_MaterialBrown) 37 | colorThemeMap.put("MATERIAL_BLUE_GREY", R.style.ThemeOverlay_MaterialBlueGrey) 38 | } 39 | 40 | private const val THEME_DEFAULT = "DEFAULT" 41 | private const val THEME_BLACK = "BLACK" 42 | 43 | private val isBlackNightTheme: Boolean 44 | get() = App.preferences.getBoolean("black_dark_theme", false) 45 | 46 | val isSystemAccent: Boolean 47 | get() = DynamicColors.isDynamicColorAvailable() && App.preferences.getBoolean( 48 | "follow_system_accent", 49 | true 50 | ) 51 | 52 | fun getNightTheme(context: Context): String { 53 | if (isBlackNightTheme 54 | && ResourceUtils.isNightMode(context.resources.configuration) 55 | ) return THEME_BLACK 56 | 57 | return THEME_DEFAULT 58 | } 59 | 60 | @StyleRes 61 | fun getNightThemeStyleRes(context: Context): Int { 62 | return when (getNightTheme(context)) { 63 | THEME_BLACK -> R.style.ThemeOverlay_Black 64 | THEME_DEFAULT -> R.style.ThemeOverlay 65 | else -> R.style.ThemeOverlay 66 | } 67 | } 68 | 69 | val colorTheme: String 70 | get() { 71 | if (isSystemAccent) { 72 | return "SYSTEM" 73 | } 74 | return App.preferences.getString("theme_color", "COLOR_BLUE")!! 75 | } 76 | 77 | @get:StyleRes 78 | val colorThemeStyleRes: Int 79 | get() { 80 | val theme = colorThemeMap[colorTheme] 81 | if (theme == null) { 82 | return R.style.ThemeOverlay_MaterialBlue 83 | } 84 | return theme 85 | } 86 | 87 | fun getDarkTheme(mode: String): Int { 88 | return when (mode) { 89 | MODE_NIGHT_FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 90 | MODE_NIGHT_YES -> AppCompatDelegate.MODE_NIGHT_YES 91 | MODE_NIGHT_NO -> AppCompatDelegate.MODE_NIGHT_NO 92 | else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 93 | } 94 | } 95 | 96 | val darkTheme: Int 97 | get() = getDarkTheme( 98 | App.preferences.getString( 99 | "dark_theme", 100 | MODE_NIGHT_FOLLOW_SYSTEM 101 | )!! 102 | ) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/yangfentuozi/runner/util/ThrowableUtil.kt: -------------------------------------------------------------------------------- 1 | package yangfentuozi.runner.util 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Looper 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import yangfentuozi.runner.R 8 | 9 | object ThrowableUtil { 10 | 11 | fun Throwable.toErrorDialog(context: Activity) { 12 | stackTraceToString().toErrorDialog(context) 13 | } 14 | 15 | fun CharSequence.toErrorDialog(context: Activity) { 16 | if (Looper.getMainLooper() == Looper.myLooper()) { 17 | createDialog(context, this) 18 | } else { 19 | context.runOnUiThread { createDialog(context, this) } 20 | } 21 | } 22 | 23 | private fun createDialog(context: Context, errorMsg: CharSequence) { 24 | MaterialAlertDialogBuilder(context) 25 | .setTitle(R.string.error) 26 | .setMessage(errorMsg) 27 | .show() 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/res/color-night/home_card_background_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/home_card_background_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backup_restore_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_configs_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dark_mode_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty_icon_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_format_color_fill_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_help_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_baseline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_import_export_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_invert_colors_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_language_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 24 | 27 | 28 | 32 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_layers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_layers_baseline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_layers_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_merge_type_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_palette_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_baseline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_restore_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_run_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_applications_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_baseline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_circle_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_terminal_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_circle_icon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_crash_report.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 37 | 41 | 42 | 46 | 47 | 53 | 54 | 61 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_env_manage.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 22 | 23 | 24 | 25 | 30 | 31 | 36 | 37 | 44 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | 23 | 24 | 32 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_stream_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | 23 | 24 | 29 | 30 | 33 | 34 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_about.xml: -------------------------------------------------------------------------------- 1 | 20 | 26 | 27 | 32 | 33 | 38 | 39 | 45 | 46 | 51 | 52 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 18 | 19 | 23 | 24 | 29 | 30 | 31 | 32 | 37 | 38 | 43 | 44 | 45 | 46 | 50 | 51 | 56 | 57 | 62 | 63 | 64 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_edit_env.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 21 | 22 | 23 | 24 | 30 | 31 | 36 | 37 | 42 | 43 | 44 | 45 |