├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── nrfr │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── nrfr │ │ │ ├── MainActivity.kt │ │ │ ├── data │ │ │ ├── CountryPresets.kt │ │ │ └── PresetCarriers.kt │ │ │ ├── manager │ │ │ └── CarrierConfigManager.kt │ │ │ ├── model │ │ │ └── SimCardInfo.kt │ │ │ └── ui │ │ │ ├── screens │ │ │ ├── AboutScreen.kt │ │ │ ├── MainScreen.kt │ │ │ └── ShizukuNotReadyScreen.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── github │ └── nrfr │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── docs └── images │ ├── app.png │ └── client.png ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── nrfr-client ├── .gitignore ├── README.md ├── app.go ├── build │ ├── README.md │ ├── appicon.png │ ├── darwin │ │ ├── Info.dev.plist │ │ └── Info.plist │ └── windows │ │ ├── icon.ico │ │ ├── info.json │ │ ├── installer │ │ ├── project.nsi │ │ └── wails_tools.nsh │ │ └── wails.exe.manifest ├── frontend │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── package.json.md5 │ ├── postcss.config.js │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ └── images │ │ │ │ └── logo.png │ │ ├── components │ │ │ ├── layout │ │ │ │ ├── ErrorBoundary.tsx │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ ├── StepIndicator.tsx │ │ │ │ └── TitleBar.tsx │ │ │ └── steps │ │ │ │ ├── AppCheck.tsx │ │ │ │ ├── AppInstall.tsx │ │ │ │ ├── Complete.tsx │ │ │ │ ├── DeviceSelection.tsx │ │ │ │ └── ServiceStart.tsx │ │ ├── main.tsx │ │ ├── styles │ │ │ └── global.css │ │ ├── types │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── wailsjs │ │ ├── go │ │ ├── main │ │ │ ├── App.d.ts │ │ │ └── App.js │ │ └── models.ts │ │ └── runtime │ │ ├── package.json │ │ ├── runtime.d.ts │ │ └── runtime.js ├── go.mod ├── go.sum ├── main.go └── wails.json └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | # 添加权限配置 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | # 获取版本号和提交信息 22 | - name: Get Version Info 23 | id: version 24 | run: | 25 | VERSION=${GITHUB_REF#refs/tags/} 26 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 27 | 28 | if git rev-list --tags --max-count=1 > /dev/null 2>&1; then 29 | LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-list --max-parents=0 HEAD) 30 | 31 | # 获取所有提交并过滤掉CI相关提交和文档提交 32 | ALL_COMMITS=$(git log --pretty=format:"%h %s (%an)" $LAST_TAG..HEAD | grep -v -E "ci:|docs:" || true) 33 | 34 | # 分别获取不同类型的提交 35 | FEAT_COMMITS=$(echo "$ALL_COMMITS" | grep "^[a-f0-9]\+ feat:" || true) 36 | FIX_COMMITS=$(echo "$ALL_COMMITS" | grep "^[a-f0-9]\+ fix:" || true) 37 | OTHER_COMMITS=$(echo "$ALL_COMMITS" | grep -v "^[a-f0-9]\+ \(feat:\|fix:\)" || true) 38 | 39 | # 组装changelog 40 | CHANGELOG="" 41 | if [ ! -z "$FEAT_COMMITS" ]; then 42 | CHANGELOG+="### 功能改进\n$FEAT_COMMITS\n" 43 | fi 44 | if [ ! -z "$FIX_COMMITS" ]; then 45 | CHANGELOG+="### 问题修复\n$FIX_COMMITS\n" 46 | fi 47 | if [ ! -z "$OTHER_COMMITS" ]; then 48 | CHANGELOG+="### 其他更新\n$OTHER_COMMITS" 49 | fi 50 | else 51 | CHANGELOG="### 首次发布\n$(git log --pretty=format:"- %h %s (%an)" | grep -v "ci:" || true)" 52 | fi 53 | 54 | # 如果changelog为空,添加默认信息 55 | if [ -z "$CHANGELOG" ]; then 56 | CHANGELOG="### 无更新内容" 57 | fi 58 | 59 | # 输出 60 | echo -e "$CHANGELOG" 61 | 62 | # 将 changelog 写入文件 63 | echo -e "$CHANGELOG" > CHANGELOG.md 64 | shell: bash 65 | 66 | # 设置 Java 环境 67 | - name: Set up JDK 68 | uses: actions/setup-java@v4 69 | with: 70 | distribution: "temurin" 71 | java-version: "21" 72 | 73 | # 设置 Android SDK 74 | - name: Set up Android SDK 75 | uses: android-actions/setup-android@v3 76 | 77 | # 下载替换 android.jar 78 | - name: Download android.jar 79 | run: | 80 | mkdir -p $ANDROID_HOME/platforms/android-34 81 | curl -L https://raw.githubusercontent.com/Reginer/aosp-android-jar/refs/heads/main/android-34/android.jar -o $ANDROID_HOME/platforms/android-34/android.jar 82 | shell: bash 83 | 84 | # 构建 Android APK 85 | - name: Build Android APK 86 | run: | 87 | gradle wrapper 88 | chmod +x ./gradlew 89 | ./gradlew assembleDebug 90 | shell: bash 91 | 92 | # 设置 Go 环境 93 | - name: Set up Go 94 | uses: actions/setup-go@v5 95 | with: 96 | go-version: "1.23" 97 | 98 | # 设置 Node.js 环境 99 | - name: Set up Node.js 100 | uses: actions/setup-node@v4 101 | with: 102 | node-version: "21" 103 | 104 | # 安装 Wails 105 | - name: Install Wails 106 | run: go install github.com/wailsapp/wails/v2/cmd/wails@latest 107 | 108 | # 构建 Wails 应用 109 | - name: Build Wails App 110 | run: | 111 | cd nrfr-client/frontend 112 | npm install 113 | cd .. 114 | wails build 115 | shell: bash 116 | 117 | # 下载最新的 Shizuku APK 118 | - name: Download Shizuku APK 119 | run: | 120 | mkdir -p nrfr-client/build/bin/resources 121 | curl -L $(curl -s https://api.github.com/repos/RikkaApps/Shizuku/releases/latest | grep "browser_download_url.*apk" | cut -d '"' -f 4) -o nrfr-client/build/bin/resources/shizuku.apk 122 | shell: bash 123 | 124 | # 复制 Android APK 125 | - name: Copy Android APK 126 | run: | 127 | cp app/build/outputs/apk/debug/app-debug.apk nrfr-client/build/bin/resources/nrfr.apk 128 | cp app/build/outputs/apk/debug/app-debug.apk nrfr-${{ steps.version.outputs.version }}.apk 129 | shell: bash 130 | 131 | # 下载 platform-tools 132 | - name: Download platform-tools 133 | run: | 134 | mkdir -p nrfr-client/build/bin/platform-tools 135 | if [ "$RUNNER_OS" == "Windows" ]; then 136 | curl -L https://dl.google.com/android/repository/platform-tools-latest-windows.zip -o platform-tools.zip 137 | elif [ "$RUNNER_OS" == "Linux" ]; then 138 | curl -L https://dl.google.com/android/repository/platform-tools-latest-linux.zip -o platform-tools.zip 139 | else 140 | curl -L https://dl.google.com/android/repository/platform-tools-latest-darwin.zip -o platform-tools.zip 141 | fi 142 | unzip platform-tools.zip 143 | cp -r platform-tools/* nrfr-client/build/bin/platform-tools/ 144 | rm -rf platform-tools platform-tools.zip 145 | shell: bash 146 | 147 | # 打包发布文件 148 | - name: Create Release Archive 149 | run: | 150 | cd nrfr-client/build/bin 151 | 7z a "../../nrfr-${{ steps.version.outputs.version }}-release.zip" ./* 152 | shell: bash 153 | 154 | # 创建 Release 155 | - name: Create Release 156 | uses: softprops/action-gh-release@v1 157 | if: startsWith(github.ref, 'refs/tags/') 158 | with: 159 | files: | 160 | nrfr-client/nrfr-${{ steps.version.outputs.version }}-release.zip 161 | nrfr-${{ steps.version.outputs.version }}.apk 162 | body_path: CHANGELOG.md 163 | env: 164 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | *.log 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Nrfr

3 |

🌍 免 Root 的 SIM 卡国家码修改工具,让你的网络更自由

4 | 5 |

6 | Platform 7 | Android Version 8 | Go Version 9 | React Version 10 | TypeScript Version 11 | Tailwind Version 12 | Wails Version 13 |

14 | 15 |

16 | Stars 17 | Forks 18 | Issues 19 | Last Commit 20 | Release 21 | Downloads 22 | License 23 | Follow on X 24 |

25 | 26 |
27 | 快速启动工具界面 28 | Android 应用界面 29 |
30 |
31 |
32 | 33 | Nrfr 是一款强大的 SIM 卡国家码修改工具,无需 Root 权限即可修改 SIM 卡国家码。本项目完全基于 Android 系统原生 API 实现,不依赖 34 | Xposed、Magisk 等任何第三方框架,仅通过调用系统级接口实现功能。通过修改国家码,你可以: 35 | 36 | - 🌏 解锁运营商限制,使用更多本地功能 37 | - 🔓 突破某些区域限制的应用和服务 38 | - 🛠️ 解决国际漫游时的兼容性问题 39 | - 🌐 帮助使用海外 SIM 卡获得更好的本地化体验 40 | - ⚙️ 解决部分应用识别 SIM 卡地区错误的问题 41 | 42 | ## 📱 使用案例 43 | 44 | ### 运营商配置优化 45 | 46 | - 手机无法正确识别运营商配置 47 | - 某些运营商特定功能无法使用 48 | - 网络配置与当地运营商不匹配 49 | 50 | ### 运营商参数适配 51 | 52 | - 运营商功能配置不完整 53 | - 网络参数与运营商默认配置不匹配 54 | - 运营商特定服务无法正常启用 55 | 56 | ### 漫游网络识别 57 | 58 | - 漫游时运营商名称显示异常 59 | - 网络配置与漫游地运营商不匹配 60 | - 运营商特定功能无法使用 61 | 62 | ### TikTok 区域限制解除 63 | 64 | - TikTok 网络错误 65 | - 无法正常使用 TikTok 的完整功能 66 | 67 | 你可以: 68 | 69 | 1. 使用 Nrfr 修改 SIM 卡国家码为支持的地区(如 JP、US 等) 70 | 2. 重新打开 TikTok,就可以正常使用了 71 | 72 | ## 💡 实现原理 73 | 74 | Nrfr 通过调用 Android 系统级 API(CarrierConfigLoader)修改系统内的运营商配置参数,而**不是直接修改 SIM 卡**。这种实现方式: 75 | 76 | - 完全在系统层面工作,不会对 SIM 卡本身进行任何修改或造成损坏 77 | - 仅改变系统对 SIM 卡信息的读取方式 78 | - 基于 Android 原生 API 实现,不依赖任何第三方框架(如 Xposed、Magisk 等) 79 | - 通过 Shizuku 仅提供必要的权限支持 80 | - 所有修改都是可逆的,随时可以还原 81 | 82 | ## ✨ 特性 83 | 84 | - 🔒 安全可靠 85 | - 无需 Root 权限 86 | - 不修改系统文件 87 | - 不影响系统稳定性 88 | - 不会对 SIM 卡造成任何影响 89 | - 🔄 功能完善 90 | - 支持随时还原修改 91 | - 支持双卡设备,可分别配置 92 | - 一次修改永久生效,重启后保持 93 | - 🚀 简单易用 94 | - 一键启动工具 95 | - 智能检测设备和 SIM 卡状态 96 | - 自动安装所需应用 97 | - 简洁优雅的用户界面 98 | - 轻量且高效,安装包体积小 99 | 100 | ## ⚠️ 注意事项 101 | 102 | - 需要安装并启用 Shizuku 103 | - 修改国家码可能会影响运营商服务,请谨慎操作 104 | - 部分设备可能不支持修改国家码 105 | - 如需还原设置,请使用应用内的还原功能 106 | 107 | ## 🚀 快速开始 108 | 109 | 下载页面有两个文件,一个是含快速启动工具的压缩包,另一个就只是 APK 安装包。**推荐使用快速启动工具**,请按照以下步骤操作: 110 | 111 | 1. 准备手机 112 | - 启用开发者选项(具体的自己查一下) 113 | - 进入开发者选项,开启 USB 调试 114 | - 开启 USB 调试(安全设置),如果有就开启 115 | - 开启 USB 安装(允许通过 USB 安装应用) 116 | - 如果提示未知来源应用安装,请允许从此来源安装 117 | 118 | 2. 连接手机到电脑 119 | - 使用数据线将手机连接到电脑 120 | - 在手机上允许 USB 调试授权 121 | 122 | 3. 下载并启动 Nrfr 快速启动工具 123 | - 从 Release 页面下载最新版本的快速启动工具 124 | - 解压并运行 Nrfr 快速启动工具 125 | - 工具会自动检测已连接的设备 126 | 127 | 4. 安装必要组件 128 | - 工具会自动安装 Shizuku 到手机 129 | - 按照提示启用 Shizuku 130 | - 等待工具自动安装 Nrfr 应用 131 | 132 | 5. 修改国家码 133 | - 在手机上打开 Nrfr 应用 134 | - 选择需要修改的 SIM 卡 135 | - 设置目标国家码 136 | - 应用修改 137 | 138 | 修改完成后无需重启设备,设置会立即生效并永久保持。如需还原,请使用应用内的还原功能。 139 | 140 | ## 📦 构建 141 | 142 | 项目包含两个部分:快速启动工具(桌面端)和手机应用(Android)。 143 | 144 | ### 快速启动工具 (nrfr-client) 145 | 146 | ```bash 147 | # 进入客户端目录 148 | cd nrfr-client 149 | 150 | # 安装依赖 151 | npm install 152 | 153 | # 开发模式 154 | wails dev 155 | 156 | # 构建发布版本 157 | wails build 158 | ``` 159 | 160 | ### Android 应用 (app) 161 | 162 | ```bash 163 | # 进入 Android 应用目录 164 | cd app 165 | 166 | # 使用 Gradle 构建 Debug 版本 167 | ./gradlew assembleDebug 168 | ``` 169 | 170 | 构建完成后,可以在以下位置找到生成的文件: 171 | 172 | - 快速启动工具: `nrfr-client/build/bin/` 173 | - Android 应用: `app/build/outputs/apk/` 174 | 175 | ## 📝 依赖项 176 | 177 | - [Shizuku](https://shizuku.rikka.app/) - 用于提供特权服务 178 | - [ADB](https://developer.android.com/tools/adb) - Android 调试桥接 179 | 180 | ## 🤝 贡献 181 | 182 | 欢迎提交 Pull Request 和 Issue!在提交之前,请确保: 183 | 184 | - 代码经过测试 185 | - 遵循现有的代码风格 186 | - 更新相关文档 187 | - 描述清楚改动的目的和影响 188 | 189 | ## 📄 许可证 190 | 191 | 本项目采用 [Apache-2.0](LICENSE) 许可证。 192 | 193 | ## ⚠️ 免责声明 194 | 195 | 本工具仅供学习和研究使用。使用本工具修改系统设置可能会影响设备的正常使用,请自行承担风险。作者不对任何可能的损失负责。 196 | 197 | ## 💖 支持 198 | 199 | 如果你觉得这个项目有帮助: 200 | 201 | - 在 X 上关注 [@actkites](https://x.com/intent/follow?screen_name=actkites) 202 | - 给项目点个 Star ⭐ 203 | - 分享给更多的人 204 | 205 | ## ⭐ Star History 206 | 207 | [![Star History Chart](https://api.star-history.com/svg?repos=Ackites/Nrfr&type=Date)](https://star-history.com/#Ackites/Nrfr&Date) 208 | 209 | ## 🙏 鸣谢 210 | 211 | - [Shizuku](https://shizuku.rikka.app/) - 感谢 Shizuku 提供的特权服务支持 212 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.apk 3 | *.aab 4 | *.ap_ 5 | *.dex 6 | *.class 7 | bin/ 8 | gen/ 9 | out/ 10 | release/ 11 | debug/ 12 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("org.jetbrains.kotlin.plugin.compose") 5 | } 6 | 7 | android { 8 | namespace = "com.github.nrfr" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | applicationId = "com.github.nrfr" 13 | minSdk = 26 14 | targetSdk = 34 15 | versionCode = 3 //版本更新 +1 16 | versionName = "1.0.3" //同步更新版本号 rfr-client/app.go 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = true 24 | isShrinkResources = true 25 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = "1.8" 34 | } 35 | buildFeatures { 36 | compose = true 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation("androidx.core:core-ktx:1.12.0") 42 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") 43 | implementation("androidx.activity:activity-compose:1.8.2") 44 | implementation(platform("androidx.compose:compose-bom:2023.10.01")) 45 | implementation("androidx.compose.ui:ui") 46 | implementation("androidx.compose.ui:ui-graphics") 47 | implementation("androidx.compose.ui:ui-tooling-preview") 48 | implementation("androidx.compose.material3:material3") 49 | 50 | // Shizuku 51 | implementation("dev.rikka.shizuku:api:13.1.5") 52 | implementation("dev.rikka.shizuku:provider:13.1.5") 53 | implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") 54 | 55 | testImplementation("junit:junit:4.13.2") 56 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 57 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 58 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) 59 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 60 | debugImplementation("androidx.compose.ui:ui-tooling") 61 | debugImplementation("androidx.compose.ui:ui-test-manifest") 62 | } 63 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/nrfr/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.github.nrfr", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr 2 | 3 | import android.content.pm.PackageManager 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.setValue 12 | import com.github.nrfr.ui.screens.AboutScreen 13 | import com.github.nrfr.ui.screens.MainScreen 14 | import com.github.nrfr.ui.screens.ShizukuNotReadyScreen 15 | import com.github.nrfr.ui.theme.NrfrTheme 16 | import org.lsposed.hiddenapibypass.HiddenApiBypass 17 | import rikka.shizuku.Shizuku 18 | 19 | class MainActivity : ComponentActivity() { 20 | private var isShizukuReady by mutableStateOf(false) 21 | private var showAbout by mutableStateOf(false) 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | enableEdgeToEdge() 26 | 27 | // 初始化 Hidden API 访问 28 | HiddenApiBypass.addHiddenApiExemptions("L") 29 | HiddenApiBypass.addHiddenApiExemptions("I") 30 | 31 | // 检查 Shizuku 状态 32 | checkShizukuStatus() 33 | 34 | // 添加 Shizuku 权限监听器 35 | Shizuku.addRequestPermissionResultListener { _, grantResult -> 36 | isShizukuReady = grantResult == PackageManager.PERMISSION_GRANTED 37 | if (!isShizukuReady) { 38 | Toast.makeText(this, "需要 Shizuku 权限才能运行", Toast.LENGTH_LONG).show() 39 | } 40 | } 41 | 42 | // 添加 Shizuku 绑定监听器 43 | Shizuku.addBinderReceivedListener { 44 | checkShizukuStatus() 45 | } 46 | 47 | setContent { 48 | NrfrTheme { 49 | if (showAbout) { 50 | AboutScreen(onBack = { showAbout = false }) 51 | } else if (isShizukuReady) { 52 | MainScreen(onShowAbout = { showAbout = true }) 53 | } else { 54 | ShizukuNotReadyScreen() 55 | } 56 | } 57 | } 58 | } 59 | 60 | private fun checkShizukuStatus() { 61 | isShizukuReady = if (Shizuku.getBinder() == null) { 62 | Toast.makeText(this, "请先安装并启用 Shizuku", Toast.LENGTH_LONG).show() 63 | false 64 | } else { 65 | val hasPermission = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 66 | if (!hasPermission) { 67 | Shizuku.requestPermission(0) 68 | } 69 | hasPermission 70 | } 71 | } 72 | 73 | override fun onDestroy() { 74 | super.onDestroy() 75 | Shizuku.removeRequestPermissionResultListener { _, _ -> } 76 | Shizuku.removeBinderReceivedListener { } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/data/CountryPresets.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.data 2 | 3 | object CountryPresets { 4 | data class CountryInfo( 5 | val code: String, 6 | val name: String 7 | ) 8 | 9 | val countries = listOf( 10 | CountryInfo("CN", "中国"), 11 | CountryInfo("HK", "中国香港"), 12 | CountryInfo("MO", "中国澳门"), 13 | CountryInfo("TW", "中国台湾"), 14 | CountryInfo("JP", "日本"), 15 | CountryInfo("KR", "韩国"), 16 | CountryInfo("US", "美国"), 17 | CountryInfo("GB", "英国"), 18 | CountryInfo("DE", "德国"), 19 | CountryInfo("FR", "法国"), 20 | CountryInfo("IT", "意大利"), 21 | CountryInfo("ES", "西班牙"), 22 | CountryInfo("PT", "葡萄牙"), 23 | CountryInfo("RU", "俄罗斯"), 24 | CountryInfo("IN", "印度"), 25 | CountryInfo("AU", "澳大利亚"), 26 | CountryInfo("NZ", "新西兰"), 27 | CountryInfo("SG", "新加坡"), 28 | CountryInfo("MY", "马来西亚"), 29 | CountryInfo("TH", "泰国"), 30 | CountryInfo("VN", "越南"), 31 | CountryInfo("ID", "印度尼西亚"), 32 | CountryInfo("PH", "菲律宾"), 33 | CountryInfo("CA", "加拿大"), 34 | CountryInfo("MX", "墨西哥"), 35 | CountryInfo("BR", "巴西"), 36 | CountryInfo("AR", "阿根廷"), 37 | CountryInfo("ZA", "南非") 38 | ).sortedBy { it.name } // 按国家名称排序 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/data/PresetCarriers.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.data 2 | 3 | object PresetCarriers { 4 | data class CarrierPreset( 5 | val name: String, 6 | val displayName: String, 7 | val region: String 8 | ) 9 | 10 | val presets = listOf( 11 | // 中国大陆运营商 12 | CarrierPreset("中国移动", "China Mobile", "CN"), 13 | CarrierPreset("中国联通", "China Unicom", "CN"), 14 | CarrierPreset("中国电信", "China Telecom", "CN"), 15 | 16 | // 中国香港运营商 17 | CarrierPreset("中国移动香港", "CMHK", "HK"), 18 | CarrierPreset("香港电讯", "HKT", "HK"), 19 | CarrierPreset("3香港", "3HK", "HK"), 20 | CarrierPreset("SmarTone", "SmarTone", "HK"), 21 | 22 | // 中国澳门运营商 23 | CarrierPreset("澳门电讯", "CTM", "MO"), 24 | CarrierPreset("3澳门", "3 Macau", "MO"), 25 | 26 | // 中国台湾运营商 27 | CarrierPreset("中华电信", "Chunghwa Telecom", "TW"), 28 | CarrierPreset("台湾大哥大", "Taiwan Mobile", "TW"), 29 | CarrierPreset("远传电信", "FarEasTone", "TW"), 30 | 31 | // 日本运营商 32 | CarrierPreset("NTT docomo", "NTT docomo", "JP"), 33 | CarrierPreset("au", "au by KDDI", "JP"), 34 | CarrierPreset("Softbank", "Softbank", "JP"), 35 | CarrierPreset("Rakuten", "Rakuten Mobile", "JP"), 36 | 37 | // 韩国运营商 38 | CarrierPreset("SK Telecom", "SK Telecom", "KR"), 39 | CarrierPreset("KT", "KT Corporation", "KR"), 40 | CarrierPreset("LG U+", "LG U+", "KR"), 41 | 42 | // 美国运营商 43 | CarrierPreset("AT&T", "AT&T", "US"), 44 | CarrierPreset("T-Mobile", "T-Mobile USA", "US"), 45 | CarrierPreset("Verizon", "Verizon", "US"), 46 | CarrierPreset("Sprint", "Sprint", "US"), 47 | 48 | // 英国运营商 49 | CarrierPreset("EE", "EE", "GB"), 50 | CarrierPreset("O2", "O2 UK", "GB"), 51 | CarrierPreset("Three", "Three UK", "GB"), 52 | CarrierPreset("Vodafone", "Vodafone UK", "GB"), 53 | 54 | // 新加坡运营商 55 | CarrierPreset("Singtel", "Singtel", "SG"), 56 | CarrierPreset("StarHub", "StarHub", "SG"), 57 | CarrierPreset("M1", "M1", "SG"), 58 | 59 | // 马来西亚运营商 60 | CarrierPreset("Maxis", "Maxis", "MY"), 61 | CarrierPreset("Celcom", "Celcom", "MY"), 62 | CarrierPreset("Digi", "Digi", "MY"), 63 | CarrierPreset("U Mobile", "U Mobile", "MY"), 64 | 65 | // 泰国运营商 66 | CarrierPreset("AIS", "AIS", "TH"), 67 | CarrierPreset("DTAC", "DTAC", "TH"), 68 | CarrierPreset("True Move H", "True Move H", "TH"), 69 | 70 | // 越南运营商 71 | CarrierPreset("Viettel", "Viettel Mobile", "VN"), 72 | CarrierPreset("Vinaphone", "Vinaphone", "VN"), 73 | CarrierPreset("Mobifone", "Mobifone", "VN"), 74 | 75 | // 印度尼西亚运营商 76 | CarrierPreset("Telkomsel", "Telkomsel", "ID"), 77 | CarrierPreset("Indosat", "Indosat Ooredoo", "ID"), 78 | CarrierPreset("XL Axiata", "XL Axiata", "ID"), 79 | 80 | // 菲律宾运营商 81 | CarrierPreset("Globe", "Globe Telecom", "PH"), 82 | CarrierPreset("Smart", "Smart Communications", "PH"), 83 | CarrierPreset("DITO", "DITO Telecommunity", "PH"), 84 | 85 | // 印度运营商 86 | CarrierPreset("Jio", "Reliance Jio", "IN"), 87 | CarrierPreset("Airtel", "Bharti Airtel", "IN"), 88 | CarrierPreset("Vi", "Vodafone Idea", "IN"), 89 | 90 | // 澳大利亚运营商 91 | CarrierPreset("Telstra", "Telstra", "AU"), 92 | CarrierPreset("Optus", "Optus", "AU"), 93 | CarrierPreset("Vodafone", "Vodafone AU", "AU"), 94 | 95 | // 加拿大运营商 96 | CarrierPreset("Bell", "Bell Mobility", "CA"), 97 | CarrierPreset("Rogers", "Rogers Wireless", "CA"), 98 | CarrierPreset("Telus", "Telus Mobility", "CA"), 99 | 100 | // 德国运营商 101 | CarrierPreset("Telekom", "T-Mobile DE", "DE"), 102 | CarrierPreset("Vodafone", "Vodafone DE", "DE"), 103 | CarrierPreset("O2", "O2 DE", "DE"), 104 | 105 | // 法国运营商 106 | CarrierPreset("Orange", "Orange FR", "FR"), 107 | CarrierPreset("SFR", "SFR", "FR"), 108 | CarrierPreset("Free", "Free Mobile", "FR"), 109 | CarrierPreset("Bouygues", "Bouygues Telecom", "FR"), 110 | 111 | // 意大利运营商 112 | CarrierPreset("TIM", "Telecom Italia", "IT"), 113 | CarrierPreset("Vodafone", "Vodafone IT", "IT"), 114 | CarrierPreset("Wind Tre", "Wind Tre", "IT"), 115 | 116 | // 西班牙运营商 117 | CarrierPreset("Movistar", "Movistar", "ES"), 118 | CarrierPreset("Vodafone", "Vodafone ES", "ES"), 119 | CarrierPreset("Orange", "Orange ES", "ES"), 120 | 121 | // 俄罗斯运营商 122 | CarrierPreset("MTS", "MTS", "RU"), 123 | CarrierPreset("MegaFon", "MegaFon", "RU"), 124 | CarrierPreset("Beeline", "Beeline", "RU"), 125 | 126 | // 巴西运营商 127 | CarrierPreset("Vivo", "Vivo", "BR"), 128 | CarrierPreset("Claro", "Claro", "BR"), 129 | CarrierPreset("TIM", "TIM Brasil", "BR"), 130 | 131 | // 自定义选项 132 | CarrierPreset("自定义", "", "") 133 | ) 134 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/manager/CarrierConfigManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.manager 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.PersistableBundle 6 | import android.telephony.CarrierConfigManager 7 | import android.telephony.SubscriptionManager 8 | import android.telephony.TelephonyFrameworkInitializer 9 | import android.telephony.TelephonyManager 10 | import com.android.internal.telephony.ICarrierConfigLoader 11 | import com.github.nrfr.model.SimCardInfo 12 | import rikka.shizuku.ShizukuBinderWrapper 13 | 14 | object CarrierConfigManager { 15 | fun getSimCards(context: Context): List { 16 | val simCards = mutableListOf() 17 | val subId1 = SubscriptionManager.getSubId(0) 18 | val subId2 = SubscriptionManager.getSubId(1) 19 | 20 | if (subId1 != null) { 21 | val config1 = getCurrentConfig(subId1[0]) 22 | simCards.add(SimCardInfo(1, subId1[0], getCarrierNameBySubId(context, subId1[0]), config1)) 23 | } 24 | if (subId2 != null) { 25 | val config2 = getCurrentConfig(subId2[0]) 26 | simCards.add(SimCardInfo(2, subId2[0], getCarrierNameBySubId(context, subId2[0]), config2)) 27 | } 28 | 29 | return simCards 30 | } 31 | 32 | private fun getCurrentConfig(subId: Int): Map { 33 | try { 34 | val carrierConfigLoader = ICarrierConfigLoader.Stub.asInterface( 35 | ShizukuBinderWrapper( 36 | TelephonyFrameworkInitializer 37 | .getTelephonyServiceManager() 38 | .carrierConfigServiceRegisterer 39 | .get() 40 | ) 41 | ) 42 | val config = carrierConfigLoader.getConfigForSubId(subId, "com.github.nrfr") ?: return emptyMap() 43 | 44 | val result = mutableMapOf() 45 | 46 | // 获取国家码配置 47 | config.getString(CarrierConfigManager.KEY_SIM_COUNTRY_ISO_OVERRIDE_STRING)?.let { 48 | result["国家码"] = it 49 | } 50 | 51 | // 获取运营商名称配置 52 | if (config.getBoolean(CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL, false)) { 53 | config.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING)?.let { 54 | result["运营商名称"] = it 55 | } 56 | } 57 | 58 | return result 59 | } catch (e: Exception) { 60 | return emptyMap() 61 | } 62 | } 63 | 64 | private fun getCarrierNameBySubId(context: Context, subId: Int): String { 65 | val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager 66 | ?: return "" 67 | 68 | return try { 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 70 | // Android 10 及以上使用新 API 71 | telephonyManager.getNetworkOperatorName(subId) 72 | } else { 73 | // Android 8-9 使用反射获取运营商名称 74 | val createForSubscriptionId = TelephonyManager::class.java.getMethod( 75 | "createForSubscriptionId", 76 | Int::class.javaPrimitiveType 77 | ) 78 | val subTelephonyManager = createForSubscriptionId.invoke(telephonyManager, subId) as TelephonyManager 79 | subTelephonyManager.networkOperatorName 80 | } 81 | } catch (e: Exception) { 82 | // 如果获取失败,回退到默认的 TelephonyManager 83 | telephonyManager.networkOperatorName 84 | } 85 | } 86 | 87 | fun setCarrierConfig(subId: Int, countryCode: String?, carrierName: String? = null) { 88 | val bundle = PersistableBundle() 89 | 90 | // 设置国家码 91 | if (!countryCode.isNullOrEmpty() && countryCode.length == 2) { 92 | bundle.putString( 93 | CarrierConfigManager.KEY_SIM_COUNTRY_ISO_OVERRIDE_STRING, 94 | countryCode.lowercase() 95 | ) 96 | } 97 | 98 | // 设置运营商名称 99 | if (!carrierName.isNullOrEmpty()) { 100 | bundle.putBoolean(CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL, true) 101 | bundle.putString(CarrierConfigManager.KEY_CARRIER_NAME_STRING, carrierName) 102 | } 103 | 104 | overrideCarrierConfig(subId, bundle) 105 | } 106 | 107 | fun resetCarrierConfig(subId: Int) { 108 | overrideCarrierConfig(subId, null) 109 | } 110 | 111 | private fun overrideCarrierConfig(subId: Int, bundle: PersistableBundle?) { 112 | val carrierConfigLoader = ICarrierConfigLoader.Stub.asInterface( 113 | ShizukuBinderWrapper( 114 | TelephonyFrameworkInitializer 115 | .getTelephonyServiceManager() 116 | .carrierConfigServiceRegisterer 117 | .get() 118 | ) 119 | ) 120 | carrierConfigLoader.overrideConfig(subId, bundle, true) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/model/SimCardInfo.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.model 2 | 3 | data class SimCardInfo( 4 | val slot: Int, 5 | val subId: Int, 6 | val carrierName: String, 7 | val currentConfig: Map = emptyMap() 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/screens/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.screens 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.ArrowBack 11 | import androidx.compose.material3.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import com.github.nrfr.R 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun AboutScreen(onBack: () -> Unit) { 24 | val context = LocalContext.current 25 | 26 | Scaffold( 27 | topBar = { 28 | CenterAlignedTopAppBar( 29 | title = { 30 | Row( 31 | verticalAlignment = Alignment.CenterVertically, 32 | horizontalArrangement = Arrangement.Center 33 | ) { 34 | Icon( 35 | painter = painterResource(id = R.drawable.ic_launcher_foreground), 36 | modifier = Modifier.size(48.dp), 37 | contentDescription = "App Icon", 38 | tint = MaterialTheme.colorScheme.primary 39 | ) 40 | Spacer(modifier = Modifier.width(4.dp)) 41 | Text("关于") 42 | } 43 | }, 44 | navigationIcon = { 45 | IconButton(onClick = onBack) { 46 | Icon(Icons.Default.ArrowBack, contentDescription = "返回") 47 | } 48 | } 49 | ) 50 | } 51 | ) { innerPadding -> 52 | Column( 53 | modifier = Modifier 54 | .fillMaxSize() 55 | .padding(innerPadding) 56 | ) { 57 | Column( 58 | modifier = Modifier 59 | .verticalScroll(rememberScrollState()) 60 | .padding(16.dp) 61 | .weight(1f, fill = false), 62 | horizontalAlignment = Alignment.CenterHorizontally, 63 | verticalArrangement = Arrangement.spacedBy(16.dp) 64 | ) { 65 | // 应用信息 66 | Card( 67 | modifier = Modifier.fillMaxWidth() 68 | ) { 69 | Column( 70 | modifier = Modifier.padding(16.dp), 71 | verticalArrangement = Arrangement.spacedBy(8.dp) 72 | ) { 73 | Text( 74 | "功能介绍", 75 | style = MaterialTheme.typography.titleMedium 76 | ) 77 | Text( 78 | "• 修改 SIM 卡的国家码配置,可用于解除部分应用的地区限制\n" + 79 | "• 帮助使用海外 SIM 卡时获得更好的本地化体验\n" + 80 | "• 解决部分应用识别 SIM 卡地区错误的问题\n" + 81 | "• 无需 Root 权限,无需修改系统文件,安全且可随时还原\n" + 82 | "• 支持 Android 8 及以上系统版本\n" + 83 | "• 支持双卡设备,可分别配置不同国家码", 84 | style = MaterialTheme.typography.bodyMedium 85 | ) 86 | } 87 | } 88 | 89 | Divider(modifier = Modifier.padding(vertical = 8.dp)) 90 | 91 | // 作者信息 92 | Text( 93 | "作者信息", 94 | style = MaterialTheme.typography.titleMedium 95 | ) 96 | Card( 97 | modifier = Modifier.fillMaxWidth() 98 | ) { 99 | Column( 100 | modifier = Modifier.padding(16.dp), 101 | verticalArrangement = Arrangement.spacedBy(8.dp) 102 | ) { 103 | Text("作者: Antkites") 104 | Text( 105 | "GitHub: Ackites", 106 | modifier = Modifier.clickable { 107 | context.startActivity( 108 | Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Ackites")) 109 | ) 110 | }, 111 | color = MaterialTheme.colorScheme.primary 112 | ) 113 | Text( 114 | "X (Twitter): @actkites", 115 | modifier = Modifier.clickable { 116 | context.startActivity( 117 | Intent( 118 | Intent.ACTION_VIEW, 119 | Uri.parse("https://x.com/intent/follow?screen_name=actkites") 120 | ) 121 | ) 122 | }, 123 | color = MaterialTheme.colorScheme.primary 124 | ) 125 | } 126 | } 127 | 128 | Divider(modifier = Modifier.padding(vertical = 8.dp)) 129 | 130 | // 开源信息 131 | Text( 132 | "开源信息", 133 | style = MaterialTheme.typography.titleMedium 134 | ) 135 | Card( 136 | modifier = Modifier.fillMaxWidth() 137 | ) { 138 | Column( 139 | modifier = Modifier.padding(16.dp), 140 | verticalArrangement = Arrangement.spacedBy(8.dp) 141 | ) { 142 | Text( 143 | "本项目已在 GitHub 开源", 144 | textAlign = TextAlign.Center, 145 | modifier = Modifier.fillMaxWidth() 146 | ) 147 | Text( 148 | "访问项目主页", 149 | color = MaterialTheme.colorScheme.primary, 150 | textAlign = TextAlign.Center, 151 | modifier = Modifier 152 | .fillMaxWidth() 153 | .clickable { 154 | context.startActivity( 155 | Intent( 156 | Intent.ACTION_VIEW, 157 | Uri.parse("https://github.com/Ackites/Nrfr") 158 | ) 159 | ) 160 | } 161 | ) 162 | } 163 | } 164 | 165 | Spacer(modifier = Modifier.weight(1f)) 166 | 167 | // 版权信息 168 | Text( 169 | "© 2024 Antkites. All rights reserved.", 170 | style = MaterialTheme.typography.bodySmall, 171 | color = MaterialTheme.colorScheme.onSurfaceVariant 172 | ) 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/screens/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.screens 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Info 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalFocusManager 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.text.input.ImeAction 17 | import androidx.compose.ui.text.input.KeyboardType 18 | import androidx.compose.ui.unit.dp 19 | import com.github.nrfr.R 20 | import com.github.nrfr.data.CountryPresets 21 | import com.github.nrfr.data.PresetCarriers 22 | import com.github.nrfr.manager.CarrierConfigManager 23 | import com.github.nrfr.model.SimCardInfo 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun MainScreen(onShowAbout: () -> Unit) { 28 | val context = LocalContext.current 29 | var selectedSimCard by remember { mutableStateOf(null) } 30 | var selectedCountryCode by remember { mutableStateOf("") } 31 | var customCountryCode by remember { mutableStateOf("") } 32 | var isCustomCountryCode by remember { mutableStateOf(false) } 33 | var selectedCarrier by remember { mutableStateOf(null) } 34 | var customCarrierName by remember { mutableStateOf("") } 35 | var isSimCardMenuExpanded by remember { mutableStateOf(false) } 36 | var isCountryCodeMenuExpanded by remember { mutableStateOf(false) } 37 | var isCarrierMenuExpanded by remember { mutableStateOf(false) } 38 | var refreshTrigger by remember { mutableStateOf(0) } 39 | 40 | // 获取实际的 SIM 卡信息 41 | val simCards = remember(context, refreshTrigger) { CarrierConfigManager.getSimCards(context) } 42 | 43 | // 当 simCards 更新时,更新选中的 SIM 卡信息 44 | LaunchedEffect(simCards, selectedSimCard) { 45 | if (selectedSimCard != null) { 46 | selectedSimCard = simCards.find { it.slot == selectedSimCard?.slot } 47 | } 48 | } 49 | 50 | Scaffold( 51 | modifier = Modifier.fillMaxSize(), 52 | topBar = { 53 | CenterAlignedTopAppBar( 54 | title = { 55 | Row( 56 | verticalAlignment = Alignment.CenterVertically, 57 | horizontalArrangement = Arrangement.Center 58 | ) { 59 | Icon( 60 | painter = painterResource(id = R.drawable.ic_launcher_foreground), 61 | modifier = Modifier.size(48.dp), 62 | contentDescription = "App Icon", 63 | tint = MaterialTheme.colorScheme.primary 64 | ) 65 | Spacer(modifier = Modifier.width(4.dp)) 66 | Text("Nrfr") 67 | } 68 | }, 69 | actions = { 70 | IconButton(onClick = onShowAbout) { 71 | Icon(Icons.Default.Info, contentDescription = "关于") 72 | } 73 | } 74 | ) 75 | } 76 | ) { innerPadding -> 77 | Column( 78 | modifier = Modifier 79 | .fillMaxSize() 80 | .padding(innerPadding) 81 | .padding(16.dp), 82 | horizontalAlignment = Alignment.CenterHorizontally, 83 | verticalArrangement = Arrangement.spacedBy(16.dp) 84 | ) { 85 | // SIM卡选择 86 | SimCardSelector( 87 | simCards = simCards, 88 | selectedSimCard = selectedSimCard, 89 | isExpanded = isSimCardMenuExpanded, 90 | onExpandedChange = { isSimCardMenuExpanded = it }, 91 | onSimCardSelected = { selectedSimCard = it } 92 | ) 93 | 94 | // 显示当前选中的 SIM 卡的配置信息 95 | selectedSimCard?.let { simCard -> 96 | CurrentConfigCard(simCard = simCard) 97 | } 98 | 99 | // 国家码选择 100 | CountryCodeSelector( 101 | selectedCountryCode = selectedCountryCode, 102 | isCustomCountryCode = isCustomCountryCode, 103 | customCountryCode = customCountryCode, 104 | isExpanded = isCountryCodeMenuExpanded, 105 | onExpandedChange = { isCountryCodeMenuExpanded = it }, 106 | onCountryCodeSelected = { code -> 107 | selectedCountryCode = code 108 | isCustomCountryCode = false 109 | }, 110 | onCustomSelected = { 111 | isCustomCountryCode = true 112 | selectedCountryCode = customCountryCode 113 | } 114 | ) 115 | 116 | // 自定义国家码输入框 117 | if (isCustomCountryCode) { 118 | CustomCountryCodeInput( 119 | value = customCountryCode, 120 | onValueChange = { 121 | if (it.length <= 2 && it.all { char -> char.isLetter() }) { 122 | customCountryCode = it.uppercase() 123 | selectedCountryCode = it.uppercase() 124 | } 125 | } 126 | ) 127 | } 128 | 129 | // 运营商选择 130 | CarrierSelector( 131 | selectedCarrier = selectedCarrier, 132 | isExpanded = isCarrierMenuExpanded, 133 | onExpandedChange = { isCarrierMenuExpanded = it }, 134 | onCarrierSelected = { carrier -> 135 | selectedCarrier = carrier 136 | customCarrierName = carrier.displayName 137 | } 138 | ) 139 | 140 | // 自定义运营商名称输入框 141 | if (selectedCarrier?.name == "自定义") { 142 | CustomCarrierNameInput( 143 | value = customCarrierName, 144 | onValueChange = { customCarrierName = it } 145 | ) 146 | } 147 | 148 | Spacer(modifier = Modifier.weight(1f)) 149 | 150 | // 按钮行 151 | ActionButtons( 152 | selectedSimCard = selectedSimCard, 153 | selectedCountryCode = selectedCountryCode, 154 | isCustomCountryCode = isCustomCountryCode, 155 | customCountryCode = customCountryCode, 156 | selectedCarrier = selectedCarrier, 157 | customCarrierName = customCarrierName, 158 | onReset = { 159 | try { 160 | CarrierConfigManager.resetCarrierConfig(it.subId) 161 | Toast.makeText(context, "设置已还原", Toast.LENGTH_SHORT).show() 162 | refreshTrigger += 1 163 | selectedCountryCode = "" 164 | selectedCarrier = null 165 | customCarrierName = "" 166 | } catch (e: Exception) { 167 | Toast.makeText(context, "还原失败: ${e.message}", Toast.LENGTH_SHORT).show() 168 | } 169 | }, 170 | onSave = { simCard -> 171 | try { 172 | val carrierName = if (selectedCarrier?.name == "自定义") { 173 | customCarrierName.takeIf { it.isNotEmpty() } 174 | } else { 175 | selectedCarrier?.displayName 176 | } 177 | val countryCode = if (isCustomCountryCode) { 178 | customCountryCode.takeIf { it.length == 2 } 179 | } else { 180 | selectedCountryCode 181 | } 182 | CarrierConfigManager.setCarrierConfig( 183 | simCard.subId, 184 | countryCode, 185 | carrierName 186 | ) 187 | Toast.makeText(context, "设置已保存", Toast.LENGTH_SHORT).show() 188 | refreshTrigger += 1 189 | } catch (e: Exception) { 190 | Toast.makeText(context, "保存失败: ${e.message}", Toast.LENGTH_SHORT).show() 191 | } 192 | } 193 | ) 194 | } 195 | } 196 | } 197 | 198 | @OptIn(ExperimentalMaterial3Api::class) 199 | @Composable 200 | private fun SimCardSelector( 201 | simCards: List, 202 | selectedSimCard: SimCardInfo?, 203 | isExpanded: Boolean, 204 | onExpandedChange: (Boolean) -> Unit, 205 | onSimCardSelected: (SimCardInfo) -> Unit 206 | ) { 207 | ExposedDropdownMenuBox( 208 | expanded = isExpanded, 209 | onExpandedChange = onExpandedChange 210 | ) { 211 | OutlinedTextField( 212 | value = selectedSimCard?.let { "SIM ${it.slot} (${it.carrierName})" } ?: "", 213 | onValueChange = {}, 214 | readOnly = true, 215 | label = { Text("选择SIM卡") }, 216 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }, 217 | modifier = Modifier 218 | .fillMaxWidth() 219 | .menuAnchor() 220 | ) 221 | ExposedDropdownMenu( 222 | expanded = isExpanded, 223 | onDismissRequest = { onExpandedChange(false) } 224 | ) { 225 | simCards.forEach { simCard -> 226 | DropdownMenuItem( 227 | text = { 228 | Column { 229 | Text("SIM ${simCard.slot} (${simCard.carrierName})") 230 | if (simCard.currentConfig.isEmpty()) { 231 | Text( 232 | "无覆盖配置", 233 | style = MaterialTheme.typography.bodySmall, 234 | color = MaterialTheme.colorScheme.onSurfaceVariant 235 | ) 236 | } else { 237 | simCard.currentConfig.forEach { (key, value) -> 238 | Text( 239 | "$key: $value", 240 | style = MaterialTheme.typography.bodySmall, 241 | color = MaterialTheme.colorScheme.onSurfaceVariant 242 | ) 243 | } 244 | } 245 | } 246 | }, 247 | onClick = { 248 | onSimCardSelected(simCard) 249 | onExpandedChange(false) 250 | } 251 | ) 252 | } 253 | } 254 | } 255 | } 256 | 257 | @Composable 258 | private fun CurrentConfigCard(simCard: SimCardInfo) { 259 | Card( 260 | modifier = Modifier 261 | .fillMaxWidth() 262 | .padding(vertical = 8.dp) 263 | ) { 264 | Column( 265 | modifier = Modifier 266 | .fillMaxWidth() 267 | .padding(16.dp) 268 | ) { 269 | Text( 270 | "当前配置", 271 | style = MaterialTheme.typography.titleMedium 272 | ) 273 | Spacer(modifier = Modifier.height(8.dp)) 274 | if (simCard.currentConfig.isEmpty()) { 275 | Text( 276 | "无覆盖配置", 277 | style = MaterialTheme.typography.bodyMedium, 278 | color = MaterialTheme.colorScheme.onSurfaceVariant 279 | ) 280 | } else { 281 | simCard.currentConfig.forEach { (key, value) -> 282 | Text( 283 | "$key: $value", 284 | style = MaterialTheme.typography.bodyMedium 285 | ) 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | @OptIn(ExperimentalMaterial3Api::class) 293 | @Composable 294 | private fun CountryCodeSelector( 295 | selectedCountryCode: String, 296 | isCustomCountryCode: Boolean, 297 | customCountryCode: String, 298 | isExpanded: Boolean, 299 | onExpandedChange: (Boolean) -> Unit, 300 | onCountryCodeSelected: (String) -> Unit, 301 | onCustomSelected: () -> Unit 302 | ) { 303 | ExposedDropdownMenuBox( 304 | expanded = isExpanded, 305 | onExpandedChange = onExpandedChange 306 | ) { 307 | OutlinedTextField( 308 | value = when { 309 | isCustomCountryCode -> "自定义" 310 | selectedCountryCode.isEmpty() -> "" 311 | else -> CountryPresets.countries.find { it.code == selectedCountryCode } 312 | ?.let { "${it.name} (${it.code})" } 313 | ?: selectedCountryCode 314 | }, 315 | onValueChange = {}, 316 | readOnly = true, 317 | label = { Text("选择国家码") }, 318 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }, 319 | modifier = Modifier 320 | .fillMaxWidth() 321 | .menuAnchor() 322 | ) 323 | ExposedDropdownMenu( 324 | expanded = isExpanded, 325 | onDismissRequest = { onExpandedChange(false) } 326 | ) { 327 | // 预设国家码列表 328 | CountryPresets.countries.forEach { countryInfo -> 329 | DropdownMenuItem( 330 | text = { Text("${countryInfo.name} (${countryInfo.code})") }, 331 | onClick = { 332 | onCountryCodeSelected(countryInfo.code) 333 | onExpandedChange(false) 334 | } 335 | ) 336 | } 337 | // 自定义选项 338 | DropdownMenuItem( 339 | text = { Text("自定义") }, 340 | onClick = { 341 | onCustomSelected() 342 | onExpandedChange(false) 343 | } 344 | ) 345 | } 346 | } 347 | } 348 | 349 | @OptIn(ExperimentalMaterial3Api::class) 350 | @Composable 351 | private fun CustomCountryCodeInput( 352 | value: String, 353 | onValueChange: (String) -> Unit 354 | ) { 355 | val focusManager = LocalFocusManager.current 356 | OutlinedTextField( 357 | value = value, 358 | onValueChange = onValueChange, 359 | label = { Text("自定义国家码 (2位字母)") }, 360 | keyboardOptions = KeyboardOptions( 361 | keyboardType = KeyboardType.Text, 362 | imeAction = ImeAction.Done 363 | ), 364 | keyboardActions = KeyboardActions( 365 | onDone = { 366 | focusManager.clearFocus() 367 | } 368 | ), 369 | singleLine = true, 370 | modifier = Modifier.fillMaxWidth() 371 | ) 372 | } 373 | 374 | @OptIn(ExperimentalMaterial3Api::class) 375 | @Composable 376 | private fun CarrierSelector( 377 | selectedCarrier: PresetCarriers.CarrierPreset?, 378 | isExpanded: Boolean, 379 | onExpandedChange: (Boolean) -> Unit, 380 | onCarrierSelected: (PresetCarriers.CarrierPreset) -> Unit 381 | ) { 382 | ExposedDropdownMenuBox( 383 | expanded = isExpanded, 384 | onExpandedChange = onExpandedChange 385 | ) { 386 | OutlinedTextField( 387 | value = selectedCarrier?.name ?: "", 388 | onValueChange = {}, 389 | readOnly = true, 390 | label = { Text("选择运营商") }, 391 | trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }, 392 | modifier = Modifier 393 | .fillMaxWidth() 394 | .menuAnchor() 395 | ) 396 | ExposedDropdownMenu( 397 | expanded = isExpanded, 398 | onDismissRequest = { onExpandedChange(false) } 399 | ) { 400 | // 分组显示运营商 401 | PresetCarriers.presets 402 | .groupBy { it.region } 403 | .forEach { (region, carriers) -> 404 | if (region.isNotEmpty()) { 405 | val regionName = CountryPresets.countries.find { it.code == region }?.name ?: region 406 | Text( 407 | regionName, 408 | style = MaterialTheme.typography.titleSmall, 409 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), 410 | color = MaterialTheme.colorScheme.primary 411 | ) 412 | carriers.forEach { carrier -> 413 | DropdownMenuItem( 414 | text = { Text(carrier.name) }, 415 | onClick = { 416 | onCarrierSelected(carrier) 417 | onExpandedChange(false) 418 | } 419 | ) 420 | } 421 | Divider(modifier = Modifier.padding(vertical = 4.dp)) 422 | } 423 | } 424 | 425 | // 自定义选项 426 | PresetCarriers.presets 427 | .filter { it.region.isEmpty() } 428 | .forEach { carrier -> 429 | DropdownMenuItem( 430 | text = { Text(carrier.name) }, 431 | onClick = { 432 | onCarrierSelected(carrier) 433 | onExpandedChange(false) 434 | } 435 | ) 436 | } 437 | } 438 | } 439 | } 440 | 441 | @OptIn(ExperimentalMaterial3Api::class) 442 | @Composable 443 | private fun CustomCarrierNameInput( 444 | value: String, 445 | onValueChange: (String) -> Unit 446 | ) { 447 | OutlinedTextField( 448 | value = value, 449 | onValueChange = onValueChange, 450 | label = { Text("自定义运营商名称") }, 451 | modifier = Modifier.fillMaxWidth() 452 | ) 453 | } 454 | 455 | @Composable 456 | private fun ActionButtons( 457 | selectedSimCard: SimCardInfo?, 458 | selectedCountryCode: String, 459 | isCustomCountryCode: Boolean, 460 | customCountryCode: String, 461 | selectedCarrier: PresetCarriers.CarrierPreset?, 462 | customCarrierName: String, 463 | onReset: (SimCardInfo) -> Unit, 464 | onSave: (SimCardInfo) -> Unit 465 | ) { 466 | Row( 467 | modifier = Modifier.fillMaxWidth(), 468 | horizontalArrangement = Arrangement.spacedBy(16.dp) 469 | ) { 470 | // 还原按钮 471 | OutlinedButton( 472 | onClick = { selectedSimCard?.let(onReset) }, 473 | modifier = Modifier.weight(1f), 474 | enabled = selectedSimCard != null 475 | ) { 476 | Text("还原设置") 477 | } 478 | 479 | // 保存按钮 480 | Button( 481 | onClick = { selectedSimCard?.let(onSave) }, 482 | modifier = Modifier.weight(1f), 483 | enabled = selectedSimCard != null && ( 484 | (isCustomCountryCode && customCountryCode.length == 2) || 485 | (!isCustomCountryCode && selectedCountryCode.isNotEmpty()) || 486 | (selectedCarrier != null && (selectedCarrier.name != "自定义" || customCarrierName.isNotEmpty())) 487 | ) 488 | ) { 489 | Text("保存生效") 490 | } 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/screens/ShizukuNotReadyScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.screens 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun ShizukuNotReadyScreen() { 13 | Column( 14 | modifier = Modifier 15 | .fillMaxSize() 16 | .padding(16.dp), 17 | horizontalAlignment = Alignment.CenterHorizontally, 18 | verticalArrangement = Arrangement.Center 19 | ) { 20 | Text( 21 | text = "需要 Shizuku 权限", 22 | style = MaterialTheme.typography.headlineMedium 23 | ) 24 | Spacer(modifier = Modifier.height(8.dp)) 25 | Text( 26 | text = "请安装并启用 Shizuku,然后重启应用", 27 | style = MaterialTheme.typography.bodyLarge 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | primary = Purple80, 16 | secondary = PurpleGrey80, 17 | tertiary = Pink80 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = Purple40, 22 | secondary = PurpleGrey40, 23 | tertiary = Pink40 24 | 25 | /* Other default colors to override 26 | background = Color(0xFFFFFBFE), 27 | surface = Color(0xFFFFFBFE), 28 | onPrimary = Color.White, 29 | onSecondary = Color.White, 30 | onTertiary = Color.White, 31 | onBackground = Color(0xFF1C1B1F), 32 | onSurface = Color(0xFF1C1B1F), 33 | */ 34 | ) 35 | 36 | @Composable 37 | fun NrfrTheme( 38 | darkTheme: Boolean = isSystemInDarkTheme(), 39 | // Dynamic color is available on Android 12+ 40 | dynamicColor: Boolean = true, 41 | content: @Composable () -> Unit 42 | ) { 43 | val colorScheme = when { 44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 | val context = LocalContext.current 46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 | } 48 | 49 | darkTheme -> DarkColorScheme 50 | else -> LightColorScheme 51 | } 52 | 53 | MaterialTheme( 54 | colorScheme = colorScheme, 55 | typography = Typography, 56 | content = content 57 | ) 58 | } 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/nrfr/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.nrfr.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ackites/Nrfr/fd847a7a1fb8d61e3f281d769699369f8030b949/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Nrfr 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |