├── .gitignore ├── README.md ├── README ├── Client.png ├── Desktop.png ├── Home Panel.png ├── Network-openvpn.png ├── SMSConversation.png ├── Store-V2rayNG.png ├── Store.png ├── System - Firewall.png ├── System Log.png ├── System-DHCP.png ├── Vector.svg ├── mobile_ui.png └── qrcode.png ├── README_zh.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── jtun │ │ └── router │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── dist │ │ │ ├── assets │ │ │ ├── Arrow-left-CEOw6kwz.png │ │ │ ├── CellularNetwork-DH9CSQGs.js │ │ │ ├── CellularNetwork-DzpDQzog.css │ │ │ ├── ClientPage-CDamWFQt.js │ │ │ ├── ClientPage-DWNn8j5L.css │ │ │ ├── DhcpPage-CF9nq6pc.css │ │ │ ├── DhcpPage-CTRKn6NC.js │ │ │ ├── DmzPage-CNtc58Jm.js │ │ │ ├── DmzPage-DqtgYYIp.css │ │ │ ├── DynamicDns-CIr5ouYM.js │ │ │ ├── DynamicDns-DGrQtc41.css │ │ │ ├── FireWall-CF_elBY-.css │ │ │ ├── FireWall-CLzkzUe3.js │ │ │ ├── GuidePage-BDnRZDmI.js │ │ │ ├── GuidePage-DtFLNpBe.css │ │ │ ├── HomePage--Qi1UdVP.css │ │ │ ├── HomePage-BIQaVfGs.js │ │ │ ├── InternetPage-D6_HBGjo.css │ │ │ ├── InternetPage-Se2pY2p0.js │ │ │ ├── IpAdress-CrU3rG-5.js │ │ │ ├── IpAdress-DYgnPOD6.css │ │ │ ├── LoginPage-CnIt_MLT.css │ │ │ ├── LoginPage-DvEnjBYp.js │ │ │ ├── NoSim-C9MXz0Rx.png │ │ │ ├── OpenVpn-D0w3Syxv.js │ │ │ ├── OpenVpn-iE4Nfc2d.css │ │ │ ├── RedirectPage-D7g5kMM0.css │ │ │ ├── RedirectPage-Dhqzv7YE.js │ │ │ ├── SmsConversation-DsIFA05Q.css │ │ │ ├── SmsConversation-Dwsiv29K.js │ │ │ ├── StorePage-CNLTA0EK.js │ │ │ ├── StorePage-DJhyQBmM.css │ │ │ ├── SystemLog-Cy2qRY8Y.css │ │ │ ├── SystemLog-gixzKMAI.js │ │ │ ├── UpgradePage-1g74trDI.css │ │ │ ├── UpgradePage-B0bja64v.js │ │ │ ├── UpnpPage-BxUkYuxd.js │ │ │ ├── UpnpPage-WcLQUmIC.css │ │ │ ├── VirtualServer-1Lme7Siz.css │ │ │ ├── VirtualServer-CmJ02WK5.js │ │ │ ├── WireLess-BS4_8dMd.css │ │ │ ├── WireLess-DDainhuz.js │ │ │ ├── aria-BUADUvnR.js │ │ │ ├── castArray-CsLOnyYC.js │ │ │ ├── constants-pKhjEyna.js │ │ │ ├── directive-4ZKEvCYR.js │ │ │ ├── domain-BfMyvRvT.css │ │ │ ├── domain-CVcwQrJe.js │ │ │ ├── el-button-FQWvR2f2.js │ │ │ ├── el-button-IKbc-kyC.css │ │ │ ├── el-card-BJ3sbP9B.css │ │ │ ├── el-card-fgCriugF.js │ │ │ ├── el-checkbox-BFZaSjz7.css │ │ │ ├── el-checkbox-o8_3OQ03.js │ │ │ ├── el-col-BP4dtlli.css │ │ │ ├── el-col-BkR5g-BW.js │ │ │ ├── el-dialog-C8XF2JDe.js │ │ │ ├── el-dialog-DMam3H11.css │ │ │ ├── el-input-CUUfrk5p.js │ │ │ ├── el-input-DbH0jb8o.css │ │ │ ├── el-loading-x7H6yciF.css │ │ │ ├── el-overlay-89ZHhflZ.js │ │ │ ├── el-overlay-DBZ27Rv4.css │ │ │ ├── el-popper-DEnoFQL7.js │ │ │ ├── el-popper-DL6Na3xw.css │ │ │ ├── el-select-BdLPv8lu.js │ │ │ ├── el-select-vzW36nso.css │ │ │ ├── el-switch-BTV1w9Lp.js │ │ │ ├── el-switch-BhrVVMax.css │ │ │ ├── icon-collect-BCoEM0qf.svg │ │ │ ├── iconfont-zYHu7zwx.ttf │ │ │ ├── index-82_0c4AB.js │ │ │ ├── index-APFVxEOi.js │ │ │ ├── index-Bfki-cik.js │ │ │ ├── index-CCLY6QsW.js │ │ │ ├── index-CPZq7CKI.css │ │ │ ├── index-CSuQBRO8.js │ │ │ ├── index-DmH9s9cX.js │ │ │ ├── index-DuTyNaIr.js │ │ │ ├── index-r5W6hzzQ.js │ │ │ ├── index-vNULV4St.js │ │ │ ├── index-z4wGpwji.js │ │ │ ├── isEqual-CxmsCrjH.js │ │ │ ├── loginbg-CPP8WZVm.png │ │ │ ├── logoImg-CwUB_hdd.png │ │ │ ├── openDownLoad-Cl2XZ_8t.png │ │ │ ├── openDownLoad-DhbK9y2K.js │ │ │ ├── request-kOUjDz9H.js │ │ │ ├── selectBox-03onemMs.js │ │ │ ├── selectBox-CcQTmTjQ.css │ │ │ ├── submiting-B7ULOMtL.gif │ │ │ ├── typescript-Bp3YSIOJ.js │ │ │ └── use-form-item-BXSSCJ3O.js │ │ │ ├── favicon.ico │ │ │ ├── fonts │ │ │ ├── iconfont-Cgxpgk-X.woff2 │ │ │ └── iconfont-D0JD_TNW.woff │ │ │ └── index.html │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── root.cpp │ ├── java │ │ └── com │ │ │ └── jtun │ │ │ └── router │ │ │ ├── App.kt │ │ │ ├── BootReceiver.kt │ │ │ ├── IpNeighbourMonitoringService.kt │ │ │ ├── MainActivity.kt │ │ │ ├── RepeaterService.kt │ │ │ ├── RoutingManager.kt │ │ │ ├── ServiceNotification.kt │ │ │ ├── TetheringService.kt │ │ │ ├── bus │ │ │ ├── RxBus.java │ │ │ ├── RxBusSubscriber.java │ │ │ └── RxSubscriptions.java │ │ │ ├── callback │ │ │ └── AppNetworkCallback.kt │ │ │ ├── config │ │ │ ├── CMD.kt │ │ │ ├── Config.kt │ │ │ └── HttpInterface.kt │ │ │ ├── control │ │ │ ├── AppStoreControl.kt │ │ │ ├── SoftApControl.kt │ │ │ ├── UpdateControl.kt │ │ │ └── WifiApControl.kt │ │ │ ├── dialog │ │ │ ├── ConfirmDialog.java │ │ │ └── OnConfirmListener.java │ │ │ ├── http │ │ │ ├── ApConfig.java │ │ │ ├── ApiService.kt │ │ │ ├── HttpServer.kt │ │ │ ├── HttpWebServer.kt │ │ │ ├── NetworkUtil.java │ │ │ ├── download │ │ │ │ ├── DownLoadStateBean.java │ │ │ │ ├── DownLoadSubscriber.java │ │ │ │ ├── DownloadApkCallBack.java │ │ │ │ ├── ProgressCallBack.java │ │ │ │ └── ProgressResponseBody.java │ │ │ ├── interceptor │ │ │ │ ├── BaseInterceptor.java │ │ │ │ ├── CacheInterceptor.java │ │ │ │ ├── ProgressInterceptor.java │ │ │ │ └── logging │ │ │ │ │ ├── I.java │ │ │ │ │ ├── Level.java │ │ │ │ │ ├── Logger.java │ │ │ │ │ ├── LoggingInterceptor.java │ │ │ │ │ └── Printer.java │ │ │ ├── request │ │ │ │ └── BaseRequest.kt │ │ │ └── response │ │ │ │ ├── BaseResponse.kt │ │ │ │ ├── DeviceInfo.java │ │ │ │ ├── InternetInfo.java │ │ │ │ ├── NetSpeed.kt │ │ │ │ ├── SocketDeviceInfo.java │ │ │ │ ├── UsedDataInfo.java │ │ │ │ ├── frp │ │ │ │ └── FrpConfig.kt │ │ │ │ ├── openvpn │ │ │ │ ├── ItemServer.kt │ │ │ │ └── OpenvpnItem.kt │ │ │ │ └── v2ray │ │ │ │ ├── Configs.kt │ │ │ │ ├── EConfigType.kt │ │ │ │ ├── ProfileItem.kt │ │ │ │ └── ServersCache.kt │ │ │ ├── net │ │ │ ├── DhcpWorkaround.kt │ │ │ ├── InetAddressComparator.kt │ │ │ ├── IpNeighbour.kt │ │ │ ├── MacAddressCompat.kt │ │ │ ├── RemoveUidInterfaceRuleCommand.kt │ │ │ ├── Routing.kt │ │ │ ├── TetherOffloadManager.kt │ │ │ ├── TetherType.kt │ │ │ ├── TetheringManager.kt │ │ │ ├── VpnFirewallManager.kt │ │ │ ├── dns │ │ │ │ ├── DnsForwarder.kt │ │ │ │ ├── DnsResolverCompat.kt │ │ │ │ └── VpnProtectedSelectorManager.kt │ │ │ ├── monitor │ │ │ │ ├── DefaultNetworkMonitor.kt │ │ │ │ ├── FallbackUpstreamMonitor.kt │ │ │ │ ├── InterfaceMonitor.kt │ │ │ │ ├── IpMonitor.kt │ │ │ │ ├── IpNeighbourMonitor.kt │ │ │ │ ├── TetherTimeoutMonitor.kt │ │ │ │ ├── TrafficRecorder.kt │ │ │ │ ├── UpstreamMonitor.kt │ │ │ │ └── VpnMonitor.kt │ │ │ └── wifi │ │ │ │ ├── P2pSupplicantConfiguration.kt │ │ │ │ ├── SoftApCapability.kt │ │ │ │ ├── SoftApConfigurationCompat.kt │ │ │ │ ├── SoftApInfo.kt │ │ │ │ ├── VendorElements.kt │ │ │ │ ├── WifiApManager.kt │ │ │ │ ├── WifiClient.kt │ │ │ │ ├── WifiDoubleLock.kt │ │ │ │ ├── WifiP2pManagerHelper.kt │ │ │ │ └── WifiSsidCompat.kt │ │ │ ├── receiver │ │ │ └── BootCompletedReceiver.kt │ │ │ ├── room │ │ │ ├── AppDatabase.kt │ │ │ ├── AppInfo.kt │ │ │ ├── ClientConnected.kt │ │ │ ├── ClientDao.kt │ │ │ ├── ClientRecord.kt │ │ │ ├── Converters.kt │ │ │ ├── Log.kt │ │ │ └── TrafficRecord.kt │ │ │ ├── root │ │ │ ├── Jni.kt │ │ │ ├── LocalOnlyHotspotCallbacks.kt │ │ │ ├── MiscCommands.kt │ │ │ ├── RepeaterCommands.kt │ │ │ ├── RootManager.kt │ │ │ ├── RoutingCommands.kt │ │ │ ├── TetheringCommands.kt │ │ │ └── WifiApCommands.kt │ │ │ ├── sms │ │ │ ├── SMSConversation.kt │ │ │ ├── Sms.kt │ │ │ └── SmsManager.java │ │ │ ├── socket │ │ │ └── SocketIO.kt │ │ │ ├── tasker │ │ │ ├── StateAction.kt │ │ │ ├── TaskerEvents.kt │ │ │ ├── TaskerPermissionManager.kt │ │ │ ├── TetheringActions.kt │ │ │ └── TetheringState.kt │ │ │ ├── util │ │ │ ├── ApkUtils.java │ │ │ ├── AppUpdate.kt │ │ │ ├── ConstantLookup.kt │ │ │ ├── CustomTabsUrlSpan.kt │ │ │ ├── DeviceStorageApp.kt │ │ │ ├── DeviceUtil.kt │ │ │ ├── Events.kt │ │ │ ├── FileHelper.kt │ │ │ ├── JLog.kt │ │ │ ├── KLog.java │ │ │ ├── KillableTileService.kt │ │ │ ├── NetworkUtils.java │ │ │ ├── RangeInput.kt │ │ │ ├── RootSession.kt │ │ │ ├── SelfDismissActivity.kt │ │ │ ├── ServiceForegroundConnector.kt │ │ │ ├── Services.kt │ │ │ ├── SmsWriteOpUtil.java │ │ │ ├── SystemCtrlUtil.java │ │ │ ├── TetheringUtil.kt │ │ │ ├── ToastUtils.java │ │ │ ├── UnblockCentral.kt │ │ │ └── Utils.kt │ │ │ └── widget │ │ │ ├── AdvancedWebView.java │ │ │ ├── BaseWebListener.kt │ │ │ └── SmartSnackbar.kt │ └── res │ │ ├── drawable │ │ ├── btn_close.xml │ │ ├── btn_save.xml │ │ ├── ic_action_settings_ethernet.xml │ │ ├── ic_action_settings_input_antenna.xml │ │ ├── ic_content_inbox.xml │ │ ├── ic_device_bluetooth.xml │ │ ├── ic_device_network_wifi.xml │ │ ├── ic_device_usb.xml │ │ ├── ic_device_wifi_tethering.xml │ │ ├── ic_image_flash_on.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_quick_settings_tile_on.xml │ │ ├── icon_client.xml │ │ ├── icon_connect.xml │ │ ├── icon_download.xml │ │ ├── icon_logo.xml │ │ ├── icon_reboot.xml │ │ ├── icon_upload.xml │ │ ├── icon_wifi.xml │ │ ├── layout_item_index_page.xml │ │ └── layout_main_item.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── dialog_confirm_program.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── icon_close.png │ │ ├── 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 │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── style.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── jtun │ └── router │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | #Android 17 | proguardMapping.txt 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | ServerConfig.kt 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Log/OS Files 26 | *.log 27 | 28 | # Android Studio generated files and folders 29 | captures/ 30 | .externalNativeBuild/ 31 | .cxx/ 32 | *.apk 33 | output.json 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/ 38 | misc.xml 39 | deploymentTargetDropDown.xml 40 | render.experimental.xml 41 | 42 | # Keystore files 43 | *.jks 44 | *.keystore 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Android Profiling 50 | *.hprof -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Language 2 | 3 | - [English](./README.md) 4 | - [中文](./README_zh.md) 5 | 6 | # Jtun Router 7 | 8 | Vector 9 | 10 | ## Description 11 | 12 | The goal of Jtun Router is to provide a network management application for Android devices that is easy to use by everyone. Whether as a mobile hotspot or a home gateway, Jtun Router offers a simple and user-friendly experience. 13 | 14 | Leveraging the openness and versatility of Android, Jtun Router includes rich network management features, a web-based interface, and an app store. Through the web interface, users can remotely control devices and download third-party apps from the app store to enable their configuration and launch. Jtun Router ensures global connectivity with minimal barriers. 15 | 16 | ## Device Features 17 | 18 | ![mobile ui](./README/mobile_ui.png) 19 | 20 | 1. **APK Store**: Jtun Router includes an app store that integrates common APKs, allowing users to easily manage and operate apps through a straightforward interface. 21 | 2. **Firmware Upgrades**: Upgrading the firmware retains current configurations, eliminating the need to reinstall APKs after updates. 22 | 3. **App Updates**: All APKs integrated into the app store can be updated online to the latest versions. 23 | 24 | Jtun Router simplifies interactions significantly, making it accessible even for users with no technical background. A one-click installation and setup process ensures a seamless experience. 25 | 26 | ## Jtun Router Interface Overview 27 | 28 | ![Desktop - 4](./README/Desktop.png) 29 | 30 | ![Client](./README/Client.png) 31 | 32 | ![Store](./README/Store.png) 33 | 34 | ![Network - openvpn](./README/Network-openvpn.png) 35 | 36 | ![Store - V2rayNG](./README/Store-V2rayNG.png) 37 | 38 | ![System - DHCP](./README/System-DHCP.png) 39 | 40 | ![System Log](./README/System Log.png) 41 | 42 | ![Tools - SMS Conversation](./README/SMSConversation.png) 43 | 44 | ## Supported Hardware 45 | 46 | - **Android Version**: Devices running Android 5.0 and above 47 | - **RAM**: Minimum 1GB 48 | - **ROM**: Minimum 8GB 49 | 50 | ## Compatibility 51 | 52 | The project integrates [VPNHotspot](https://github.com/Mygod/VPNHotspot) to easily connect to your VPN. Share your VPN connection through hotspots or repeaters (root required). 53 | 54 | #### ROM 55 | 56 | For testing and stability, the application runs on a modified system based on [LineageOS 18.1](https://lineageos.org/). 57 | 58 | Online ROM downloads (more devices are being adapted): 59 | - [Mi4](https://drive.google.com/drive/folders/1WAFptXNIyNS3VZezOFzByYu-OOs6JXFj?usp=sharing) 60 | - [Mi5](https://drive.google.com/drive/folders/1PbkI5I_Fz7TFMVUwetMjaiu0UnzVOCKy?usp=sharing) 61 | 62 | ### Installation 63 | 64 | Download the [app-release.apk](https://github.com/jtun-coder/JtunRouting/releases) and install it on your Android device. Launch the app and grant the necessary permissions. 65 | 66 | ## Support 67 | 68 | - Email: [jtun@86.ltd](mailto:jtun@86.ltd) 69 | 70 | ## Join the Group 71 | 72 | qrcode 73 | -------------------------------------------------------------------------------- /README/Client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Client.png -------------------------------------------------------------------------------- /README/Desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Desktop.png -------------------------------------------------------------------------------- /README/Home Panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Home Panel.png -------------------------------------------------------------------------------- /README/Network-openvpn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Network-openvpn.png -------------------------------------------------------------------------------- /README/SMSConversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/SMSConversation.png -------------------------------------------------------------------------------- /README/Store-V2rayNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Store-V2rayNG.png -------------------------------------------------------------------------------- /README/Store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/Store.png -------------------------------------------------------------------------------- /README/System - Firewall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/System - Firewall.png -------------------------------------------------------------------------------- /README/System Log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/System Log.png -------------------------------------------------------------------------------- /README/System-DHCP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/System-DHCP.png -------------------------------------------------------------------------------- /README/Vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README/mobile_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/mobile_ui.png -------------------------------------------------------------------------------- /README/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/README/qrcode.png -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ## Language 2 | 3 | - [English](./README.md) 4 | - [中文](./README_zh.md) 5 | 6 | # Jtun Router 7 | 8 | Vector 9 | 10 | ## 描述 11 | 12 | JtunRouter 目标是提供一个人人可用的安卓设备网络管理的应用,不管是作为移动热点或是家庭网关,都有简单易上手的操作体验。 13 | 14 | 基于Android的的开放性和可玩性,JtunRouter包含丰富的网络管理、WEB界面管理、应用商店等功能。通过WEB界面远程操控设备,并下载应用商店内三方应用,实现对第三方应用的启动和配置功能。以极小的门槛全球互联。 15 | 16 | ## 设备端 17 | 18 | 19 | 20 | ![mobile ui](./README/mobile_ui.png) 21 | 22 | 23 | 24 | 1. Jtun Router 提供了应用商店:APK Store,尽可能的集成常用APK,可以让大家通过简单易上手的界面操作App 25 | 2. Jtun Router 固件升级会保留当前配置,避免升级后需要重新安装APK。 26 | 3. Jtun Router 应用商店集成的APK都可以在线升级到最新版本。 27 | 28 | ##### Jtun Router 做了很多交互向简化,使得更易上手和调试,即便是无基础也可一键安装上手并使用。 29 | 30 | 31 | 32 | ## Jtun Router 界面概览 33 | 34 | ![Desktop - 4](./README/Desktop.png) 35 | 36 | ![Client](./README/Client.png) 37 | 38 | ![Store](./README/Store.png) 39 | 40 | ![Network - openvpn](./README/Network-openvpn.png) 41 | 42 | ![Store - V2rayNG](./README/Store-V2rayNG.png) 43 | 44 | ![System - DHCP](./README/System-DHCP.png) 45 | 46 | ![System Log](./README/System Log.png) 47 | 48 | ![Tools - SMS Conversation](./README/SMSConversation.png) 49 | 50 | ## 支持硬件 51 | 52 | - **Android 版本**: 5.0 以上设备 53 | - **RAM** :最小 1G 54 | - ROM:最小 8G 55 | 56 | ## 兼容性 57 | 58 | 项目集成[VPNHotspot](https://github.com/Mygod/VPNHotspot)轻松连接到您的 VPN。通过热点或中继器共享您的 VPN 连接。 (需要root) 59 | 60 | #### Rom 只读存储器 61 | 62 | 为了便于测试和稳定性,该应用程序在基于[LineageOS 18.1 的](https://lineageos.org/)修改系统上运行。 63 | 64 | 在线Rom下载(项目已集成到系统中,正在适配更多设备...) 65 | 66 | [mi4](https://drive.google.com/drive/folders/1WAFptXNIyNS3VZezOFzByYu-OOs6JXFj?usp=sharing) 67 | 68 | [mi5](https://drive.google.com/drive/folders/1PbkI5I_Fz7TFMVUwetMjaiu0UnzVOCKy?usp=sharing) 69 | 70 | ### 安装 71 | 72 | 下载[app-release.apk](https://github.com/jtun-coder/JtunRouting/releases)并将其安装在您的 Android 设备上,然后启动它并授予必要的权限。 73 | 74 | ## 支持 75 | 76 | 邮箱: [jtun@86.ltd](mailto:jtun@86.ltd) 77 | 78 | ## 加入群组 79 | 80 | qrcode 81 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.jtun.router", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 5, 15 | "versionName": "1.0.05", 16 | "outputFile": "JtunRouter_v1.0.05.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/jtun/router/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router 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.jtun.router", appContext.packageName) 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/Arrow-left-CEOw6kwz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/Arrow-left-CEOw6kwz.png -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/DmzPage-CNtc58Jm.js: -------------------------------------------------------------------------------- 1 | import{_ as w,a4 as z,k as i,a1 as I,o as M,g as V,s as e,x as n,p as d,a as v,Z as f,w as c,S as Z}from"./index-CCLY6QsW.js";import{E as x,a as B}from"./el-col-BkR5g-BW.js";import{E as C}from"./el-input-CUUfrk5p.js";import{E as L}from"./el-switch-BTV1w9Lp.js";import{m as g}from"./index-CSuQBRO8.js";import{E as b}from"./index-DmH9s9cX.js";import{E as m}from"./index-DuTyNaIr.js";import"./index-82_0c4AB.js";import"./typescript-Bp3YSIOJ.js";import"./index-Bfki-cik.js";import"./index-APFVxEOi.js";import"./constants-pKhjEyna.js";import"./use-form-item-BXSSCJ3O.js";import"./request-kOUjDz9H.js";import"./directive-4ZKEvCYR.js";import"./aria-BUADUvnR.js";const k={class:"OnlineClient"},O={class:"OnlineClientTop"},U={class:"OnlineHeader"},T={class:"headerLeft"},N={class:"OnlineBody"},P={class:"SmallText"},R={class:"UpgradeDuring"},$={class:"duringLeft"},A={class:"leftTop"},H={class:"duringRight"},j={class:"IpAddress"},q={class:"OnlineBody"},F={class:"UpgradeDuring"},G={class:""},J={__name:"DmzPage",setup(K){const h=z();let r=i(""),t=i(""),a=i(""),_=i("");I(()=>{r.value=h.globalData.logon_pwd,y(),console.log(a.value,t.value,"222")});const y=async()=>{const o={cmd:"154",logon_pwd:r.value},s=await g(o,"readdmz");console.log(s,"2222"),s.code==200&&(_.value=s.dmz.device_name,a.value=s.dmz.device_ip,t.value=s.dmz.enabled==1),console.log(a.value,t.value)},D=async()=>{const o=b.service({target:"body",text:"Save DMZ settings..."});try{const s={cmd:"155",logon_pwd:r.value,device_name:_.value,device_ip:a.value,enabled:t.value==!0?1:0};(await g(s,"setdmz")).code==200?m.success("Save successfully"):m.error("Save failed")}catch{m.error("Save failed")}finally{o.close()}};return(o,s)=>{const u=L,E=C,p=B,S=x;return M(),V("div",null,[e("div",k,[e("div",O,[e("div",U,[e("div",T,n(o.$t("system.dmz")),1)]),e("div",N,[e("div",P,n(o.$t("system.descE")),1),e("div",R,[e("div",$,[e("div",A,n(o.$t("system.dmzStatus")),1)]),e("div",H,[d(u,{modelValue:v(t),"onUpdate:modelValue":s[0]||(s[0]=l=>f(t)?t.value=l:t=l)},null,8,["modelValue"])])])]),e("div",j,n(o.$t("system.dmzIP")),1),e("div",q,[d(S,{gutter:24},{default:c(()=>[d(p,{xs:24,sm:18,md:18,lg:18,xl:18},{default:c(()=>[e("div",F,[e("div",G,[d(E,{class:"inputDeep",modelValue:v(a),"onUpdate:modelValue":s[1]||(s[1]=l=>f(a)?a.value=l:a=l)},{default:c(()=>[Z("DMZ Status")]),_:1},8,["modelValue"])])])]),_:1}),d(p,{xs:6,sm:6,md:6,lg:6,xl:6},{default:c(()=>[e("div",{class:"SaveBotton",onClick:D},n(o.$t("system.save")),1)]),_:1})]),_:1})])])])])}}},_e=w(J,[["__scopeId","data-v-b3041cf8"]]);export{_e as default}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/FireWall-CF_elBY-.css: -------------------------------------------------------------------------------- 1 | .OnlineClient .OnlineClientTop[data-v-592fa5df]{padding:24px;box-sizing:border-box;border-radius:16px;border:1px solid #eaebf6;background:#fff}.OnlineClient .OnlineClientTop .OnlineHeader[data-v-592fa5df]{display:flex;justify-content:space-between}.OnlineClient .OnlineClientTop .OnlineHeader .headerLeft[data-v-592fa5df]{color:#333;font-family:SF Pro;font-size:20px;font-style:normal;font-weight:510;line-height:100%}.OnlineClient .OnlineClientTop .OnlineHeader .headerRight[data-v-592fa5df]{cursor:pointer;color:#e12d2d;font-family:SF Pro;font-size:16px;font-style:normal;font-weight:510;line-height:100%}.OnlineClient .OnlineClientTop .OnlineBody[data-v-592fa5df]{padding-top:20px}.OnlineClient .OnlineClientTop .OnlineBody .WifiTable[data-v-592fa5df]{padding:20px 24px;display:flex;justify-content:space-between;align-items:center;border-radius:16px;border:1px solid #eaebf6;background:#f9fbff;margin-bottom:16px}.OnlineClient .OnlineClientTop .OnlineBody .WifiTable .WifiHeader[data-v-592fa5df]{color:#333;font-family:SF Pro;font-size:16px;font-style:normal;font-weight:400;line-height:100%}.OnlineClient .OnlineClientTop .OnlineBody .saveBtn[data-v-592fa5df]{cursor:pointer;margin:24px auto 0;display:flex;height:56px;padding:18px 97px;justify-content:center;align-items:center;gap:8px;flex-shrink:0;border-radius:16px;border:1px solid rgba(0,102,255,.15);background:#0066ff1a;color:#06f;font-family:SF Pro;font-size:20px;font-style:normal;font-weight:510;line-height:100%} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/FireWall-CLzkzUe3.js: -------------------------------------------------------------------------------- 1 | import{_ as h,a4 as g,k as c,a1 as y,o as W,g as b,s as e,x as _,p as f,a as u,Z as m}from"./index-CCLY6QsW.js";import{E as V}from"./el-switch-BTV1w9Lp.js";import{p}from"./index-CSuQBRO8.js";import{E as B}from"./index-DmH9s9cX.js";import{E as v}from"./index-DuTyNaIr.js";import"./index-82_0c4AB.js";import"./index-APFVxEOi.js";import"./constants-pKhjEyna.js";import"./use-form-item-BXSSCJ3O.js";import"./request-kOUjDz9H.js";import"./directive-4ZKEvCYR.js";import"./index-Bfki-cik.js";import"./typescript-Bp3YSIOJ.js";import"./aria-BUADUvnR.js";const S={class:"OnlineClient"},D={class:"OnlineClientTop"},E={class:"OnlineHeader"},F={class:"headerLeft"},O={class:"OnlineBody"},k={class:"WifiTable"},I={class:"WifiHeader"},x={class:"WifiBody"},H={class:"WifiTable"},T={class:"WifiHeader"},C={class:"WifiBody"},U={class:"WifiTable"},L={class:"WifiHeader"},M={class:"WifiBody"},N={__name:"FireWall",setup($){const r=g();let d=c(""),o=c(!1),i=c(!1),s=c(!1);y(()=>{d.value=r.globalData.logon_pwd,o.value=r.globalData.firewall_switch=="1",i.value=r.globalData.ip_firewall_switch=="1",s.value=r.globalData.domain_firewall_switch=="1",console.log(s.value)});const w=async()=>{const t=B.service({target:"body",text:"Save settings..."});try{let l={cmd:301,logon_pwd:d.value,firewall_switch:o.value==!0?"1":"0"},n={cmd:302,logon_pwd:d.value,ip_firewall_switch:i.value==!0?"1":"0"},a={cmd:306,logon_pwd:d.value,domain_firewall_switch:s.value==!0?"1":"0"};console.log(s.value,i.value,o.value),await p(l),await p(n),await p(a),v.success("Save successfully")}catch(l){v.error(`An error occurred: ${l.message||l}`)}finally{t.close()}};return(t,l)=>{const n=V;return W(),b("div",null,[e("div",S,[e("div",D,[e("div",E,[e("div",F,_(t.$t("system.firewall")),1)]),e("div",O,[e("div",k,[e("div",I,_(t.$t("system.enableFirewall")),1),e("div",x,[f(n,{style:{width:"43px"},modelValue:u(o),"onUpdate:modelValue":l[0]||(l[0]=a=>m(o)?o.value=a:o=a)},null,8,["modelValue"])])]),e("div",H,[e("div",T,_(t.$t("system.enableIP")),1),e("div",C,[f(n,{style:{width:"43px"},modelValue:u(i),"onUpdate:modelValue":l[1]||(l[1]=a=>m(i)?i.value=a:i=a)},null,8,["modelValue"])])]),e("div",U,[e("div",L,_(t.$t("system.enableDomain")),1),e("div",M,[f(n,{style:{width:"43px"},modelValue:u(s),"onUpdate:modelValue":l[2]||(l[2]=a=>m(s)?s.value=a:s=a)},null,8,["modelValue"])])]),e("div",{class:"saveBtn",onClick:w},"Save")])])])])}}},le=h(N,[["__scopeId","data-v-592fa5df"]]);export{le as default}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/GuidePage-DtFLNpBe.css: -------------------------------------------------------------------------------- 1 | .custom-loading[data-v-2396bf92]{background:url(/assets/submiting-B7ULOMtL.gif) no-repeat center center;background-size:contain;width:50px;height:50px}.Guide[data-v-2396bf92]{width:100%}.Guide .Main[data-v-2396bf92]{margin-top:40px}.Guide .Main .mt30[data-v-2396bf92]{padding-left:20px;padding-right:20px;margin-top:30px;min-height:56px}.Guide .Main .mt30 .SaveButton[data-v-2396bf92]{width:100%;min-height:56px;background:#06f;border-radius:16px;font-size:20px;font-weight:510;color:#fff;border:none}.Guide .Main .title[data-v-2396bf92]{font-size:24px;font-weight:700;text-align:center}.Guide .Main .Smalltitle[data-v-2396bf92]{text-align:left;font-size:20px;line-height:56px}.Guide .Main .ItemInput[data-v-2396bf92]{height:100%;display:flex;padding:0 20px;border-radius:16px;color:#06f;background:#f9fbff;font-family:SF Pro;font-size:20px;font-style:normal;font-weight:510;border:1px solid rgba(0,102,255,.15)}.Guide .Main .ItemInput .inputDeep[data-v-2396bf92] .el-input__wrapper{padding:0;background:#f9fbff;box-shadow:0 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;cursor:default}.Guide .Main .ItemInput .inputDeep[data-v-2396bf92] .el-input__wrapper .el-input__inner{color:#333;font-family:SF Pro;font-size:16px;font-weight:510}.Guide .Main .ItemTip[data-v-2396bf92]{height:100%;display:flex;justify-content:space-between;align-items:center;padding:0 20px} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/NoSim-C9MXz0Rx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/NoSim-C9MXz0Rx.png -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/SystemLog-Cy2qRY8Y.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px){.OnlineLeft[data-v-6ad4f192]{display:flex;flex-wrap:wrap;gap:8px!important}.OnlineLeft .OnlineItem[data-v-6ad4f192]{line-height:36px!important}.OnlineClientBottom P[data-v-6ad4f192]{font-size:10px}}.mt24[data-v-6ad4f192]{margin-top:20px}.OnlineClient[data-v-6ad4f192]{border:1px solid #eaebf6;border-radius:16px;background:#fff}.OnlineClient .OnlineClientTop[data-v-6ad4f192]{padding:24px;box-sizing:border-box;display:flex;flex-wrap:wrap;justify-content:space-between}.OnlineClient .OnlineClientTop .OnlineLeft[data-v-6ad4f192]{display:flex;flex-wrap:wrap;gap:24px}.OnlineClient .OnlineClientTop .OnlineLeft .OnlineItem[data-v-6ad4f192]{cursor:pointer;display:flex;border-radius:16px;padding:6px 12px;border:1px solid #eaebf6;color:#999;font-family:SF Pro;font-size:16px;font-style:normal;text-wrap:nowrap;font-weight:510;line-height:56px}.OnlineClient .OnlineClientTop .OnlineLeft .activeOnlineItem[data-v-6ad4f192]{color:#333;border:1px solid rgba(0,102,255,.2);background:#f2f7ff}.OnlineClient .OnlineClientTop .OnlineRight[data-v-6ad4f192]{margin-top:8px;height:56px;display:flex}.OnlineClient .OnlineClientTop .OnlineRight .autofresh[data-v-6ad4f192]{box-sizing:border-box;padding:16px 20px;display:flex;align-items:center;border-radius:16px;border:1px solid #eaebf6;margin-right:16px}.OnlineClient .OnlineClientTop .OnlineRight .autofresh .SwitchOpen[data-v-6ad4f192]{margin-right:10px}.OnlineClient .OnlineClientTop .OnlineRight .refreshButton[data-v-6ad4f192]{cursor:pointer;padding:18px;border-radius:16px;border:1px solid rgba(0,102,255,.15);background:#0066ff1a;color:#06f;font-family:SF Pro;font-size:16px;font-style:normal;font-weight:400}.OnlineClient .OnlineClientBottom[data-v-6ad4f192]{height:500px;overflow:auto;margin-left:20px;color:#666;font-family:SF Pro;font-size:14px;font-style:normal;font-weight:400}.OnlineClient .OnlineClientBottom P[data-v-6ad4f192]{width:100%;margin:0;word-wrap:break-word;white-space:normal;box-sizing:border-box}.OnlineClient .loadingMore[data-v-6ad4f192]{margin-top:8px;margin-bottom:24px;display:flex;justify-content:center}.OnlineClient .loadingMore .ViewMore[data-v-6ad4f192]{cursor:pointer;color:#06f;font-family:SF Pro;font-size:16px;font-weight:400} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/SystemLog-gixzKMAI.js: -------------------------------------------------------------------------------- 1 | import{_ as V,k as a,a4 as $,C as D,D as M,ar as N,F as E,o as l,g as i,s as o,J as I,Y as w,x as c,p as R,q as U,a as k,n as F}from"./index-CCLY6QsW.js";/* empty css */import{E as T}from"./el-switch-BTV1w9Lp.js";import{$ as z,a0 as A,a1 as q}from"./index-CSuQBRO8.js";import{v as G}from"./directive-4ZKEvCYR.js";import"./index-82_0c4AB.js";import"./index-APFVxEOi.js";import"./constants-pKhjEyna.js";import"./use-form-item-BXSSCJ3O.js";import"./request-kOUjDz9H.js";const J={class:"OnlineClient mt24"},Y={class:"OnlineClientTop"},j={class:"OnlineLeft"},x=["onClick"],H={class:"OnlineRight"},K={class:"autofresh"},P={class:"SwitchOpen"},Q={class:"OnlineClientBottom"},W={class:"loadingMore"},X={__name:"SystemLog",setup(Z){const d=a(1),u=$();let v=a(0);const C=a([{name:"runlog"},{name:"operationlog"},{name:"Applog"},{name:"Upgradelog"},{name:"Exceptionlog"}]),f=a(!1),g=a(""),m=a(""),n=D({logList:[]});let p=a(!1);const S=e=>{if(!e)return"无效日期";try{const t=typeof e=="string"?parseInt(e):e;return isNaN(t)?(console.error("无效的时间戳:",e),"无效日期"):(t.toString().length===10?new Date(t*1e3):new Date(t)).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1})}catch(t){return console.error("日期格式化错误:",t),"无效日期"}},_=async e=>{v.value=e-1,console.log(v.value),d.value=e;try{const t=await z({password:u.token,type:d.value,offset:0,limit:100});t.code==0&&(n.logList=t.data,g.value=t.data[0].id,m.value=t.data[t.data.length-1].id)}catch(t){console.log(t)}finally{p.value=!1}},y=async()=>{const e=await A({id:g.value,password:u.token,type:d.value,limit:100});n.logList=[...e.data,...n.logList],g.value=e.data[0].id},B=async()=>{console.log(g.value);try{const e=await q({password:u.token,type:d.value,id:m.value,limit:100});e.code==0&&(n.logList=[...n.logList,...e.data],m.value=e.data[e.data.length-1].id)}catch(e){console.log(e)}finally{p.value=!1}};let h;return M(f,e=>{e?h=setInterval(()=>{y()},1e3):clearInterval(h)}),N(()=>{clearInterval(h)}),E(()=>{_(1)}),(e,t)=>{const L=T,O=G;return l(),i("div",null,[o("div",J,[o("div",Y,[o("div",j,[(l(!0),i(I,null,w(C.value,(s,r)=>(l(),i("div",{key:r,class:F({OnlineItem:!0,activeOnlineItem:k(v)==r}),onClick:b=>_(r+1)},c(e.$t(`log.${s.name}`)),11,x))),128))]),o("div",H,[o("div",K,[o("div",P,c(e.$t("log.Autorefresh")),1),R(L,{modelValue:f.value,"onUpdate:modelValue":t[0]||(t[0]=s=>f.value=s),"active-color":"#06F","inactive-color":"#ff4949"},null,8,["modelValue"])]),o("div",{class:"refreshButton",onClick:y},c(e.$t("log.refresh")),1)])]),U((l(),i("div",Q,[(l(!0),i(I,null,w(n.logList,(s,r)=>(l(),i("p",{key:r,class:"logItem"},c(S(s.time)+" "+s.tag+" "+s.msg),1))),128))])),[[O,k(p)]]),o("div",W,[o("div",{class:"ViewMore",onClick:B},c(e.$t("log.ViewMore")),1)])])])}}},de=V(X,[["__scopeId","data-v-6ad4f192"]]);export{de as default}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/UpnpPage-BxUkYuxd.js: -------------------------------------------------------------------------------- 1 | import{_ as B,a4 as w,k as v,C,a1 as $,o as c,g as _,s,x as t,p as o,a as p,Z as H,w as d,J as O,Y as k,c as S}from"./index-CCLY6QsW.js";import{E as U,a as E}from"./el-col-BkR5g-BW.js";import{E as A}from"./el-switch-BTV1w9Lp.js";import{m as f}from"./index-CSuQBRO8.js";import"./index-82_0c4AB.js";import"./typescript-Bp3YSIOJ.js";import"./index-APFVxEOi.js";import"./constants-pKhjEyna.js";import"./use-form-item-BXSSCJ3O.js";import"./request-kOUjDz9H.js";const L={class:"OnlineClient"},N={class:"OnlineClientTop"},P={class:"OnlineHeader"},R={class:"headerLeft"},T={class:"OnlineBody"},V={class:"SmallText"},b={class:"UpgradeDuring"},D={class:"duringLeft"},W={class:"leftTop"},F={class:"leftBttom"},M={class:"duringRight"},G={class:"OnlineClient mt24"},J={class:"OnlineClientTop"},Y={class:"OnlineHeader"},Z={class:"headerLeft"},j={class:"OnlineBody"},q={style:{overflow:"auto"}},z={class:"WifiTable"},K={class:"WifiHeader"},Q={class:"HeaderItem"},X={class:"HeaderItem"},ss={class:"HeaderItem"},es={class:"HeaderItem"},ts={class:"HeaderItem"},os={class:"HeaderItem"},ds={class:"WifiBody"},ls={key:0,class:"Nodata"},as={key:1},ns={class:"BodyItem"},is={class:"BodyItem"},cs={class:"BodyItem"},_s={class:"BodyItem"},rs={class:"BodyItem"},ms={class:"BodyItem"},ps={__name:"UpnpPage",setup(us){const g=w();let r=v(""),n=v(""),m=C([]);$(()=>{r.value=g.globalData.logon_pwd,u()});const u=async()=>{const e={cmd:"152",logon_pwd:r.value},i=await f(e,"readupnp");n.value=i.upnp.enabled==1,m=i.upnp.mapping},y=async()=>{try{let e={cmd:"153",logon_pwd:r.value,enabled:n.value==!0?1:0};(await f(e,"setupnp")).code==200&&u()}catch(e){console.log(e)}};return(e,i)=>{const x=A,l=E,h=U;return c(),_("div",null,[s("div",L,[s("div",N,[s("div",P,[s("div",R,t(e.$t("system.upnp")),1)]),s("div",T,[s("div",V,t(e.$t("system.descF")),1),s("div",b,[s("div",D,[s("div",W,t(e.$t("system.upnpStatus")),1),s("div",F,t(e.$t("system.descG")),1)]),s("div",M,[o(x,{onChange:y,modelValue:p(n),"onUpdate:modelValue":i[0]||(i[0]=a=>H(n)?n.value=a:n=a)},null,8,["modelValue"])])])])])]),s("div",G,[s("div",J,[s("div",Y,[s("div",Z,t(e.$t("system.list")),1)]),s("div",j,[s("div",q,[s("div",z,[s("div",K,[o(h,{gutter:24},{default:d(()=>[o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",Q,t(e.$t("system.remoteAddress")),1)]),_:1}),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",X,t(e.$t("system.remoteAddress")),1)]),_:1}),o(l,{lg:3,md:3,sm:3,xl:3,xs:3},{default:d(()=>[s("div",ss,t(e.$t("system.protocol")),1)]),_:1}),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",es,t(e.$t("system.externalAddress")),1)]),_:1}),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",ts,t(e.$t("system.internetPort")),1)]),_:1}),o(l,{lg:5,md:5,sm:5,xl:5,xs:5},{default:d(()=>[s("div",os,t(e.$t("system.applicationDescription")),1)]),_:1})]),_:1})]),s("div",ds,[p(m).length==0?(c(),_("div",ls,"Nodata")):(c(),_("div",as,[(c(!0),_(O,null,k(p(m),(a,I)=>(c(),S(h,{key:I,gutter:24,class:"RowItem"},{default:d(()=>[o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",ns,t(a.remoteAddress),1)]),_:2},1024),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",is,t(a.localAddress),1)]),_:2},1024),o(l,{lg:3,md:3,sm:3,xl:3,xs:3},{default:d(()=>[s("div",cs,t(a.protocol),1)]),_:2},1024),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",_s,t(a.externalPort),1)]),_:2},1024),o(l,{lg:4,md:4,sm:4,xl:4,xs:4},{default:d(()=>[s("div",rs,t(a.internalPort),1)]),_:2},1024),o(l,{lg:5,md:5,sm:5,xl:5,xs:5},{default:d(()=>[s("div",ms,t(a.description==""?"None":a.description),1)]),_:2},1024)]),_:2},1024))),128))]))])])])])])])])}}},$s=B(ps,[["__scopeId","data-v-8ce7a244"]]);export{$s as default}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/aria-BUADUvnR.js: -------------------------------------------------------------------------------- 1 | const e={tab:"Tab",enter:"Enter",space:"Space",left:"ArrowLeft",up:"ArrowUp",right:"ArrowRight",down:"ArrowDown",esc:"Escape",delete:"Delete",backspace:"Backspace",numpadEnter:"NumpadEnter",pageUp:"PageUp",pageDown:"PageDown",home:"Home",end:"End"};export{e as E}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/castArray-CsLOnyYC.js: -------------------------------------------------------------------------------- 1 | import{az as a}from"./index-CCLY6QsW.js";function e(){if(!arguments.length)return[];var r=arguments[0];return a(r)?r:[r]}export{e as c}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/constants-pKhjEyna.js: -------------------------------------------------------------------------------- 1 | import{bq as o,ay as g,b6 as p,ax as s,az as m,br as b,aK as v,b8 as S,aM as x,bs as P,j as w}from"./index-CCLY6QsW.js";function O(n){return n}function I(n,e,t){switch(t.length){case 0:return n.call(e);case 1:return n.call(e,t[0]);case 2:return n.call(e,t[0],t[1]);case 3:return n.call(e,t[0],t[1],t[2])}return n.apply(e,t)}var C=800,T=16,A=Date.now;function K(n){var e=0,t=0;return function(){var a=A(),r=T-(a-t);if(t=a,r>0){if(++e>=C)return arguments[0]}else e=0;return n.apply(void 0,arguments)}}function E(n){return function(){return n}}var M=o?function(n,e){return o(n,"toString",{configurable:!0,enumerable:!1,value:E(e),writable:!0})}:O,N=K(M),f=Math.max;function _(n,e,t){return e=f(e===void 0?n.length-1:e,0),function(){for(var a=arguments,r=-1,i=f(a.length-e,0),l=Array(i);++r-1&&n%1==0&&n<=k}var G="[object Arguments]";function c(n){return g(n)&&p(n)==G}var d=Object.prototype,H=d.hasOwnProperty,L=d.propertyIsEnumerable,y=c(function(){return arguments}())?c:function(n){return g(n)&&H.call(n,"callee")&&!L.call(n,"callee")};function R(n,e){for(var t=-1,a=e.length,r=n.length;++tV(W,n),j=Symbol("formContextKey"),nn=Symbol("formItemContextKey");export{R as a,nn as b,F as c,y as d,j as f,$ as h,O as i,V as p,Z as u}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-card-BJ3sbP9B.css: -------------------------------------------------------------------------------- 1 | .el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank);background-color:var(--el-card-bg-color);border:1px solid var(--el-card-border-color);border-radius:var(--el-card-border-radius);color:var(--el-text-color-primary);overflow:hidden;transition:var(--el-transition-duration)}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:var(--el-box-shadow-light)}.el-card__header{border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)}.el-card__body{padding:var(--el-card-padding)}.el-card__footer{border-top:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-col-BkR5g-BW.js: -------------------------------------------------------------------------------- 1 | import{j as g,d as f,u as _,f as o,E as R,o as $,c as h,w,r as v,n as j,a as c,h as N,a3 as x,b as C,M as r,B as S,al as b,N as K}from"./index-CCLY6QsW.js";import{_ as E}from"./index-82_0c4AB.js";import{m as u}from"./typescript-Bp3YSIOJ.js";const O=Symbol("rowContextKey"),B=["start","center","end","space-around","space-between","space-evenly"],P=["top","middle","bottom"],k=g({tag:{type:String,default:"div"},gutter:{type:Number,default:0},justify:{type:String,values:B,default:"start"},align:{type:String,values:P}}),L=f({name:"ElRow"}),A=f({...L,props:k,setup(p){const e=p,l=_("row"),a=o(()=>e.gutter);R(O,{gutter:a});const i=o(()=>{const t={};return e.gutter&&(t.marginRight=t.marginLeft=`-${e.gutter/2}px`),t}),d=o(()=>[l.b(),l.is(`justify-${e.justify}`,e.justify!=="start"),l.is(`align-${e.align}`,!!e.align)]);return(t,m)=>($(),h(x(t.tag),{class:j(c(d)),style:N(c(i))},{default:w(()=>[v(t.$slots,"default")]),_:3},8,["class","style"]))}});var D=E(A,[["__file","row.vue"]]);const Q=C(D),I=g({tag:{type:String,default:"div"},span:{type:Number,default:24},offset:{type:Number,default:0},pull:{type:Number,default:0},push:{type:Number,default:0},xs:{type:r([Number,Object]),default:()=>u({})},sm:{type:r([Number,Object]),default:()=>u({})},md:{type:r([Number,Object]),default:()=>u({})},lg:{type:r([Number,Object]),default:()=>u({})},xl:{type:r([Number,Object]),default:()=>u({})}}),J=f({name:"ElCol"}),M=f({...J,props:I,setup(p){const e=p,{gutter:l}=S(O,{gutter:o(()=>0)}),a=_("col"),i=o(()=>{const t={};return l.value&&(t.paddingLeft=t.paddingRight=`${l.value/2}px`),t}),d=o(()=>{const t=[];return["span","offset","pull","push"].forEach(s=>{const n=e[s];b(n)&&(s==="span"?t.push(a.b(`${e[s]}`)):n>0&&t.push(a.b(`${s}-${e[s]}`)))}),["xs","sm","md","lg","xl"].forEach(s=>{b(e[s])?t.push(a.b(`${s}-${e[s]}`)):K(e[s])&&Object.entries(e[s]).forEach(([n,y])=>{t.push(n!=="span"?a.b(`${s}-${n}-${y}`):a.b(`${s}-${y}`))})}),l.value&&t.push(a.is("guttered")),[a.b(),t]});return(t,m)=>($(),h(x(t.tag),{class:j(c(d)),style:N(c(i))},{default:w(()=>[v(t.$slots,"default")]),_:3},8,["class","style"]))}});var T=E(M,[["__file","col.vue"]]);const U=C(T);export{Q as E,U as a}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-dialog-DMam3H11.css: -------------------------------------------------------------------------------- 1 | :root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{background:var(--el-popup-modal-bg-color);height:100%;left:0;opacity:var(--el-popup-modal-opacity);position:fixed;top:0;width:100%}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;margin:var(--el-dialog-margin-top,15vh) auto 50px;overflow-wrap:break-word;padding:var(--el-dialog-padding-primary);position:relative;width:var(--el-dialog-width,50%)}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{bottom:0;left:0;margin:0;overflow:auto;position:fixed;right:0;top:0}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px))}.el-dialog__headerbtn{background:transparent;border:none;cursor:pointer;font-size:var(--el-message-close-size,16px);height:48px;outline:none;padding:0;position:absolute;right:0;top:0;width:48px}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{color:var(--el-text-color-primary);font-size:var(--el-dialog-title-font-size);line-height:var(--el-dialog-font-line-height)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{box-sizing:border-box;padding-top:var(--el-dialog-padding-primary);text-align:right}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-overlay-dialog{bottom:0;left:0;overflow:auto;position:fixed;right:0;top:0}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translate3d(0,-20px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20px,0)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-loading-x7H6yciF.css: -------------------------------------------------------------------------------- 1 | :root{--el-loading-spinner-size:42px;--el-loading-fullscreen-spinner-size:50px}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{background-color:var(--el-mask-color);bottom:0;left:0;margin:0;position:absolute;right:0;top:0;transition:opacity var(--el-transition-duration);z-index:2000}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:calc((0px - var(--el-loading-fullscreen-spinner-size))/2)}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:var(--el-loading-fullscreen-spinner-size);width:var(--el-loading-fullscreen-spinner-size)}.el-loading-spinner{margin-top:calc((0px - var(--el-loading-spinner-size))/2);position:absolute;text-align:center;top:50%;width:100%}.el-loading-spinner .el-loading-text{color:var(--el-color-primary);font-size:14px;margin:3px 0}.el-loading-spinner .circular{animation:loading-rotate 2s linear infinite;display:inline;height:var(--el-loading-spinner-size);width:var(--el-loading-spinner-size)}.el-loading-spinner .path{animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:var(--el-color-primary);stroke-linecap:round}.el-loading-spinner i{color:var(--el-color-primary)}.el-loading-fade-enter-from,.el-loading-fade-leave-to{opacity:0}@keyframes loading-rotate{to{transform:rotate(1turn)}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-overlay-DBZ27Rv4.css: -------------------------------------------------------------------------------- 1 | .el-overlay{background-color:var(--el-overlay-color-lighter);bottom:0;height:100%;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2000}.el-overlay .el-overlay-root{height:0} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/el-popper-DL6Na3xw.css: -------------------------------------------------------------------------------- 1 | .el-popper{--el-popper-border-radius:var(--el-popover-border-radius,4px);border-radius:var(--el-popper-border-radius);font-size:12px;line-height:20px;min-width:10px;overflow-wrap:break-word;padding:5px 11px;position:absolute;visibility:visible;z-index:2000}.el-popper.is-dark{color:var(--el-bg-color)}.el-popper.is-dark,.el-popper.is-dark>.el-popper__arrow:before{background:var(--el-text-color-primary);border:1px solid var(--el-text-color-primary)}.el-popper.is-dark>.el-popper__arrow:before{right:0}.el-popper.is-light,.el-popper.is-light>.el-popper__arrow:before{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light)}.el-popper.is-light>.el-popper__arrow:before{right:0}.el-popper.is-pure{padding:0}.el-popper__arrow,.el-popper__arrow:before{height:10px;position:absolute;width:10px;z-index:-1}.el-popper__arrow:before{background:var(--el-text-color-primary);box-sizing:border-box;content:" ";transform:rotate(45deg)}.el-popper[data-popper-placement^=top]>.el-popper__arrow{bottom:-5px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-bottom-right-radius:2px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow{top:-5px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-top-left-radius:2px}.el-popper[data-popper-placement^=left]>.el-popper__arrow{right:-5px}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-top-right-radius:2px}.el-popper[data-popper-placement^=right]>.el-popper__arrow{left:-5px}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-bottom-left-radius:2px}.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-left-color:transparent!important;border-top-color:transparent!important}.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:transparent!important;border-right-color:transparent!important}.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:transparent!important;border-left-color:transparent!important}.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-right-color:transparent!important;border-top-color:transparent!important} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/icon-collect-BCoEM0qf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/iconfont-zYHu7zwx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/iconfont-zYHu7zwx.ttf -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/index-APFVxEOi.js: -------------------------------------------------------------------------------- 1 | import{l as i,f as d,g as u,c as o,s as t,w as a,n as c,o as r}from"./index-82_0c4AB.js";import{M as f,aY as p,f as _,a as m,A as E,B as I}from"./index-CCLY6QsW.js";class C extends Error{constructor(e){super(e),this.name="ElementPlusError"}}function w(s,e){throw new C(`[${s}] ${e}`)}function M(s,e){}const P=f([String,Object,Function]),V={Close:o},y={Close:o,SuccessFilled:t,InfoFilled:r,WarningFilled:a,CircleCloseFilled:c},F={success:t,warning:a,error:c,info:r},$={validating:i,success:d,error:u},b="update:modelValue",x="change",D="input",n={prefix:Math.floor(Math.random()*1e4),current:0},g=Symbol("elIdInjection"),T=()=>E()?I(g,n):n,v=s=>{const e=T(),l=p();return _(()=>m(s)||`${l.value}-id-${e.prefix}-${e.current++}`)};export{V as C,D as I,F as T,b as U,$ as V,x as a,T as b,y as c,M as d,P as i,w as t,v as u}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/index-DmH9s9cX.js: -------------------------------------------------------------------------------- 1 | import{v as o,L as a}from"./directive-4ZKEvCYR.js";const n={install(i){i.directive("loading",o),i.config.globalProperties.$loading=a},directive:o,service:a};export{n as E}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/index-vNULV4St.js: -------------------------------------------------------------------------------- 1 | import{D as r,a}from"./index-CCLY6QsW.js";const n=({from:t,replacement:o,scope:s,version:m,ref:p,type:c="API"},e)=>{r(()=>a(e),f=>{},{immediate:!0})};export{n as u}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/loginbg-CPP8WZVm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/loginbg-CPP8WZVm.png -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/logoImg-CwUB_hdd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/logoImg-CwUB_hdd.png -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/openDownLoad-Cl2XZ_8t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/openDownLoad-Cl2XZ_8t.png -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/openDownLoad-DhbK9y2K.js: -------------------------------------------------------------------------------- 1 | const n="/assets/openDownLoad-Cl2XZ_8t.png";export{n as O}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/selectBox-03onemMs.js: -------------------------------------------------------------------------------- 1 | import{_ as k,k as l,o as a,g as c,s as e,x as i,a as f,q as h,z as w,J as g,Y as B}from"./index-CCLY6QsW.js";import{O as x}from"./openDownLoad-DhbK9y2K.js";/* empty css */const y={class:"dropdownBlock"},D={class:"dropApnRight"},S={class:"disAble"},C=["src"],N={class:"drowdownBottom"},O=["onClick"],q={__name:"selectBox",props:{selectData:{type:String,required:!0},options:{type:Array,required:!0}},emits:["update"],setup(n,{emit:d}){const p=n,u=d,s=l("");s.value=p.selectData;const t=l(!1),_=()=>{t.value=!t.value},v=o=>{s.value=o,t.value=!1,u("update",{data:o})};return(o,A)=>(a(),c("div",y,[e("div",D,[e("div",{class:"dropdownUp",onClick:_},[e("div",S,i(s.value),1),e("img",{class:"OpenDrow",src:f(x),alt:""},null,8,C)]),h(e("div",N,[(a(!0),c(g,null,B(n.options,(r,m)=>(a(),c("div",{key:m,class:"drowdownItem",onClick:I=>v(r)},i(r),9,O))),128))],512),[[w,t.value]])])]))}},E=k(q,[["__scopeId","data-v-16c05ffe"]]);export{E as s}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/selectBox-CcQTmTjQ.css: -------------------------------------------------------------------------------- 1 | .dropdownBlock[data-v-16c05ffe]{width:100%;display:flex;justify-content:space-between;align-items:center;min-height:56px;border-radius:16px;border:1px solid #eaebf6;background:#f9fbff}.dropdownBlock .dropApnLeft[data-v-16c05ffe]{padding-left:16px;color:#333;font-family:SF Pro;font-size:16px;font-style:normal;font-weight:510;line-height:16px}.dropdownBlock .dropApnRight[data-v-16c05ffe]{width:100%;font-size:16px;font-style:normal;font-weight:510;line-height:16px;border-radius:16px;display:flex;flex-direction:column}.dropdownBlock .dropApnRight .inputDeep[data-v-16c05ffe] .el-input__wrapper{padding:0;cursor:default}.dropdownBlock .dropApnRight .inputDeep[data-v-16c05ffe] .el-input__wrapper .el-input__inner{font-size:16px;color:#333!important;cursor:default!important}.dropdownBlock .dropApnRight .dropdownUp[data-v-16c05ffe]{box-sizing:border-box;padding:0 24px;height:42px;color:#333;font-family:SF Pro;font-size:14px;font-style:normal;font-weight:510;line-height:16px;display:flex;justify-content:space-between;align-items:center}.dropdownBlock .dropApnRight .dropdownUp .OpenDrow[data-v-16c05ffe]{cursor:pointer}.dropdownBlock .dropApnRight .drowdownBottom[data-v-16c05ffe]{box-sizing:border-box;border-radius:20px;background:#f9fbff}.dropdownBlock .dropApnRight .drowdownBottom .drowdownItem[data-v-16c05ffe]{color:#666;font-family:SF Pro;font-size:14px;font-style:normal;font-weight:510;line-height:56px;box-sizing:border-box;padding:0 24px;align-items:center;border-radius:20px;height:56px}.dropdownBlock .dropApnRight .drowdownBottom .drowdownItem[data-v-16c05ffe]:hover{background:#06f!important;color:#fff!important} 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/submiting-B7ULOMtL.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/assets/submiting-B7ULOMtL.gif -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/typescript-Bp3YSIOJ.js: -------------------------------------------------------------------------------- 1 | const a=t=>t;export{a as m}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/assets/use-form-item-BXSSCJ3O.js: -------------------------------------------------------------------------------- 1 | import{f as c,b as p}from"./constants-pKhjEyna.js";import{A as b,f as t,k as d,bp as z,B as v,a as m,F,D as y,Q as h,ar as w}from"./index-CCLY6QsW.js";import{u as _}from"./index-APFVxEOi.js";const I=s=>{const o=b();return t(()=>{var u,l;return(l=(u=o==null?void 0:o.proxy)==null?void 0:u.$props)==null?void 0:l[s]})},L=(s,o={})=>{const u=d(void 0),l=o.prop?u:I("size"),e=o.global?u:z(),a=o.form?{size:void 0}:v(c,void 0),n=o.formItem?{size:void 0}:v(p,void 0);return t(()=>l.value||m(s)||(n==null?void 0:n.size)||(a==null?void 0:a.size)||e.value||"")},R=s=>{const o=I("disabled"),u=v(c,void 0);return t(()=>o.value||m(s)||(u==null?void 0:u.disabled)||!1)},S=()=>{const s=v(c,void 0),o=v(p,void 0);return{form:s,formItem:o}},U=(s,{formItemContext:o,disableIdGeneration:u,disableIdManagement:l})=>{u||(u=d(!1)),l||(l=d(!1));const e=d();let a;const n=t(()=>{var i;return!!(!(s.label||s.ariaLabel)&&o&&o.inputIds&&((i=o.inputIds)==null?void 0:i.length)<=1)});return F(()=>{a=y([h(s,"id"),u],([i,f])=>{const r=i??(f?void 0:_().value);r!==e.value&&(o!=null&&o.removeInputId&&(e.value&&o.removeInputId(e.value),!(l!=null&&l.value)&&!f&&r&&o.addInputId(r)),e.value=r)},{immediate:!0})}),w(()=>{a&&a(),o!=null&&o.removeInputId&&e.value&&o.removeInputId(e.value)}),{isLabeledByFormItem:n,inputId:e}};export{S as a,L as b,U as c,R as u}; 2 | -------------------------------------------------------------------------------- /app/src/main/assets/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/favicon.ico -------------------------------------------------------------------------------- /app/src/main/assets/dist/fonts/iconfont-Cgxpgk-X.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/fonts/iconfont-Cgxpgk-X.woff2 -------------------------------------------------------------------------------- /app/src/main/assets/dist/fonts/iconfont-D0JD_TNW.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/assets/dist/fonts/iconfont-D0JD_TNW.woff -------------------------------------------------------------------------------- /app/src/main/assets/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JTUN 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project("vpnhotspot") 14 | 15 | # Creates and names a library, sets it as either STATIC 16 | # or SHARED, and provides the relative paths to its source code. 17 | # You can define multiple libraries, and CMake builds them for you. 18 | # Gradle automatically packages shared libraries with your APK. 19 | # 20 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 21 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 22 | # is preferred for the same purpose. 23 | # 24 | # In order to load a library into your app from Java/Kotlin, you must call 25 | # System.loadLibrary() and pass the name of the library defined here; 26 | # for GameActivity/NativeActivity derived applications, the same library name must be 27 | # used in the AndroidManifest.xml file. 28 | add_library(${CMAKE_PROJECT_NAME} SHARED 29 | # List C/C++ source files with relative paths to this CMakeLists.txt. 30 | root.cpp) 31 | 32 | # Specifies libraries CMake should link to your target library. You 33 | # can link libraries from various origins, such as libraries defined in this 34 | # build script, prebuilt third-party libraries, or Android system libraries. 35 | target_link_libraries(${CMAKE_PROJECT_NAME} 36 | # List libraries link to the target library 37 | android 38 | log) 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/IpNeighbourMonitoringService.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router 2 | 3 | import android.app.Service 4 | import com.jtun.router.net.IpNeighbour 5 | import com.jtun.router.net.monitor.IpNeighbourMonitor 6 | import java.net.Inet4Address 7 | 8 | abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback { 9 | private var neighbours: Collection = emptyList() 10 | 11 | protected abstract val activeIfaces: List 12 | protected open val inactiveIfaces get() = emptyList() 13 | 14 | override fun onIpNeighbourAvailable(neighbours: Collection) { 15 | this.neighbours = neighbours 16 | updateNotification() 17 | } 18 | protected open fun updateNotification() { 19 | val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> 20 | neighbours 21 | .filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID } 22 | .distinctBy { it.lladdr } 23 | .size 24 | } 25 | ServiceNotification.startForeground(this, activeIfaces.associateWith { sizeLookup[it] ?: 0 }, inactiveIfaces, 26 | false) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/bus/RxBusSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.bus; 2 | 3 | 4 | import io.reactivex.rxjava3.observers.DisposableObserver; 5 | 6 | /** 7 | * 为RxBus使用的Subscriber, 主要提供next事件的try,catch 8 | */ 9 | public abstract class RxBusSubscriber extends DisposableObserver { 10 | 11 | @Override 12 | public void onNext(T t) { 13 | try { 14 | onEvent(t); 15 | } catch (Exception e) { 16 | e.printStackTrace(); 17 | } 18 | } 19 | 20 | @Override 21 | public void onComplete() { 22 | } 23 | 24 | @Override 25 | public void onError(Throwable e) { 26 | e.printStackTrace(); 27 | } 28 | 29 | protected abstract void onEvent(T t); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/bus/RxSubscriptions.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.bus; 2 | 3 | 4 | import io.reactivex.rxjava3.disposables.CompositeDisposable; 5 | import io.reactivex.rxjava3.disposables.Disposable; 6 | 7 | /** 8 | * 管理 CompositeSubscription 9 | */ 10 | public class RxSubscriptions { 11 | private static CompositeDisposable mSubscriptions = new CompositeDisposable(); 12 | 13 | public static boolean isDisposed() { 14 | return mSubscriptions.isDisposed(); 15 | } 16 | 17 | public static void add(Disposable s) { 18 | if (s != null) { 19 | mSubscriptions.add(s); 20 | } 21 | } 22 | 23 | public static void remove(Disposable s) { 24 | if (s != null) { 25 | mSubscriptions.remove(s); 26 | } 27 | } 28 | 29 | public static void clear() { 30 | mSubscriptions.clear(); 31 | } 32 | 33 | public static void dispose() { 34 | mSubscriptions.dispose(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/callback/AppNetworkCallback.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.callback 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import com.jtun.router.App 8 | import com.jtun.router.util.KLog 9 | import com.jtun.router.util.NetworkUtils 10 | import com.jtun.router.util.TetheringUtil 11 | 12 | class AppNetworkCallback : ConnectivityManager.NetworkCallback() { 13 | //当网络状态修改但仍旧是可用状态时调用 14 | override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { 15 | super.onCapabilitiesChanged(network, networkCapabilities) 16 | if (NetworkUtils.isConnectedAvailableNetwork(App.app)) { 17 | KLog.d("onCapabilitiesChanged ---> ====网络可正常上网===网络类型为: " + NetworkUtils.getConnectedNetworkType(App.app)) 18 | TetheringUtil.startTetheringService() 19 | } 20 | 21 | //表明此网络连接验证成功 22 | if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { 23 | if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { 24 | KLog.d("===当前在使用Mobile流量上网===") 25 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { 26 | KLog.d("====当前在使用WiFi上网===") 27 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) { 28 | KLog.d("=====当前使用蓝牙上网=====") 29 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { 30 | KLog.d("=====当前使用以太网上网=====") 31 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { 32 | KLog.d("===当前使用VPN上网====") 33 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) { 34 | KLog.d("===表示此网络使用Wi-Fi感知传输====") 35 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) { 36 | KLog.d("=====表示此网络使用LoWPAN传输=====") 37 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB)) { 38 | KLog.d("=====表示此网络使用USB传输=====") 39 | } 40 | } 41 | } 42 | 43 | override fun onAvailable(network: Network) { 44 | super.onAvailable(network) 45 | KLog.e("==网络连接成功,通知可以使用的时候调用====onAvailable===") 46 | } 47 | 48 | override fun onUnavailable() { 49 | KLog.e("==当网络连接超时或网络请求达不到可用要求时调用====onUnavailable===") 50 | super.onUnavailable() 51 | } 52 | 53 | override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { 54 | KLog.e("==当访问指定的网络被阻止或解除阻塞时调用===onBlockedStatusChanged==") 55 | super.onBlockedStatusChanged(network, blocked) 56 | } 57 | 58 | override fun onLosing(network: Network, maxMsToLive: Int) { 59 | KLog.e("==当网络正在断开连接时调用===onLosing===") 60 | super.onLosing(network, maxMsToLive) 61 | } 62 | 63 | override fun onLost(network: Network) { 64 | KLog.e("==当网络已断开连接时调用===onLost===") 65 | super.onLost(network) 66 | } 67 | 68 | override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { 69 | KLog.e("==当网络连接的属性被修改时调用===onLinkPropertiesChanged===") 70 | super.onLinkPropertiesChanged(network, linkProperties) 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/config/CMD.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.config 2 | 3 | object CMD { 4 | const val LOGIN = 1000//登录 5 | const val SET_PASSWORD = 1001//修改命令 6 | const val TRAFFIC = 1002//获取流量统计 7 | const val UPDATE_APK = 100//更新Apk 8 | const val UPDATE_OS = 101//更新固件 9 | const val RESTART = 200//重启 10 | const val SHUTDOWN = 201//关机 11 | const val RESET = 202//重置 12 | const val PING = 300 //ping 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/config/Config.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.config 2 | 3 | object Config { 4 | const val HTTP_WEB_PORT = 2000 5 | const val HTTP_PORT = 2001 6 | const val ALIST_PORT = 5244 7 | 8 | const val VMESS = "vmess://" 9 | const val CUSTOM = "" 10 | const val SHADOWSOCKS = "ss://" 11 | const val SOCKS = "socks://" 12 | const val VLESS = "vless://" 13 | const val TROJAN = "trojan://" 14 | const val WIREGUARD = "wireguard://" 15 | 16 | const val AP_TRIFFIC_CAL_START = "ap_triffic_cal_start"; 17 | const val V2RAY_PACKAGE_NAME = "com.jtun.v2ray" 18 | const val ALIST_PACKAGE_NAME = "com.jtun.android.alistlite" 19 | const val OPENVPN_PACKAGE_NAME = "com.jtun.openvpn" 20 | const val FRP_PACKAGE_NAME = "com.jtun.frpc_android" 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/config/HttpInterface.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.config 2 | 3 | object HttpInterface { 4 | const val LOGIN = "/login"//接口地址 5 | const val SET_ADMIN_PASS = "/setAdminPass"//修改管理密码 6 | const val NET_STATE = "/netState"//连接状态 7 | const val DEVICE_INFO = "/deviceInfo"//设备信息 8 | const val AP_CONFIG = "/apConfig"//获取热点配置信息 9 | const val SET_AP_CONFIG = "/setApConfig"//修改热点信息 10 | const val GET_CLIENTS = "/getClients"//获取连接客户端列表 11 | const val SET_CLIENT = "/setClient"//修改客户端 12 | const val DELETE_CLIENT = "/deleteClient"//删除客户端 13 | const val GET_INTERNET_INFO = "/internetInfo"//获取网络信息 14 | const val SET_DATA_PLAN = "/setDataPlan"//设置数据流量 15 | const val GET_USED_INFO = "/getUsedInfo"//获取使用流量信息 16 | const val DELETE_CLIENT_LIST = "/deleteClientList"//删除多个客户端 17 | const val GET_APP_LIST = "/getApps"//获取已安装第三方应用列表 18 | const val START_APP = "/startApp"//启动指定应用 19 | const val STOP_APP = "/stopApp"//停止指定应用 20 | const val V2RAY_CONFIG = "/v2rayGetConfig"//获取v2ray配置信息 21 | const val V2RAY_IMPORT = "/v2rayImport" //导入v2ray配置信息 22 | const val V2RAY_START = "/v2rayStart" //启动v2ray服务 23 | const val V2RAY_STOP = "/v2rayStop" //停止v2ray服务 24 | const val V2RAY_CHECK_CONFIG = "/v2rayCheckConfig"//选中指定配置 25 | const val V2RAY_DELETE_CONFIG = "/v2rayDeleteConfig"//删除指定配置 26 | const val V2RAY_PING_TEST = "/v2rayPingTest"//测试网络 27 | 28 | const val ALIST_STATE = "/alistState"//alist运行状态 29 | const val ALIST_START = "/alistStart"//alist启动 30 | const val ALIST_STOP = "/alistStop"//alist停止运行 31 | const val ALIST_ADMIN_PASS = "/alistAdminPass"//alist管理密码 32 | 33 | const val OPENVPN_IMPORT = "/openvpnImport"//导入openvpn 34 | const val OPENVPN_START = "/openvpnStart"//启动 35 | const val OPENVPN_STOP = "/openvpnStop"//停止 36 | const val OPENVPN_LIST = "/openvpnList"//获取列表 37 | const val OPENVPN_REMOVE = "/openvpnRemove"//删除配置信息 38 | 39 | const val FRP_LIST = "/frpList"//获取配置列表 40 | const val FRP_REMOVE = "/frpRemove"//删除配置 41 | const val FRP_ADD = "/frpAdd"//增加配置 42 | const val FRP_START = "/frpStart"//启动服务 43 | const val FRP_STOP = "/frpStop"//停止服务 44 | const val FRP_CONFIG = "/frpConfig"//获取服务详情 45 | const val FRP_EDIT = "/frpEdit"//修改服务 46 | 47 | const val APP_INSTALL = "/appInstall"//安装 48 | const val APP_UNINSTALL = "/appUninstall"//卸载 49 | const val INSTALL_INFO = "/appInstallInfo"//获取安装信息 50 | 51 | const val SMS_BY_CONTACT = "/smsByContact"//获取短信会话,获取最新一条短信 52 | const val SMS_LIST = "/smsList"//获取短信列表 53 | const val SEND_SMS = "/sendSms"//发送短信 54 | const val READ_SMS = "/readSms"//已读短信e 55 | const val DELETE_SMS = "/deleteSms"//删除短信 56 | const val DELETE_SMS_CONTACT = "/deleteSmsByContact"//删除短信会话 57 | const val DELETE_SMS_LIST = "/deleteSmsList"//根据id列表删除短信 58 | const val BLACK_CLIENT="/blackClient"//添加设备黑名单 59 | const val BLACK_CLIENT_REMOVE="/removeBlackClient"//删除设备黑名单 60 | const val LOG_LIST="/logList"//获取设备日志 61 | const val REFRESH_LIST_BY_ID="/refreshListById"//根据id获取设备最新日志 62 | const val MORE_LIST_BY_ID="/moreListById"//根据id获取设备最新日志 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/dialog/OnConfirmListener.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.dialog; 2 | 3 | public interface OnConfirmListener { 4 | void onConfirm(String msg); 5 | void onCancel(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http 2 | 3 | import io.reactivex.rxjava3.core.Observable 4 | import okhttp3.ResponseBody 5 | import retrofit2.http.GET 6 | import retrofit2.http.Streaming 7 | import retrofit2.http.Url 8 | 9 | interface ApiService { 10 | @Streaming 11 | @GET 12 | fun download(@Url url: String): Observable 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/HttpWebServer.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http 2 | 3 | import android.content.Context 4 | import com.jtun.router.config.Config 5 | import com.jtun.router.util.JLog 6 | import fi.iki.elonen.NanoHTTPD 7 | import java.io.BufferedReader 8 | import java.io.IOException 9 | import java.io.InputStreamReader 10 | 11 | 12 | class HttpWebServer(private val mainContext:Context) : NanoHTTPD(Config.HTTP_WEB_PORT) { 13 | fun startServer() { 14 | start(SOCKET_READ_TIMEOUT, false) 15 | JLog.r("HttpWebServer","Start HttpWebServer") 16 | } 17 | 18 | override fun serve(session: IHTTPSession?): Response { 19 | val uri = session!!.uri 20 | println("####MyWebServer:$uri") 21 | var filename = uri.substring(1) 22 | if (uri == "/") filename = "index.html" 23 | val mimetype: String 24 | if (filename.contains(".html") || filename.contains(".htm")) { 25 | mimetype = "text/html" 26 | } else if (filename.contains(".js")) { 27 | mimetype = "text/javascript" 28 | } else if (filename.contains(".css")) { 29 | mimetype = "text/css" 30 | } else { 31 | // filename = "index.html" 32 | // mimetype = "text/html" 33 | val inputStream = mainContext.assets.open("dist/$filename") 34 | return newFixedLengthResponse(Response.Status.OK,"application/octet-stream",inputStream, inputStream.available().toLong()) 35 | } 36 | 37 | var response: String? = "" 38 | var line: String? 39 | val reader: BufferedReader? 40 | try { 41 | reader = BufferedReader(InputStreamReader(mainContext.assets.open("dist/$filename"))) 42 | while ((reader.readLine().also { line = it }) != null) { 43 | response += line 44 | } 45 | reader.close() 46 | } catch (e: IOException) { 47 | e.printStackTrace() 48 | } 49 | return newFixedLengthResponse(Response.Status.OK, mimetype, response) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/download/DownLoadStateBean.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.download; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by goldze on 2017/5/11. 10 | */ 11 | 12 | public class DownLoadStateBean implements Serializable, Parcelable { 13 | long total; // 文件总大小 14 | long bytesLoaded; //已加载文件的大小 15 | String tag; // 多任务下载时的一个标记 16 | 17 | public DownLoadStateBean(long total, long bytesLoaded) { 18 | this.total = total; 19 | this.bytesLoaded = bytesLoaded; 20 | } 21 | 22 | public DownLoadStateBean(long total, long bytesLoaded, String tag) { 23 | this.total = total; 24 | this.bytesLoaded = bytesLoaded; 25 | this.tag = tag; 26 | } 27 | 28 | public long getTotal() { 29 | return total; 30 | } 31 | 32 | public void setTotal(long total) { 33 | this.total = total; 34 | } 35 | 36 | public long getBytesLoaded() { 37 | return bytesLoaded; 38 | } 39 | 40 | public void setBytesLoaded(long bytesLoaded) { 41 | this.bytesLoaded = bytesLoaded; 42 | } 43 | 44 | public String getTag() { 45 | return tag; 46 | } 47 | 48 | public void setTag(String tag) { 49 | this.tag = tag; 50 | } 51 | 52 | @Override 53 | public int describeContents() { 54 | return 0; 55 | } 56 | 57 | @Override 58 | public void writeToParcel(Parcel dest, int flags) { 59 | dest.writeLong(this.total); 60 | dest.writeLong(this.bytesLoaded); 61 | dest.writeString(this.tag); 62 | } 63 | 64 | protected DownLoadStateBean(Parcel in) { 65 | this.total = in.readLong(); 66 | this.bytesLoaded = in.readLong(); 67 | this.tag = in.readString(); 68 | } 69 | 70 | public static final Creator CREATOR = new Creator() { 71 | @Override 72 | public DownLoadStateBean createFromParcel(Parcel source) { 73 | return new DownLoadStateBean(source); 74 | } 75 | 76 | @Override 77 | public DownLoadStateBean[] newArray(int size) { 78 | return new DownLoadStateBean[size]; 79 | } 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/download/DownLoadSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.download; 2 | 3 | 4 | import io.reactivex.rxjava3.observers.DisposableObserver; 5 | 6 | /** 7 | * Created by goldze on 2017/5/11. 8 | */ 9 | 10 | public class DownLoadSubscriber extends DisposableObserver { 11 | private ProgressCallBack fileCallBack; 12 | 13 | public DownLoadSubscriber(ProgressCallBack fileCallBack) { 14 | this.fileCallBack = fileCallBack; 15 | } 16 | 17 | @Override 18 | public void onStart() { 19 | super.onStart(); 20 | if (fileCallBack != null) 21 | fileCallBack.onStart(); 22 | } 23 | 24 | @Override 25 | public void onComplete() { 26 | if (fileCallBack != null) 27 | fileCallBack.onCompleted(); 28 | } 29 | 30 | @Override 31 | public void onError(Throwable e) { 32 | if (fileCallBack != null) 33 | fileCallBack.onError(e); 34 | } 35 | 36 | @Override 37 | public void onNext(T t) { 38 | if (fileCallBack != null) 39 | fileCallBack.onSuccess(fileCallBack.getFilePath()); 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/download/DownloadApkCallBack.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.download; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.io.FileNotFoundException; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | import okhttp3.ResponseBody; 12 | 13 | public abstract class DownloadApkCallBack extends ProgressCallBack { 14 | 15 | public DownloadApkCallBack(String cacheDir, String destFileDir, String destFileName) { 16 | super(cacheDir, destFileDir, destFileName); 17 | } 18 | 19 | 20 | @Override 21 | public void saveFile(ResponseBody body) { 22 | InputStream is = null; 23 | byte[] buf = new byte[2048]; 24 | int len; 25 | FileOutputStream fos = null; 26 | try { 27 | is = body.byteStream(); 28 | File dir = new File(cacheDir); 29 | if (!dir.exists()) { 30 | dir.mkdirs(); 31 | } 32 | File file = new File(dir, destFileName); 33 | fos = new FileOutputStream(file); 34 | while ((len = is.read(buf)) != -1) { 35 | fos.write(buf, 0, len); 36 | } 37 | fos.flush(); 38 | setFilePath(file.getAbsolutePath()); 39 | unsubscribe(); 40 | //onCompleted(); 41 | } catch (FileNotFoundException e) { 42 | e.printStackTrace(); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } finally { 46 | try { 47 | if (is != null) is.close(); 48 | if (fos != null) fos.close(); 49 | } catch (IOException e) { 50 | Log.e("saveFile", e.getMessage()); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/download/ProgressResponseBody.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.download; 2 | 3 | 4 | import com.jtun.router.bus.RxBus; 5 | 6 | import java.io.IOException; 7 | 8 | import okhttp3.MediaType; 9 | import okhttp3.ResponseBody; 10 | import okio.Buffer; 11 | import okio.BufferedSource; 12 | import okio.ForwardingSource; 13 | import okio.Okio; 14 | import okio.Source; 15 | 16 | /** 17 | * Created by goldze on 2017/5/11. 18 | */ 19 | 20 | public class ProgressResponseBody extends ResponseBody { 21 | private ResponseBody responseBody; 22 | 23 | private BufferedSource bufferedSource; 24 | private String tag; 25 | 26 | public ProgressResponseBody(ResponseBody responseBody) { 27 | this.responseBody = responseBody; 28 | } 29 | 30 | public ProgressResponseBody(ResponseBody responseBody, String tag) { 31 | this.responseBody = responseBody; 32 | this.tag = tag; 33 | } 34 | 35 | @Override 36 | public MediaType contentType() { 37 | return responseBody.contentType(); 38 | } 39 | 40 | @Override 41 | public long contentLength() { 42 | return responseBody.contentLength(); 43 | } 44 | 45 | @Override 46 | public BufferedSource source() { 47 | if (bufferedSource == null) { 48 | bufferedSource = Okio.buffer(source(responseBody.source())); 49 | } 50 | return bufferedSource; 51 | } 52 | 53 | private Source source(Source source) { 54 | return new ForwardingSource(source) { 55 | long bytesReaded = 0; 56 | 57 | @Override 58 | public long read(Buffer sink, long byteCount) throws IOException { 59 | long bytesRead = super.read(sink, byteCount); 60 | bytesReaded += bytesRead == -1 ? 0 : bytesRead; 61 | //使用RxBus的方式,实时发送当前已读取(上传/下载)的字节数据 62 | RxBus.getDefault().post(new DownLoadStateBean(contentLength(), bytesReaded, tag)); 63 | return bytesRead; 64 | } 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/BaseInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | import java.util.Set; 6 | 7 | import okhttp3.Interceptor; 8 | import okhttp3.Request; 9 | import okhttp3.Response; 10 | 11 | /** 12 | * Created by goldze on 2017/5/10. 13 | */ 14 | public class BaseInterceptor implements Interceptor { 15 | private Map headers; 16 | 17 | public BaseInterceptor(Map headers) { 18 | this.headers = headers; 19 | } 20 | 21 | @Override 22 | public Response intercept(Chain chain) throws IOException { 23 | Request.Builder builder = chain.request() 24 | .newBuilder(); 25 | if (headers != null && headers.size() > 0) { 26 | Set keys = headers.keySet(); 27 | for (String headerKey : keys) { 28 | builder.addHeader(headerKey, headers.get(headerKey)).build(); 29 | } 30 | } 31 | //请求信息 32 | return chain.proceed(builder.build()); 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/CacheInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor; 2 | 3 | import android.content.Context; 4 | 5 | import com.jtun.router.http.NetworkUtil; 6 | 7 | import java.io.IOException; 8 | 9 | import okhttp3.CacheControl; 10 | import okhttp3.Interceptor; 11 | import okhttp3.Request; 12 | import okhttp3.Response; 13 | 14 | /** 15 | * Created by goldze on 2017/5/10. 16 | * 无网络状态下智能读取缓存的拦截器 17 | */ 18 | public class CacheInterceptor implements Interceptor { 19 | 20 | private Context context; 21 | 22 | public CacheInterceptor(Context context) { 23 | this.context = context; 24 | } 25 | 26 | @Override 27 | public Response intercept(Chain chain) throws IOException { 28 | Request request = chain.request(); 29 | if (NetworkUtil.isNetworkAvailable(context)) { 30 | Response response = chain.proceed(request); 31 | // read from cache for 60 s 32 | int maxAge = 60; 33 | return response.newBuilder() 34 | .removeHeader("Pragma") 35 | .removeHeader("Cache-Control") 36 | .header("Cache-Control", "public, max-age=" + maxAge) 37 | .build(); 38 | } else { 39 | //读取缓存信息 40 | request = request.newBuilder() 41 | .cacheControl(CacheControl.FORCE_CACHE) 42 | .build(); 43 | Response response = chain.proceed(request); 44 | //set cache times is 3 days 45 | int maxStale = 60 * 60 * 24 * 3; 46 | return response.newBuilder() 47 | .removeHeader("Pragma") 48 | .removeHeader("Cache-Control") 49 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) 50 | .build(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/ProgressInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor; 2 | 3 | 4 | import com.jtun.router.http.download.ProgressResponseBody; 5 | 6 | import java.io.IOException; 7 | 8 | import okhttp3.Interceptor; 9 | import okhttp3.Response; 10 | 11 | /** 12 | * Created by goldze on 2017/5/10. 13 | */ 14 | 15 | public class ProgressInterceptor implements Interceptor { 16 | 17 | @Override 18 | public Response intercept(Chain chain) throws IOException { 19 | String url = chain.request().url().url().toString(); 20 | Response originalResponse = chain.proceed(chain.request()); 21 | return originalResponse.newBuilder() 22 | .body(new ProgressResponseBody(originalResponse.body(),url)) 23 | .build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/logging/I.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor.logging; 2 | 3 | 4 | import java.util.logging.Level; 5 | 6 | import okhttp3.internal.platform.Platform; 7 | 8 | /** 9 | * @author ihsan on 10/02/2017. 10 | */ 11 | class I { 12 | 13 | protected I() { 14 | throw new UnsupportedOperationException(); 15 | } 16 | 17 | static void log(int type, String tag, String msg) { 18 | java.util.logging.Logger logger = java.util.logging.Logger.getLogger(tag); 19 | switch (type) { 20 | case Platform.INFO: 21 | logger.log(Level.INFO, msg); 22 | break; 23 | default: 24 | logger.log(Level.WARNING, msg); 25 | break; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/logging/Level.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor.logging; 2 | 3 | /** 4 | * @author ihsan on 21/02/2017. 5 | */ 6 | 7 | public enum Level { 8 | /** 9 | * No logs. 10 | */ 11 | NONE, 12 | /** 13 | *

Example: 14 | *

{@code
15 |      *  - URL
16 |      *  - Method
17 |      *  - Headers
18 |      *  - Body
19 |      * }
20 | */ 21 | BASIC, 22 | /** 23 | *

Example: 24 | *

{@code
25 |      *  - URL
26 |      *  - Method
27 |      *  - Headers
28 |      * }
29 | */ 30 | HEADERS, 31 | /** 32 | *

Example: 33 | *

{@code
34 |      *  - URL
35 |      *  - Method
36 |      *  - Body
37 |      * }
38 | */ 39 | BODY 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/interceptor/logging/Logger.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.interceptor.logging; 2 | 3 | import okhttp3.internal.platform.Platform; 4 | 5 | /** 6 | * @author ihsan on 11/07/2017. 7 | */ 8 | @SuppressWarnings({"WeakerAccess", "unused"}) 9 | public interface Logger { 10 | void log(int level, String tag, String msg); 11 | 12 | Logger DEFAULT = new Logger() { 13 | @Override 14 | public void log(int level, String tag, String message) { 15 | Platform.get().log(level, message, null); 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/request/BaseRequest.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.request 2 | 3 | data class BaseRequest (val cmd:Int) -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response 2 | 3 | import com.google.gson.Gson 4 | 5 | data class BaseResponse(val code :Int = 0, 6 | val message:String = "success", 7 | val data : Any? = Any()){ 8 | 9 | fun toJsonString():String{ 10 | return Gson().toJson(this) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/InternetInfo.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response; 2 | 3 | public class InternetInfo { 4 | private String iccid = "iccid"; 5 | private String imei = "imei"; 6 | private String operatorName; 7 | private String networkType; 8 | private int BAND; 9 | private int earfcn; 10 | private int RSRP; 11 | private int RSSI; 12 | private int RERQ; 13 | private int CellID; 14 | private int PCI; 15 | 16 | public String getIccid() { 17 | return iccid; 18 | } 19 | 20 | public void setIccid(String iccid) { 21 | this.iccid = iccid; 22 | } 23 | 24 | public String getImei() { 25 | return imei; 26 | } 27 | 28 | public void setImei(String imei) { 29 | this.imei = imei; 30 | } 31 | 32 | public String getOperatorName() { 33 | return operatorName; 34 | } 35 | 36 | public void setOperatorName(String operatorName) { 37 | this.operatorName = operatorName; 38 | } 39 | 40 | public String getNetworkType() { 41 | return networkType; 42 | } 43 | 44 | public void setNetworkType(String networkType) { 45 | this.networkType = networkType; 46 | } 47 | 48 | public int getBAND() { 49 | return BAND; 50 | } 51 | 52 | public void setBAND(int BAND) { 53 | this.BAND = BAND; 54 | } 55 | 56 | public int getEarfcn() { 57 | return earfcn; 58 | } 59 | 60 | public void setEarfcn(int earfcn) { 61 | this.earfcn = earfcn; 62 | } 63 | 64 | public int getRSRP() { 65 | return RSRP; 66 | } 67 | 68 | public void setRSRP(int RSRP) { 69 | this.RSRP = RSRP; 70 | } 71 | 72 | public int getRSSI() { 73 | return RSSI; 74 | } 75 | 76 | public void setRSSI(int RSSI) { 77 | this.RSSI = RSSI; 78 | } 79 | 80 | public int getRERQ() { 81 | return RERQ; 82 | } 83 | 84 | public void setRERQ(int RERQ) { 85 | this.RERQ = RERQ; 86 | } 87 | 88 | public int getCellID() { 89 | return CellID; 90 | } 91 | 92 | public void setCellID(int cellID) { 93 | CellID = cellID; 94 | } 95 | 96 | public int getPCI() { 97 | return PCI; 98 | } 99 | 100 | public void setPCI(int PCI) { 101 | this.PCI = PCI; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/NetSpeed.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response 2 | 3 | data class NetSpeed(val totalRxBytes:Long,val totalTxBytes:Long,val state:Int,val numClients:Int) -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/SocketDeviceInfo.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response; 2 | 3 | import com.jtun.router.http.ApConfig; 4 | import com.jtun.router.room.ClientConnected; 5 | 6 | import java.util.List; 7 | 8 | public class SocketDeviceInfo { 9 | private ApConfig apconfig; 10 | private DeviceInfo device; 11 | private List client; 12 | 13 | public ApConfig getApconfig() { 14 | return apconfig; 15 | } 16 | 17 | public void setApconfig(ApConfig apconfig) { 18 | this.apconfig = apconfig; 19 | } 20 | 21 | public DeviceInfo getDevice() { 22 | return device; 23 | } 24 | 25 | public void setDevice(DeviceInfo device) { 26 | this.device = device; 27 | } 28 | 29 | public List getClient() { 30 | return client; 31 | } 32 | 33 | public void setClient(List client) { 34 | this.client = client; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/UsedDataInfo.java: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response; 2 | 3 | import com.jtun.router.control.WifiApControl; 4 | 5 | public class UsedDataInfo { 6 | private long today; 7 | private long month; 8 | private long all; 9 | 10 | public long getToday() { 11 | return today; 12 | } 13 | 14 | public void setToday(long today) { 15 | this.today = today; 16 | } 17 | 18 | public long getMonth() { 19 | return month; 20 | } 21 | 22 | public void setMonth(long month) { 23 | this.month = month; 24 | } 25 | 26 | public long getAll() { 27 | return all; 28 | } 29 | 30 | public void setAll(long all) { 31 | this.all = all; 32 | } 33 | 34 | public void initData(){ 35 | today = WifiApControl.Companion.getInstance().getTodayUsed(); 36 | month = WifiApControl.Companion.getInstance().getMonthUsed(); 37 | all = WifiApControl.Companion.getInstance().getAllUsed(); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/frp/FrpConfig.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response.frp 2 | 3 | data class FrpConfig(val uid:String,val name:String,val cfg:String,val connecting:Boolean) 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/openvpn/ItemServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2024 Arne Schwabe 3 | * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt 4 | */ 5 | 6 | package com.jtun.router.http.response.openvpn 7 | 8 | data class ItemServer(val serverName:String,val port:String,val useUdp:Boolean,val enabled:Boolean,val timeout: Int) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/openvpn/OpenvpnItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2024 Arne Schwabe 3 | * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt 4 | */ 5 | 6 | package com.jtun.router.http.response.openvpn 7 | 8 | /** 9 | * open vpn 配置信息 10 | * uuid 唯一标识 11 | * name 名称 12 | * message 连接显示状态 13 | * active 是否连接 14 | */ 15 | data class OpenvpnItem (val uuid:String,val name:String,val message:String,val active:Boolean,val servers: List) -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/v2ray/Configs.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response.v2ray 2 | 3 | data class Configs(val selected:String?,val isRunning:Boolean, val list: List) 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/v2ray/EConfigType.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response.v2ray 2 | 3 | import com.jtun.router.config.Config 4 | 5 | 6 | enum class EConfigType(val value: Int, val protocolScheme: String) { 7 | VMESS(1, Config.VMESS), 8 | CUSTOM(2, Config.CUSTOM), 9 | SHADOWSOCKS(3, Config.SHADOWSOCKS), 10 | SOCKS(4, Config.SOCKS), 11 | VLESS(5, Config.VLESS), 12 | TROJAN(6, Config.TROJAN), 13 | WIREGUARD(7, Config.WIREGUARD); 14 | 15 | companion object { 16 | fun fromInt(value: Int) = values().firstOrNull { it.value == value } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/v2ray/ProfileItem.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response.v2ray 2 | 3 | data class ProfileItem( 4 | val configType: EConfigType, 5 | var subscriptionId: String = "", 6 | var remarks: String = "", 7 | var server: String?, 8 | var serverPort: Int?, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/http/response/v2ray/ServersCache.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.http.response.v2ray 2 | 3 | data class ServersCache( 4 | val guid: String, 5 | val profile: ProfileItem 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/DhcpWorkaround.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net 2 | 3 | import android.content.SharedPreferences 4 | import com.jtun.router.App.Companion.app 5 | import com.jtun.router.net.Routing.Companion.IP 6 | import com.jtun.router.root.RoutingCommands 7 | import com.jtun.router.util.RootSession 8 | import com.jtun.router.widget.SmartSnackbar 9 | import kotlinx.coroutines.CancellationException 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | import java.io.IOException 14 | 15 | /** 16 | * Assuming RULE_PRIORITY_VPN_OUTPUT_TO_LOCAL = 11000. 17 | * Normally this is used to forward packets from remote to local, but it works anyways. 18 | * It just needs to be before RULE_PRIORITY_SECURE_VPN = 12000. 19 | * It would be great if we can gain better understanding into why this is only needed on some of the devices but not 20 | * others. 21 | * 22 | * Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57 23 | */ 24 | object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener { 25 | private const val KEY_ENABLED = "service.dhcpWorkaround" 26 | 27 | init { 28 | app.pref.registerOnSharedPreferenceChangeListener(this) 29 | } 30 | 31 | val shouldEnable get() = app.pref.getBoolean(KEY_ENABLED, false) 32 | fun enable(enabled: Boolean) = GlobalScope.launch { 33 | val action = if (enabled) "add" else "del" 34 | try { 35 | RootSession.use { 36 | try { 37 | // ROUTE_TABLE_LOCAL_NETWORK: https://cs.android.com/android/platform/superproject/+/master:system/netd/server/RouteController.cpp;l=74;drc=b6dc40ac3d566d952d8445fc6ac796109c0cbc87 38 | it.exec("$IP rule $action iif lo uidrange 0-0 lookup 97 priority 11000") 39 | } catch (e: RoutingCommands.UnexpectedOutputException) { 40 | if (Routing.shouldSuppressIpError(e, enabled)) return@use 41 | Timber.w(IOException("Failed to tweak dhcp workaround rule", e)) 42 | SmartSnackbar.make(e).show() 43 | } 44 | } 45 | } catch (_: CancellationException) { 46 | } catch (e: Exception) { 47 | Timber.w(e) 48 | SmartSnackbar.make(e).show() 49 | } 50 | } 51 | 52 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 53 | if (key == KEY_ENABLED) enable(shouldEnable) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/InetAddressComparator.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net 2 | 3 | import java.net.InetAddress 4 | 5 | object InetAddressComparator : Comparator { 6 | override fun compare(o1: InetAddress?, o2: InetAddress?): Int { 7 | if (o1 == null && o2 == null) return 0 8 | val a1 = o1?.address 9 | val a2 = o2?.address 10 | val r = (a1?.size ?: 0).compareTo(a2?.size ?: 0) 11 | return if (r == 0) a1!!.zip(a2!!).map { (l, r) -> l - r }.find { it != 0 } ?: 0 else r 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/MacAddressCompat.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net 2 | 3 | import android.net.MacAddress 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | 7 | /** 8 | * This used to be a compat support class for [MacAddress]. 9 | * Now it is just a convenient class for backwards compatibility. 10 | */ 11 | @JvmInline 12 | value class MacAddressCompat(val addr: Long) { 13 | companion object { 14 | /** 15 | * The MacAddress zero MAC address. 16 | * 17 | * Not publicly exposed or treated specially since the OUI 00:00:00 is registered. 18 | */ 19 | val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0)) 20 | val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0)) 21 | 22 | fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply { 23 | order(ByteOrder.LITTLE_ENDIAN) 24 | put(toByteArray()) 25 | rewind() 26 | }.long 27 | } 28 | 29 | fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run { 30 | order(ByteOrder.LITTLE_ENDIAN) 31 | putLong(addr) 32 | array().take(6) 33 | }.toByteArray()) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/TetherOffloadManager.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net 2 | 3 | import android.provider.Settings 4 | import com.jtun.router.App.Companion.app 5 | import com.jtun.router.root.SettingsGlobalPut 6 | 7 | /** 8 | * It's hard to change tethering rules with Tethering hardware acceleration enabled for now. 9 | * 10 | * See also: 11 | * android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED 12 | * https://android.googlesource.com/platform/frameworks/base/+/android-8.1.0_r1/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java#45 13 | * https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp 14 | */ 15 | object TetherOffloadManager { 16 | private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled" 17 | val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0 18 | suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/dns/VpnProtectedSelectorManager.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.dns 2 | 3 | import android.annotation.SuppressLint 4 | import android.net.VpnService 5 | import io.ktor.network.selector.SelectInterest 6 | import io.ktor.network.selector.Selectable 7 | import io.ktor.network.selector.SelectorManager 8 | import timber.log.Timber 9 | import java.net.ProtocolFamily 10 | import java.nio.channels.spi.SelectorProvider 11 | 12 | class VpnProtectedSelectorManager(private val manager: SelectorManager) : SelectorProvider(), SelectorManager { 13 | companion object { 14 | @SuppressLint("StaticFieldLeak") 15 | private val protector = VpnService() 16 | } 17 | 18 | private fun checkProtect(success: Boolean) { 19 | if (!success) Timber.w(Exception("protect failed")) 20 | } 21 | 22 | override fun openDatagramChannel() = manager.provider.openDatagramChannel().apply { 23 | checkProtect(protector.protect(socket())) 24 | } 25 | override fun openDatagramChannel(family: ProtocolFamily?) = manager.provider.openDatagramChannel(family).apply { 26 | checkProtect(protector.protect(socket())) 27 | } 28 | override fun openPipe() = manager.provider.openPipe() 29 | override fun openSelector() = manager.provider.openSelector() 30 | override fun openServerSocketChannel() = manager.provider.openServerSocketChannel() 31 | override fun openSocketChannel() = manager.provider.openSocketChannel().apply { 32 | checkProtect(protector.protect(socket())) 33 | } 34 | 35 | override val coroutineContext get() = manager.coroutineContext 36 | override val provider get() = this 37 | override fun close() = manager.close() 38 | override fun notifyClosed(selectable: Selectable) = manager.notifyClosed(selectable) 39 | override suspend fun select(selectable: Selectable, interest: SelectInterest) = manager.select(selectable, interest) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/monitor/DefaultNetworkMonitor.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.monitor 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | import com.jtun.router.util.Services 9 | import com.jtun.router.util.globalNetworkRequestBuilder 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | 13 | object DefaultNetworkMonitor : UpstreamMonitor() { 14 | private var registered = false 15 | override var currentLinkProperties: LinkProperties? = null 16 | private set 17 | /** 18 | * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: 19 | * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e 20 | */ 21 | private val networkRequest = globalNetworkRequestBuilder().apply { 22 | addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 23 | addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 24 | }.build() 25 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 26 | override fun onAvailable(network: Network) { 27 | val properties = Services.connectivity.getLinkProperties(network) 28 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 29 | currentNetwork = network 30 | currentLinkProperties = properties 31 | callbacks.toList() 32 | } 33 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 34 | } 35 | 36 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 37 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 38 | currentNetwork = network 39 | currentLinkProperties = properties 40 | callbacks.toList() 41 | } 42 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 43 | } 44 | 45 | override fun onLost(network: Network) { 46 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 47 | currentNetwork = null 48 | currentLinkProperties = null 49 | callbacks.toList() 50 | } 51 | GlobalScope.launch { callbacks.forEach { it.onAvailable() } } 52 | } 53 | } 54 | 55 | override fun registerCallbackLocked(callback: Callback) { 56 | if (registered) { 57 | val currentLinkProperties = currentLinkProperties 58 | if (currentLinkProperties != null) GlobalScope.launch { 59 | callback.onAvailable(currentLinkProperties) 60 | } 61 | } else { 62 | if (Build.VERSION.SDK_INT >= 31) { 63 | Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback, 64 | Services.mainHandler) 65 | } else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler) 66 | registered = true 67 | } 68 | } 69 | 70 | override fun destroyLocked() { 71 | if (!registered) return 72 | Services.connectivity.unregisterNetworkCallback(networkCallback) 73 | registered = false 74 | currentLinkProperties = null 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/monitor/FallbackUpstreamMonitor.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.monitor 2 | 3 | import android.content.SharedPreferences 4 | import com.jtun.router.App.Companion.app 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | 8 | abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor() { 9 | companion object : SharedPreferences.OnSharedPreferenceChangeListener { 10 | const val KEY = "service.upstream.fallback" 11 | 12 | init { 13 | app.pref.registerOnSharedPreferenceChangeListener(this) 14 | } 15 | 16 | private fun generateMonitor(): UpstreamMonitor { 17 | val upstream = app.pref.getString(KEY, null) 18 | return if (upstream.isNullOrEmpty()) DefaultNetworkMonitor else InterfaceMonitor(upstream) 19 | } 20 | private var monitor = generateMonitor() 21 | val currentNetwork get() = monitor.currentNetwork 22 | 23 | fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } 24 | fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } 25 | 26 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 27 | if (key == KEY) GlobalScope.launch { // prevent callback called in main 28 | synchronized(this) { 29 | val old = monitor 30 | val callbacks = synchronized(old) { 31 | old.callbacks.toList().also { 32 | old.callbacks.clear() 33 | old.destroyLocked() 34 | } 35 | } 36 | val new = generateMonitor() 37 | monitor = new 38 | for (callback in callbacks) new.registerCallback(callback) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/monitor/UpstreamMonitor.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.monitor 2 | 3 | import android.content.SharedPreferences 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import com.jtun.router.App.Companion.app 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | 10 | abstract class UpstreamMonitor { 11 | companion object : SharedPreferences.OnSharedPreferenceChangeListener { 12 | const val KEY = "service.upstream" 13 | 14 | init { 15 | app.pref.registerOnSharedPreferenceChangeListener(this) 16 | } 17 | 18 | private fun generateMonitor(): UpstreamMonitor { 19 | val upstream = app.pref.getString(KEY, null) 20 | return if (upstream.isNullOrEmpty()) VpnMonitor else InterfaceMonitor(upstream) 21 | } 22 | private var monitor = generateMonitor() 23 | val currentNetwork get() = monitor.currentNetwork 24 | 25 | fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } 26 | fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } 27 | 28 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 29 | if (key == KEY) GlobalScope.launch { // prevent callback called in main 30 | synchronized(this) { 31 | val old = monitor 32 | val callbacks = synchronized(old) { 33 | old.callbacks.toList().also { 34 | old.callbacks.clear() 35 | old.destroyLocked() 36 | } 37 | } 38 | val new = generateMonitor() 39 | monitor = new 40 | for (callback in callbacks) new.registerCallback(callback) 41 | } 42 | } 43 | } 44 | } 45 | 46 | interface Callback { 47 | /** 48 | * Called if some possibly stacked interface is available 49 | */ 50 | fun onAvailable(properties: LinkProperties? = null) { } 51 | } 52 | 53 | val callbacks = mutableSetOf() 54 | var currentNetwork: Network? = null 55 | protected set 56 | protected abstract val currentLinkProperties: LinkProperties? 57 | protected abstract fun registerCallbackLocked(callback: Callback) 58 | abstract fun destroyLocked() 59 | 60 | fun registerCallback(callback: Callback) { 61 | synchronized(this) { 62 | if (callbacks.add(callback)) registerCallbackLocked(callback) 63 | } 64 | } 65 | fun unregisterCallback(callback: Callback) = synchronized(this) { 66 | if (callbacks.remove(callback) && callbacks.isEmpty()) destroyLocked() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/monitor/VpnMonitor.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.monitor 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import com.jtun.router.net.VpnFirewallManager 8 | import com.jtun.router.util.Services 9 | import com.jtun.router.util.globalNetworkRequestBuilder 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | 14 | object VpnMonitor : UpstreamMonitor() { 15 | private val request = globalNetworkRequestBuilder().apply { 16 | addTransportType(NetworkCapabilities.TRANSPORT_VPN) 17 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 18 | }.build() 19 | private var registered = false 20 | 21 | private val available = HashMap() 22 | override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] } 23 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 24 | private fun fireCallbacks(properties: LinkProperties?, callbacks: Iterable) = GlobalScope.launch { 25 | if (properties != null) VpnFirewallManager.excludeIfNeeded(this) 26 | callbacks.forEach { it.onAvailable(properties) } 27 | } 28 | 29 | override fun onAvailable(network: Network) { 30 | val properties = Services.connectivity.getLinkProperties(network) 31 | fireCallbacks(properties, synchronized(this@VpnMonitor) { 32 | available[network] = properties 33 | currentNetwork = network 34 | callbacks.toList() 35 | }) 36 | } 37 | 38 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 39 | fireCallbacks(properties, synchronized(this@VpnMonitor) { 40 | available[network] = properties 41 | if (currentNetwork == null) currentNetwork = network 42 | else if (currentNetwork != network) return 43 | callbacks.toList() 44 | }) 45 | } 46 | 47 | override fun onLost(network: Network) { 48 | var properties: LinkProperties? = null 49 | val callbacks = synchronized(this@VpnMonitor) { 50 | if (available.remove(network) == null || currentNetwork != network) return 51 | if (available.isNotEmpty()) { 52 | val next = available.entries.first() 53 | currentNetwork = next.key 54 | Timber.d("Switching to ${next.value} as VPN interface") 55 | properties = next.value 56 | } else currentNetwork = null 57 | callbacks.toList() 58 | } 59 | fireCallbacks(properties, callbacks) 60 | } 61 | } 62 | 63 | override fun registerCallbackLocked(callback: Callback) { 64 | if (registered) { 65 | val currentLinkProperties = currentLinkProperties 66 | if (currentLinkProperties != null) GlobalScope.launch { 67 | callback.onAvailable(currentLinkProperties) 68 | } 69 | } else { 70 | Services.registerNetworkCallback(request, networkCallback) 71 | registered = true 72 | } 73 | } 74 | 75 | override fun destroyLocked() { 76 | if (!registered) return 77 | Services.connectivity.unregisterNetworkCallback(networkCallback) 78 | registered = false 79 | available.clear() 80 | currentNetwork = null 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/wifi/SoftApCapability.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import com.jtun.router.util.LongConstantLookup 8 | import com.jtun.router.util.UnblockCentral 9 | import timber.log.Timber 10 | 11 | @JvmInline 12 | @RequiresApi(30) 13 | value class SoftApCapability(val inner: Parcelable) { 14 | companion object { 15 | val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") } 16 | private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") } 17 | private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) } 18 | @get:RequiresApi(31) 19 | private val getSupportedChannelList by lazy { 20 | clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java) 21 | } 22 | @get:RequiresApi(31) 23 | @get:TargetApi(33) 24 | private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) } 25 | 26 | @RequiresApi(31) 27 | const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L 28 | @RequiresApi(31) 29 | const val SOFTAP_FEATURE_BAND_5G_SUPPORTED = 64L 30 | @RequiresApi(31) 31 | const val SOFTAP_FEATURE_BAND_6G_SUPPORTED = 128L 32 | @RequiresApi(31) 33 | const val SOFTAP_FEATURE_BAND_60G_SUPPORTED = 256L 34 | val featureLookup by lazy { LongConstantLookup(clazz, "SOFTAP_FEATURE_") } 35 | } 36 | 37 | val maxSupportedClients get() = getMaxSupportedClients(inner) as Int 38 | val supportedFeatures: Long get() { 39 | var supportedFeatures = 0L 40 | var probe = 1L 41 | while (probe != 0L) { 42 | if (areFeaturesSupported(inner, probe) as Boolean) supportedFeatures = supportedFeatures or probe 43 | probe += probe 44 | } 45 | return supportedFeatures 46 | } 47 | fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray 48 | @get:RequiresApi(31) 49 | val countryCode: String? get() = try { 50 | getCountryCode(inner) as String? 51 | } catch (e: ReflectiveOperationException) { 52 | if (Build.VERSION.SDK_INT >= 33) Timber.w(e) 53 | null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/wifi/SoftApInfo.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.net.MacAddress 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import com.jtun.router.util.ConstantLookup 8 | import com.jtun.router.util.UnblockCentral 9 | import timber.log.Timber 10 | 11 | @JvmInline 12 | @RequiresApi(30) 13 | value class SoftApInfo(val inner: Parcelable) { 14 | companion object { 15 | val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") } 16 | private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") } 17 | private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") } 18 | @get:RequiresApi(31) 19 | private val getBssid by lazy { clazz.getDeclaredMethod("getBssid") } 20 | @get:RequiresApi(31) 21 | private val getWifiStandard by lazy { clazz.getDeclaredMethod("getWifiStandard") } 22 | @get:RequiresApi(31) 23 | private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) } 24 | @get:RequiresApi(31) 25 | private val getAutoShutdownTimeoutMillis by lazy { clazz.getDeclaredMethod("getAutoShutdownTimeoutMillis") } 26 | 27 | val channelWidthLookup = ConstantLookup("CHANNEL_WIDTH_") { clazz } 28 | } 29 | 30 | val frequency get() = getFrequency(inner) as Int 31 | val bandwidth get() = getBandwidth(inner) as Int 32 | @get:RequiresApi(31) 33 | val bssid get() = getBssid(inner) as MacAddress? 34 | @get:RequiresApi(31) 35 | val wifiStandard get() = getWifiStandard(inner) as Int 36 | @get:RequiresApi(31) 37 | val apInstanceIdentifier get() = try { 38 | getApInstanceIdentifier(inner) as? String 39 | } catch (e: ReflectiveOperationException) { 40 | Timber.w(e) 41 | null 42 | } 43 | @get:RequiresApi(31) 44 | val autoShutdownTimeoutMillis get() = getAutoShutdownTimeoutMillis(inner) as Long 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/wifi/VendorElements.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.wifi 2 | 3 | import android.net.wifi.ScanResult 4 | import androidx.annotation.RequiresApi 5 | import timber.log.Timber 6 | 7 | @RequiresApi(33) 8 | object VendorElements { 9 | fun serialize(input: List) = input.joinToString("\n") { element -> 10 | element.bytes.let { buffer -> 11 | StringBuilder().apply { 12 | @OptIn(ExperimentalStdlibApi::class) 13 | while (buffer.hasRemaining()) append(buffer.get().toHexString()) 14 | }.toString() 15 | }.also { 16 | if (element.id != 221 || element.idExt != 0 || it.isEmpty()) Timber.w(Exception( 17 | "Unexpected InformationElement ${element.id}, ${element.idExt}, $it")) 18 | } 19 | } 20 | 21 | fun deserialize(input: CharSequence?) = (input ?: "").split("\n").mapNotNull { line -> 22 | @OptIn(ExperimentalStdlibApi::class) 23 | if (line.isBlank()) null else ScanResult.InformationElement(221, 0, line.hexToByteArray()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/wifi/WifiClient.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.net.MacAddress 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import com.jtun.router.util.UnblockCentral 8 | import timber.log.Timber 9 | 10 | @JvmInline 11 | @RequiresApi(30) 12 | value class WifiClient(val inner: Parcelable) { 13 | companion object { 14 | val clazz by lazy { Class.forName("android.net.wifi.WifiClient") } 15 | private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") } 16 | @get:RequiresApi(31) 17 | private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) } 18 | } 19 | 20 | val macAddress get() = getMacAddress(inner) as MacAddress 21 | @get:RequiresApi(31) 22 | val apInstanceIdentifier get() = try { 23 | getApInstanceIdentifier(inner) as? String 24 | } catch (e: ReflectiveOperationException) { 25 | Timber.w(e) 26 | null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/net/wifi/WifiSsidCompat.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.net.wifi 2 | 3 | import android.net.wifi.WifiSsid 4 | import android.os.Parcelable 5 | import androidx.annotation.RequiresApi 6 | import kotlinx.parcelize.Parcelize 7 | import org.jetbrains.annotations.Contract 8 | import java.nio.ByteBuffer 9 | import java.nio.CharBuffer 10 | import java.nio.charset.Charset 11 | import java.nio.charset.CodingErrorAction 12 | 13 | @Parcelize 14 | data class WifiSsidCompat(val bytes: ByteArray) : Parcelable { 15 | companion object { 16 | private val hexTester = Regex("^(?:[0-9a-f]{2})*$", RegexOption.IGNORE_CASE) 17 | private val qrSanitizer = Regex("([\\\\\":;,])") 18 | 19 | @OptIn(ExperimentalStdlibApi::class) 20 | fun fromHex(hex: String?) = hex?.run { WifiSsidCompat(hexToByteArray()) } 21 | 22 | @Contract("null -> null; !null -> !null") 23 | fun fromUtf8Text(text: String?, truncate: Boolean = false) = text?.toByteArray()?.let { 24 | WifiSsidCompat(if (truncate && it.size > 32) it.sliceArray(0 until 32) else it) 25 | } 26 | 27 | fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" } 28 | 29 | @RequiresApi(33) 30 | fun WifiSsid.toCompat() = WifiSsidCompat(bytes) 31 | } 32 | 33 | init { 34 | require(bytes.size <= 32) { "${bytes.size} > 32" } 35 | } 36 | 37 | @RequiresApi(31) 38 | fun toPlatform() = WifiSsid.fromBytes(bytes) 39 | 40 | fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run { 41 | val result = charset.newDecoder().apply { 42 | onMalformedInput(CodingErrorAction.REPORT) 43 | onUnmappableCharacter(CodingErrorAction.REPORT) 44 | }.decode(ByteBuffer.wrap(bytes), this, true) 45 | if (result.isError) null else flip().toString() 46 | } 47 | @OptIn(ExperimentalStdlibApi::class) 48 | val hex get() = bytes.toHexString() 49 | 50 | fun toMeCard(): String { 51 | val utf8 = decode() ?: return hex 52 | return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8) 53 | } 54 | 55 | override fun toString() = String(bytes) 56 | 57 | override fun equals(other: Any?): Boolean { 58 | if (this === other) return true 59 | if (javaClass != other?.javaClass) return false 60 | other as WifiSsidCompat 61 | if (!bytes.contentEquals(other.bytes)) return false 62 | return true 63 | } 64 | 65 | override fun hashCode() = bytes.contentHashCode() 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/receiver/BootCompletedReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.receiver 2 | 3 | import android.app.ActivityManager 4 | import android.content.BroadcastReceiver 5 | import android.content.ComponentName 6 | import android.content.Context 7 | import android.content.Intent 8 | import com.jtun.router.util.ApkUtils 9 | import com.jtun.router.util.KLog 10 | 11 | class BootCompletedReceiver : BroadcastReceiver() { 12 | private fun startAct(context: Context, cn: ComponentName) { 13 | val packageName: String = ApkUtils.getTopPackageName(context) 14 | val isRun = isPackageRunning(context, cn.packageName) 15 | KLog.i(cn.packageName + " isRun : " + isRun) 16 | KLog.i("top package : $packageName") 17 | if (packageName == cn.packageName) { 18 | return 19 | } 20 | val intent = Intent(Intent.ACTION_MAIN) 21 | intent.addCategory(Intent.CATEGORY_LAUNCHER) 22 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 23 | 24 | try { 25 | intent.setComponent(cn) 26 | context.startActivity(intent) 27 | } catch (e: Exception) { 28 | KLog.i(e.message) 29 | } 30 | } 31 | 32 | /** 33 | * 根据报名返回该进程是否启动 34 | * @param context 上下文 35 | * @param packagename 包名 36 | * @return 37 | */ 38 | private fun isPackageRunning(context: Context, packagename: String?): Boolean { 39 | return findPIDbyPackageName(context, packagename) != -1 40 | } 41 | 42 | /** 43 | * 根据报名查找指定pid 44 | * @param context 上下文 45 | * @param packagename 包名 46 | * @return pid 47 | */ 48 | private fun findPIDbyPackageName(context: Context, packagename: String?): Int { 49 | val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 50 | var result = -1 51 | if (am != null) { 52 | for (pi in am.runningAppProcesses) { 53 | if (pi.processName.equals(packagename, ignoreCase = true)) { 54 | result = pi.pid 55 | } 56 | if (result != -1) break 57 | } 58 | } else { 59 | result = -1 60 | } 61 | 62 | return result 63 | } 64 | 65 | override fun onReceive(context: Context, intent: Intent?) { 66 | KLog.i("接收到开机广播") 67 | /** 68 | * 主程序 69 | */ 70 | // val pkgName = context.packageName 71 | // val clsName = "com.jtun.router" + ".MainActivity" 72 | // val cn = ComponentName(pkgName, clsName) 73 | // KLog.w(null, "ComponentName:$cn") 74 | // startAct(context, cn) 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.room.TypeConverters 7 | import com.jtun.router.App.Companion.app 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | 11 | @Database(entities = [ClientRecord::class,TrafficRecord::class,ClientConnected::class,AppInfo::class,Log::class], version = 6) 12 | @TypeConverters(Converters::class) 13 | abstract class AppDatabase : RoomDatabase() { 14 | companion object { 15 | const val DB_NAME = "app.db" 16 | 17 | val instance by lazy { 18 | Room.databaseBuilder(app.deviceStorage, AppDatabase::class.java, DB_NAME).apply { 19 | fallbackToDestructiveMigration() 20 | setQueryExecutor { GlobalScope.launch { it.run() } } 21 | }.build() 22 | } 23 | } 24 | 25 | abstract val clientRecordDao: ClientRecord.Dao 26 | abstract val trafficRecordDao: TrafficRecord.Dao 27 | abstract val clientDao: ClientDao 28 | abstract val appDao : AppInfo.Dao 29 | abstract val logDao : Log.Dao 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.PrimaryKey 7 | import androidx.room.Query 8 | import com.dolphin.localsocket.bean.PackageInfo 9 | import com.jtun.router.util.KLog 10 | 11 | @Entity 12 | data class AppInfo(@PrimaryKey 13 | val packageName:String, 14 | val canonicalName :String?, 15 | val versionCode:Long, 16 | val versionName : String, 17 | val name:String, 18 | val brief:String, 19 | var isRun :Boolean = true){ 20 | companion object{ 21 | fun AppInfo.toCompat() = PackageInfo(packageName,canonicalName,versionCode,versionName,name,brief,isRun) 22 | fun PackageInfo.toCompat() = AppInfo(packageName,canonicalName,versionCode,versionName,name,brief,isRun) 23 | } 24 | 25 | @androidx.room.Dao 26 | abstract class Dao { 27 | @Query("SELECT * FROM `AppInfo` WHERE `packageName` = :packageName") 28 | protected abstract suspend fun lookup(packageName: String): AppInfo? 29 | suspend fun lookupOrDefault(packageName: String) = lookup(packageName) 30 | 31 | @Insert(onConflict = OnConflictStrategy.REPLACE) 32 | protected abstract suspend fun updateInternal(value: AppInfo): Long 33 | suspend fun update(value: AppInfo) { 34 | val result = updateInternal(value) 35 | KLog.i("updateInternal result : $result") 36 | } 37 | 38 | @Query("SELECT * FROM `AppInfo`") 39 | protected abstract suspend fun selectList(): List 40 | suspend fun getList() = selectList() 41 | 42 | @Query("DELETE FROM `AppInfo` WHERE `packageName` = :packageName") 43 | protected abstract suspend fun delete(packageName: String) 44 | suspend fun deleteApp(packageName: String) { 45 | delete(packageName) 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/ClientConnected.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class ClientConnected(@PrimaryKey 8 | val mac: String,//mac 9 | var nickname: String = "",//名称 10 | var ipAddress : String = "",//ip地址 11 | var linkTime:Long = 0,//连接时间 12 | var allowInternet:Boolean = true, //是否允许连接网络 13 | var onLine : Boolean = false){ //是否在线 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/ClientDao.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import androidx.room.Insert 4 | import androidx.room.OnConflictStrategy 5 | import androidx.room.Query 6 | import androidx.room.Transaction 7 | import com.jtun.router.util.KLog 8 | 9 | @androidx.room.Dao 10 | abstract class ClientDao { 11 | @Query("SELECT * FROM `ClientConnected` WHERE `mac` = :mac") 12 | protected abstract suspend fun lookup(mac: String): ClientConnected? 13 | suspend fun lookupOrDefault(mac: String) = lookup(mac) ?: ClientConnected(mac) 14 | 15 | @Insert(onConflict = OnConflictStrategy.REPLACE) 16 | protected abstract suspend fun updateInternal(value: ClientConnected): Long 17 | suspend fun update(value: ClientConnected) { 18 | val result = updateInternal(value) 19 | KLog.i("updateInternal result : $result") 20 | } 21 | 22 | @Query("SELECT * FROM `ClientConnected`") 23 | protected abstract suspend fun selectList(): List 24 | suspend fun getList() = selectList() 25 | 26 | @Query("DELETE FROM `ClientConnected` WHERE `mac` = :mac") 27 | protected abstract suspend fun deleteClient(mac: String) 28 | suspend fun deleteClientMac(mac: String) { 29 | deleteClient(mac) 30 | } 31 | 32 | @Transaction 33 | open suspend fun upsert(mac: String, operation: suspend ClientConnected.() -> Unit) = lookupOrDefault(mac).apply { 34 | operation() 35 | update(this) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/ClientRecord.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import android.net.MacAddress 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.map 6 | import androidx.room.* 7 | import com.jtun.router.net.MacAddressCompat.Companion.toLong 8 | 9 | @Entity 10 | data class ClientRecord(@PrimaryKey 11 | val mac: MacAddress, 12 | var nickname: CharSequence = "", 13 | var blocked: Boolean = false, 14 | var macLookupPending: Boolean = true) { 15 | @androidx.room.Dao 16 | abstract class Dao { 17 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 18 | protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord? 19 | fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac) 20 | 21 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 22 | protected abstract suspend fun lookup(mac: MacAddress): ClientRecord? 23 | suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac) 24 | 25 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 26 | protected abstract fun lookupSync(mac: MacAddress): LiveData 27 | fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) } 28 | 29 | @Insert(onConflict = OnConflictStrategy.REPLACE) 30 | protected abstract suspend fun updateInternal(value: ClientRecord): Long 31 | suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac.toLong()) 32 | 33 | @Transaction 34 | open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault( 35 | mac).apply { 36 | operation() 37 | update(this) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import android.net.MacAddress 4 | import android.text.TextUtils 5 | import androidx.room.TypeConverter 6 | import be.mygod.librootkotlinx.useParcel 7 | import com.jtun.router.net.MacAddressCompat 8 | import com.jtun.router.net.MacAddressCompat.Companion.toLong 9 | import timber.log.Timber 10 | import java.net.InetAddress 11 | 12 | object Converters { 13 | @JvmStatic 14 | @TypeConverter 15 | fun persistCharSequence(cs: CharSequence) = useParcel { p -> 16 | TextUtils.writeToParcel(cs, p, 0) 17 | p.marshall() 18 | } 19 | 20 | @JvmStatic 21 | @TypeConverter 22 | fun unpersistCharSequence(data: ByteArray) = useParcel { p -> 23 | p.unmarshall(data, 0, data.size) 24 | p.setDataPosition(0) 25 | try { 26 | TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)!! 27 | } catch (e: RuntimeException) { 28 | Timber.w(e) 29 | "" 30 | } 31 | } 32 | 33 | @JvmStatic 34 | @TypeConverter 35 | fun persistMacAddress(address: MacAddress) = address.toLong() 36 | 37 | @JvmStatic 38 | @TypeConverter 39 | fun unpersistMacAddress(address: Long) = MacAddressCompat(address).toPlatform() 40 | 41 | @JvmStatic 42 | @TypeConverter 43 | fun persistInetAddress(address: InetAddress): ByteArray = address.address 44 | 45 | @JvmStatic 46 | @TypeConverter 47 | fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/Log.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.PrimaryKey 7 | import androidx.room.Query 8 | 9 | @Entity 10 | data class Log(val type:Int, 11 | val tag:String, 12 | val msg:String, 13 | val time:Long, 14 | @PrimaryKey(autoGenerate = true) 15 | val id:Long = 0,){ 16 | companion object{ 17 | const val MAX_ITEMS = 20000 18 | } 19 | @androidx.room.Dao 20 | abstract class Dao { 21 | @Insert(onConflict = OnConflictStrategy.REPLACE) 22 | protected abstract suspend fun updateInternal(value: Log): Long 23 | suspend fun update(value: Log) { 24 | //判断是否超过指定数量,如果超过先删除历史的 25 | deleteMaxLog(MAX_ITEMS) 26 | val result = updateInternal(value) 27 | } 28 | @Query("SELECT * FROM `Log` order by time desc limit :limit offset :offset") 29 | protected abstract suspend fun selectList(limit: Int, offset: Int): List 30 | suspend fun getList(limit: Int, offset: Int) = selectList(limit,offset) 31 | 32 | @Query("SELECT * FROM `Log` order by time desc") 33 | protected abstract suspend fun selectList(): List 34 | suspend fun getList() = selectList() 35 | 36 | @Query("SELECT * FROM `Log` WHERE type=:type order by time desc limit :limit offset :offset") 37 | protected abstract suspend fun selectList(type: Int, limit: Int, offset: Int): List 38 | suspend fun getList(type: Int, limit: Int, offset: Int) = selectList(type,limit,offset) 39 | 40 | @Query("SELECT * FROM `Log` WHERE type=:type AND id > :id order by time desc limit :limit") 41 | abstract suspend fun refreshListById(type: Int, limit: Int, id: Long): List 42 | 43 | @Query("SELECT * FROM `Log` WHERE type=:type AND id < :id order by time desc limit :limit") 44 | abstract suspend fun moreListById(type: Int, limit: Int, id: Long): List 45 | 46 | /** 47 | * 删除超过最大数量的日志 48 | */ 49 | @Query("delete from `Log` where (select count(id) from `Log`) > :max and id in (select id from `Log` order by time desc limit (select count(id) from `Log`) offset :max )") 50 | protected abstract suspend fun deleteMaxLog(max:Int) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/room/TrafficRecord.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.room 2 | 3 | import android.net.MacAddress 4 | import android.os.Parcelable 5 | import androidx.room.* 6 | import kotlinx.parcelize.Parcelize 7 | import java.net.InetAddress 8 | 9 | @Entity(foreignKeys = [ForeignKey(entity = TrafficRecord::class, parentColumns = ["id"], childColumns = ["previousId"], 10 | onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)], 11 | indices = [Index(value = ["previousId"], unique = true)]) 12 | data class TrafficRecord( 13 | /** 14 | * Setting id = null should only be used when a new row is created and not yet inserted into the database. 15 | * 16 | * https://www.sqlite.org/lang_createtable.html#primkeyconst: 17 | * > Unless the column is an INTEGER PRIMARY KEY or the table is a WITHOUT ROWID table or the column is declared 18 | * > NOT NULL, SQLite allows NULL values in a PRIMARY KEY column. 19 | */ 20 | @PrimaryKey(autoGenerate = true) 21 | var id: Long? = null, 22 | val timestamp: Long = System.currentTimeMillis(), 23 | /** 24 | * Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord. 25 | */ 26 | val mac: MacAddress, 27 | /** 28 | * For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case. 29 | */ 30 | val ip: InetAddress, 31 | @Deprecated("This field is no longer used.") 32 | val upstream: String? = null, 33 | val downstream: String, 34 | var sentPackets: Long = 0, 35 | var sentBytes: Long = 0, 36 | var receivedPackets: Long = 0, 37 | var receivedBytes: Long = 0, 38 | /** 39 | * ID for the previous traffic record. 40 | */ 41 | val previousId: Long? = null) { 42 | @androidx.room.Dao 43 | abstract class Dao { 44 | @Insert 45 | protected abstract fun insertInternal(value: TrafficRecord): Long 46 | fun insert(value: TrafficRecord) { 47 | check(value.id == null) 48 | value.id = insertInternal(value) 49 | } 50 | 51 | @Query(""" 52 | SELECT MIN(TrafficRecord.timestamp) AS timestamp, 53 | COUNT(TrafficRecord.id) AS count, 54 | SUM(TrafficRecord.sentPackets) AS sentPackets, 55 | SUM(TrafficRecord.sentBytes) AS sentBytes, 56 | SUM(TrafficRecord.receivedPackets) AS receivedPackets, 57 | SUM(TrafficRecord.receivedBytes) AS receivedBytes 58 | FROM TrafficRecord LEFT JOIN TrafficRecord AS Next ON TrafficRecord.id = Next.previousId 59 | /* We only want to find the last record for each chain so that we don't double count */ 60 | WHERE TrafficRecord.mac = :mac AND Next.id IS NULL 61 | """) 62 | abstract suspend fun queryStats(mac: MacAddress): ClientStats 63 | } 64 | } 65 | 66 | @Parcelize 67 | data class ClientStats( 68 | val timestamp: Long = 0, 69 | val count: Long = 0, 70 | val sentPackets: Long = 0, 71 | val sentBytes: Long = 0, 72 | val receivedPackets: Long = 0, 73 | val receivedBytes: Long = 0 74 | ) : Parcelable 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/root/Jni.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.root 2 | 3 | object Jni { 4 | init { 5 | System.loadLibrary("vpnhotspot") 6 | } 7 | external fun removeUidInterfaceRules(path: String?, uid: Int, rules: Long): Boolean 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/root/LocalOnlyHotspotCallbacks.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.root 2 | 3 | import android.net.wifi.SoftApConfiguration 4 | import android.os.Parcelable 5 | import androidx.annotation.RequiresApi 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @RequiresApi(30) 9 | sealed class LocalOnlyHotspotCallbacks : Parcelable { 10 | @Parcelize 11 | data class OnStarted(val config: SoftApConfiguration) : LocalOnlyHotspotCallbacks() 12 | @Parcelize 13 | class OnStopped : LocalOnlyHotspotCallbacks() { 14 | override fun equals(other: Any?) = other is OnStopped 15 | override fun hashCode() = 0x80acd3ca.toInt() 16 | } 17 | @Parcelize 18 | data class OnFailed(val reason: Int) : LocalOnlyHotspotCallbacks() 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/root/RootManager.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.root 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Parcelable 5 | import android.util.Log 6 | import be.mygod.librootkotlinx.* 7 | import com.jtun.router.App.Companion.app 8 | import com.jtun.router.util.Services 9 | import com.jtun.router.util.UnblockCentral 10 | import kotlinx.parcelize.Parcelize 11 | import timber.log.Timber 12 | 13 | object RootManager : RootSession(), Logger { 14 | @Parcelize 15 | class RootInit : RootCommandNoResult { 16 | override suspend fun execute(): Parcelable? { 17 | Timber.plant(object : Timber.DebugTree() { 18 | @SuppressLint("LogNotTimber") 19 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 20 | if (priority >= Log.WARN) { 21 | System.err.println("$priority/$tag: $message") 22 | t?.printStackTrace() 23 | } 24 | if (t == null) { 25 | Log.println(priority, tag, message) 26 | } else { 27 | Log.println(priority, tag, message) 28 | Log.d(tag, message, t) 29 | if (priority >= Log.WARN) t.printStackTrace(System.err) 30 | } 31 | } 32 | }) 33 | Logger.me = RootManager 34 | Services.init { systemContext } 35 | UnblockCentral.needInit = false 36 | return null 37 | } 38 | } 39 | 40 | override fun d(m: String?, t: Throwable?) = Timber.d(t, m) 41 | override fun e(m: String?, t: Throwable?) = Timber.e(t, m) 42 | override fun i(m: String?, t: Throwable?) = Timber.i(t, m) 43 | override fun w(m: String?, t: Throwable?) = Timber.w(t, m) 44 | 45 | override suspend fun initServer(server: RootServer) { 46 | Logger.me = this 47 | AppProcess.shouldRelocateHeuristics.let { 48 | server.init(app, it) 49 | } 50 | server.execute(RootInit()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/root/RoutingCommands.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.root 2 | 3 | import android.os.Parcelable 4 | import be.mygod.librootkotlinx.RootCommand 5 | import be.mygod.librootkotlinx.RootCommandNoResult 6 | import com.jtun.router.net.Routing 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.coroutineScope 10 | import kotlinx.coroutines.withContext 11 | import kotlinx.parcelize.Parcelize 12 | import timber.log.Timber 13 | 14 | object RoutingCommands { 15 | 16 | @Parcelize 17 | class StopApp(val packageName:String) : RootCommandNoResult { 18 | override suspend fun execute() = withContext(Dispatchers.IO) { 19 | val process = ProcessBuilder("sh").fixPath(true).start() 20 | process.outputStream.bufferedWriter().appendLine("am force-stop $packageName") 21 | when (val code = process.waitFor()) { 22 | 0 -> { } 23 | else -> Timber.w("Unexpected exit code $code") 24 | } 25 | check(process.waitFor() == 0) 26 | null 27 | } 28 | } 29 | @Parcelize 30 | class Clean : RootCommandNoResult { 31 | override suspend fun execute() = withContext(Dispatchers.IO) { 32 | val process = ProcessBuilder("sh").fixPath(true).start() 33 | process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands) 34 | when (val code = process.waitFor()) { 35 | 0 -> { } 36 | else -> Timber.w("Unexpected exit code $code") 37 | } 38 | check(process.waitFor() == 0) 39 | null 40 | } 41 | } 42 | 43 | class UnexpectedOutputException(msg: String, val result: ProcessResult) : RuntimeException(msg) 44 | 45 | @Parcelize 46 | data class ProcessResult(val exit: Int, val out: String, val err: String) : Parcelable { 47 | fun message(command: List, out: Boolean = this.out.isNotEmpty(), 48 | err: Boolean = this.err.isNotEmpty()): String? { 49 | val msg = StringBuilder("${command.joinToString(" ")} exited with $exit") 50 | if (out) msg.append("\n${this.out}") 51 | if (err) msg.append("\n=== stderr ===\n${this.err}") 52 | return if (exit != 0 || out || err) msg.toString() else null 53 | } 54 | 55 | fun check(command: List, out: Boolean = this.out.isNotEmpty(), 56 | err: Boolean = this.err.isNotEmpty()) = message(command, out, err)?.let { msg -> 57 | throw UnexpectedOutputException(msg, this) 58 | } 59 | } 60 | 61 | @Parcelize 62 | data class Process(val command: List, private val redirect: Boolean = false) : RootCommand { 63 | @Suppress("BlockingMethodInNonBlockingContext") 64 | override suspend fun execute() = withContext(Dispatchers.IO) { 65 | val process = ProcessBuilder(command).fixPath(redirect).start() 66 | coroutineScope { 67 | val output = async { process.inputStream.bufferedReader().readText() } 68 | val error = async { if (redirect) "" else process.errorStream.bufferedReader().readText() } 69 | ProcessResult(process.waitFor(), output.await(), error.await()) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/root/TetheringCommands.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.root 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.RequiresApi 5 | import be.mygod.librootkotlinx.RootCommandChannel 6 | import com.jtun.router.net.TetheringManager 7 | import kotlinx.coroutines.CompletableDeferred 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.channels.ClosedSendChannelException 10 | import kotlinx.coroutines.channels.onClosed 11 | import kotlinx.coroutines.channels.onFailure 12 | import kotlinx.coroutines.channels.produce 13 | import kotlinx.coroutines.launch 14 | import kotlinx.parcelize.Parcelize 15 | 16 | object TetheringCommands { 17 | /** 18 | * This is the only command supported since other callbacks do not require signature permissions. 19 | */ 20 | @Parcelize 21 | data class OnClientsChanged(val clients: List) : Parcelable { 22 | fun dispatch(callback: TetheringManager.TetheringEventCallback) = callback.onClientsChanged(clients) 23 | } 24 | 25 | @Parcelize 26 | @RequiresApi(30) 27 | class RegisterTetheringEventCallback : RootCommandChannel { 28 | override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) { 29 | val finish = CompletableDeferred() 30 | val callback = object : TetheringManager.TetheringEventCallback { 31 | private fun push(parcel: OnClientsChanged) { 32 | trySend(parcel).onClosed { 33 | finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally")) 34 | return 35 | }.onFailure { throw it!! } 36 | } 37 | 38 | override fun onClientsChanged(clients: Collection) = 39 | push(OnClientsChanged(clients.toList())) 40 | } 41 | TetheringManager.registerTetheringEventCallback(callback) { 42 | scope.launch { 43 | try { 44 | it.run() 45 | } catch (e: Throwable) { 46 | finish.completeExceptionally(e) 47 | } 48 | } 49 | } 50 | try { 51 | finish.await() 52 | } finally { 53 | TetheringManager.unregisterTetheringEventCallback(callback) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/sms/SMSConversation.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.sms 2 | 3 | data class SMSConversation(val address:String,//发送人地址 4 | val lastMessage:Sms, //最后一条信息 5 | val threadId:Int)//会话id 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/sms/Sms.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.sms 2 | 3 | data class Sms(val id:Int, 4 | val address:String,//发件人地址 5 | val person:String?,//如果发件人在通讯录中则为具体姓名,陌生人为null 6 | val body :String?,//短信具体内容 7 | val data:String,//时间戳 8 | val type:Int, //短信类型1是接收到的,2是已发出 9 | val read:Int = 1)//1表示已读 0为未读 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/tasker/StateAction.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.tasker 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.jtun.router.TetheringService 9 | import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoInput 10 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoInput 11 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 12 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 13 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultErrorWithOutput 14 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess 15 | 16 | class GetStateConfig : AppCompatActivity(), TaskerPluginConfigNoInput { 17 | override val context: Context 18 | get() = this 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | GetStateHelper(this).apply { 23 | onCreate() 24 | finishForTasker() 25 | } 26 | } 27 | } 28 | 29 | class GetStateHelper(config: GetStateConfig) : TaskerPluginConfigHelperNoInput(config) { 30 | override val outputClass: Class = TetheringState::class.java 31 | override val runnerClass: Class = GetStateRunner::class.java 32 | } 33 | 34 | class GetStateRunner : TaskerPluginRunnerActionNoInput() { 35 | override fun run( 36 | context: Context, 37 | input: TaskerInput, 38 | ) = if (context.checkCallingPermission(Manifest.permission.ACCESS_NETWORK_STATE) == 39 | PackageManager.PERMISSION_GRANTED) { 40 | TaskerPluginResultSucess(TetheringState(TetheringService.activeTetherTypes)) 41 | } else TaskerPluginResultErrorWithOutput(SecurityException("Need ACCESS_NETWORK_STATE permission")) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/tasker/TaskerEvents.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.tasker 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.jtun.router.TetheringService 9 | import com.joaomgcd.taskerpluginlibrary.condition.TaskerPluginRunnerConditionEvent 10 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 11 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper 12 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 13 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 14 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionSatisfied 15 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionUnknown 16 | import timber.log.Timber 17 | 18 | class TetheringEventConfig : AppCompatActivity(), TaskerPluginConfigNoInput { 19 | override val context: Context 20 | get() = this 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | TetheringEventHelper(this).apply { 25 | onCreate() 26 | finishForTasker() 27 | } 28 | } 29 | } 30 | 31 | class TetheringEventHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { 32 | override val runnerClass: Class = TetheringEventRunner::class.java 33 | override val inputClass: Class = Unit::class.java 34 | override val outputClass: Class = TetheringState::class.java 35 | } 36 | 37 | class TetheringEventRunner : TaskerPluginRunnerConditionEvent() { 38 | override fun getSatisfiedCondition( 39 | context: Context, 40 | input: TaskerInput, 41 | update: Unit?, 42 | ) = if (context.checkCallingPermission(Manifest.permission.ACCESS_NETWORK_STATE) == 43 | PackageManager.PERMISSION_GRANTED) { 44 | TaskerPluginResultConditionSatisfied( 45 | context = context, 46 | regular = TetheringState(TetheringService.activeTetherTypes), 47 | ) 48 | } else { 49 | Timber.w("TetheringEventRunner needs ACCESS_NETWORK_STATE permission") 50 | TaskerPluginResultConditionUnknown() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/tasker/TetheringState.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.tasker 2 | 3 | import com.jtun.router.net.TetherType 4 | import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject 5 | import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable 6 | 7 | @TaskerOutputObject 8 | class TetheringState( 9 | @get:TaskerOutputVariable("wifi") 10 | val wifi: Boolean = false, 11 | @get:TaskerOutputVariable("bluetooth") 12 | val bluetooth: Boolean = false, 13 | @get:TaskerOutputVariable("usb") 14 | val usb: Boolean = false, 15 | @get:TaskerOutputVariable("ethernet") 16 | val ethernet: Boolean = false, 17 | ) { 18 | companion object { 19 | operator fun invoke(types: Set): TetheringState { 20 | return TetheringState( 21 | wifi = types.contains(TetherType.WIFI), 22 | bluetooth = types.contains(TetherType.BLUETOOTH), 23 | usb = types.contains(TetherType.USB) || types.contains(TetherType.NCM), 24 | ethernet = types.contains(TetherType.ETHERNET), 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/AppUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.app.Activity 4 | 5 | interface AppUpdate { 6 | class IgnoredException(cause: Throwable?) : RuntimeException(cause) 7 | 8 | val downloaded: Boolean? get() = null 9 | val message: String? get() = null 10 | val stalenessDays: Int? get() = null 11 | fun updateForResult(activity: Activity, requestCode: Int): Unit = error("Update not supported") 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/ConstantLookup.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.os.Build 4 | import androidx.collection.LongSparseArray 5 | import androidx.collection.SparseArrayCompat 6 | import com.jtun.router.App.Companion.app 7 | import com.jtun.router.R 8 | import timber.log.Timber 9 | 10 | class ConstantLookup(private val prefix: String, private val lookup29: Array, 11 | private val clazz: () -> Class<*>) { 12 | val lookup by lazy { 13 | SparseArrayCompat().apply { 14 | for (field in clazz().declaredFields) try { 15 | if (field?.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name) 16 | } catch (e: Exception) { 17 | Timber.w(e) 18 | } 19 | } 20 | } 21 | 22 | operator fun invoke(reason: Int, trimPrefix: Boolean = false): String { 23 | if (Build.VERSION.SDK_INT >= 30) try { 24 | lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } 25 | } catch (e: ReflectiveOperationException) { 26 | Timber.w(e) 27 | } 28 | return lookup29.getOrNull(reason)?.let { if (trimPrefix) it else prefix + it } 29 | ?: app.getString(R.string.failure_reason_unknown, reason) 30 | } 31 | } 32 | 33 | fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) = 34 | ConstantLookup(prefix, lookup29, clazz) 35 | inline fun ConstantLookup(prefix: String, vararg lookup29: String?) = 36 | ConstantLookup(prefix, lookup29) { T::class.java } 37 | 38 | class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { 39 | private val lookup = LongSparseArray().apply { 40 | for (field in clazz.declaredFields) try { 41 | if (field?.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name) 42 | } catch (e: Exception) { 43 | Timber.w(e) 44 | } 45 | } 46 | 47 | operator fun invoke(reason: Long, trimPrefix: Boolean = false): String { 48 | try { 49 | lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } 50 | } catch (e: ReflectiveOperationException) { 51 | Timber.w(e) 52 | } 53 | return app.getString(R.string.failure_reason_unknown, reason) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/CustomTabsUrlSpan.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.text.style.URLSpan 4 | import android.view.View 5 | 6 | class CustomTabsUrlSpan(url: String) : URLSpan(url) { 7 | override fun onClick(widget: View) = widget.context.launchUrl(url) 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/DeviceStorageApp.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.ComponentCallbacks 6 | import android.content.Context 7 | import android.content.res.Configuration 8 | 9 | @SuppressLint("MissingSuperCall", "Registered") 10 | class DeviceStorageApp(private val app: Application) : Application() { 11 | init { 12 | attachBaseContext(app.createDeviceProtectedStorageContext()) 13 | } 14 | 15 | /** 16 | * Thou shalt not get the REAL underlying application context which would no longer be operating under device 17 | * protected storage. 18 | */ 19 | override fun getApplicationContext(): Context = this 20 | 21 | override fun onCreate() = app.onCreate() 22 | override fun onTerminate() = app.onTerminate() 23 | override fun onConfigurationChanged(newConfig: Configuration) = app.onConfigurationChanged(newConfig) 24 | override fun onLowMemory() = app.onLowMemory() 25 | override fun onTrimMemory(level: Int) = app.onTrimMemory(level) 26 | override fun registerComponentCallbacks(callback: ComponentCallbacks?) = app.registerComponentCallbacks(callback) 27 | override fun unregisterComponentCallbacks(callback: ComponentCallbacks?) = 28 | app.unregisterComponentCallbacks(callback) 29 | override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) = 30 | app.registerActivityLifecycleCallbacks(callback) 31 | override fun unregisterActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) = 32 | app.unregisterActivityLifecycleCallbacks(callback) 33 | override fun registerOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) = 34 | app.registerOnProvideAssistDataListener(callback) 35 | override fun unregisterOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) = 36 | app.unregisterOnProvideAssistDataListener(callback) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/Events.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | /** 6 | * These class are based off https://github.com/1blustone/kotlin-events. 7 | */ 8 | open class Event0 : ConcurrentHashMap Unit>() { 9 | operator fun invoke() { 10 | for ((_, handler) in this) handler() 11 | } 12 | } 13 | 14 | class StickyEvent0 : Event0() { 15 | override fun put(key: Any, value: () -> Unit): (() -> Unit)? = 16 | super.put(key, value).also { if (it == null) value() } 17 | } 18 | 19 | open class Event1 : ConcurrentHashMap Unit>() { 20 | operator fun invoke(arg: T) { 21 | for ((_, handler) in this) handler(arg) 22 | } 23 | } 24 | 25 | class StickyEvent1(private val fire: () -> T) : Event1() { 26 | override fun put(key: Any, value: (T) -> Unit): ((T) -> Unit)? = 27 | super.put(key, value).also { if (it == null) value(fire()) } 28 | } 29 | 30 | open class Event2 : ConcurrentHashMap Unit>() { 31 | operator fun invoke(arg1: T1, arg2: T2) { 32 | for ((_, handler) in this) handler(arg1, arg2) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/FileHelper.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.content.Context 4 | import android.text.TextUtils 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.FileOutputStream 8 | 9 | object FileHelper { 10 | var cachePath : String = "" 11 | var settingPath : String = "" 12 | 13 | fun init(context:Context){ 14 | val path = getDiskCacheDir(context) 15 | settingPath = "$path/setting" 16 | cachePath = "$path/cache" 17 | checkFile(settingPath) 18 | checkFile(cachePath) 19 | } 20 | private fun checkFile(path:String){ 21 | val file = File(path) 22 | if (!file.exists()) { 23 | file.mkdir() 24 | } 25 | } 26 | fun moveFile(srcFileName: String, destDir: String): Boolean { 27 | return moveFile(srcFileName, destDir, "") 28 | } 29 | fun getDiskCacheDir(context: Context): String { 30 | var cachePath = context.externalCacheDir 31 | if (cachePath == null) { 32 | cachePath = context.cacheDir 33 | } 34 | try { 35 | return cachePath!!.absolutePath 36 | } catch (e: Exception) { 37 | } 38 | return "" 39 | } 40 | /** 41 | * 移动文件 42 | * 43 | * @param srcFileName 源文件完整路径 44 | * @param destDir 目的目录完整路径 45 | * @param name 文件名 46 | * @return 文件移动成功返回true,否则返回false 47 | */ 48 | private fun moveFile(srcFileName: String, destDir: String, name: String?): Boolean { 49 | val srcFile = File(srcFileName) 50 | if (!srcFile.exists() || !srcFile.isFile) return false 51 | val destDirFile = File(destDir) 52 | if (!destDirFile.exists()) destDirFile.mkdirs() 53 | return srcFile.renameTo(File(destDir + File.separator + (if (TextUtils.isEmpty(name)) srcFile.name else name))) 54 | } 55 | fun getNameOfUrl(url: String): String { 56 | return getNameOfUrl(url, "") 57 | } 58 | 59 | fun getNameOfUrl(url: String, defaultName: String): String { 60 | var name = "" 61 | val pos = url.lastIndexOf('/') 62 | if (pos >= 0) name = url.substring(pos + 1) 63 | 64 | if (TextUtils.isEmpty(name)) name = defaultName 65 | 66 | return name 67 | } 68 | fun writeFile(fileName: String?, content: String): Boolean { 69 | var res = false 70 | try { 71 | val fout = FileOutputStream(fileName) 72 | fout.write(content.toByteArray(), 0, content.toByteArray().size) 73 | fout.flush() 74 | fout.close() 75 | res = true 76 | } catch (e: java.lang.Exception) { 77 | e.printStackTrace() 78 | } 79 | return res 80 | } 81 | fun readFile(fileName: String?): String { 82 | var res = "" 83 | try { 84 | val fin = FileInputStream(fileName) 85 | // FileInputStream fin = openFileInput(fileName); 86 | // 用这个就不行了,必须用FileInputStream 87 | val length = fin.available() 88 | val buffer = ByteArray(length) 89 | fin.read(buffer) 90 | res = String(buffer, charset("UTF-8")) 91 | fin.close() 92 | } catch (e: java.lang.Exception) { 93 | e.printStackTrace() 94 | } 95 | if (res.startsWith("\ufeff")) { 96 | res = res.substring(1) 97 | } 98 | return res 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/JLog.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import com.jtun.router.App 4 | import com.jtun.router.room.AppDatabase 5 | import com.jtun.router.room.Log 6 | import kotlinx.coroutines.launch 7 | 8 | /** 9 | * 保存日志到数据库 10 | */ 11 | object JLog { 12 | const val R = 1 //运行日志 13 | const val O = 2 //操作日志 14 | const val T = 3 //第三方应用日志 15 | const val U = 4 //升级日志 16 | const val E = 5 //错误日志 17 | 18 | fun log(type:Int,tag:String,msg:String){ 19 | val log = Log(type,tag,msg,System.currentTimeMillis()) 20 | App.app.applicationScope.launch { 21 | AppDatabase.instance.logDao.update(log) 22 | } 23 | } 24 | fun r(tag:String,msg:String){ 25 | log(R,tag,msg) 26 | } 27 | fun o(tag:String,msg:String){ 28 | log(O,tag,msg) 29 | } 30 | fun t(tag:String,msg:String){ 31 | log(T,tag,msg) 32 | } 33 | fun u(tag:String,msg:String){ 34 | log(U,tag,msg) 35 | } 36 | fun e(tag:String,msg:String){ 37 | log(E,tag,msg) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/KillableTileService.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ForegroundServiceStartNotAllowedException 5 | import android.app.PendingIntent 6 | import android.content.ComponentName 7 | import android.content.Intent 8 | import android.content.ServiceConnection 9 | import android.graphics.PixelFormat 10 | import android.os.Build 11 | import android.os.DeadObjectException 12 | import android.os.IBinder 13 | import android.service.quicksettings.Tile 14 | import android.service.quicksettings.TileService 15 | import android.view.View 16 | import android.view.WindowManager 17 | import androidx.core.view.doOnPreDraw 18 | import java.lang.ref.WeakReference 19 | 20 | abstract class KillableTileService : TileService(), ServiceConnection { 21 | protected var tapPending = false 22 | 23 | /** 24 | * Compat helper for setSubtitle. 25 | */ 26 | protected fun Tile.subtitle(value: CharSequence?) { 27 | if (Build.VERSION.SDK_INT >= 29) subtitle = value 28 | } 29 | 30 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 31 | if (tapPending) { 32 | tapPending = false 33 | onClick() 34 | } 35 | } 36 | 37 | override fun onBind(intent: Intent?) = try { 38 | super.onBind(intent) 39 | } catch (e: RuntimeException) { 40 | if (e.cause !is DeadObjectException) throw e 41 | null 42 | } 43 | 44 | protected fun runActivity(intent: Intent) = unlockAndRun { 45 | if (Build.VERSION.SDK_INT < 34) @Suppress("DEPRECATION") @SuppressLint("StartActivityAndCollapseDeprecated") { 46 | startActivityAndCollapse(intent) 47 | } else startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)) 48 | } 49 | fun dismiss() = runActivity(Intent(this, SelfDismissActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) 50 | @Suppress("LeakingThis") 51 | protected val dismissHandle = WeakReference(this) 52 | override fun onDestroy() { 53 | dismissHandle.clear() 54 | super.onDestroy() 55 | } 56 | 57 | // Workaround on U: https://github.com/zhanghai/MaterialFiles/commit/7a2b228dfef8e5080d4cc887208b1ac5458c160e 58 | protected fun startForegroundServiceCompat(service: Intent) { 59 | try { 60 | startForegroundService(service) 61 | } catch (e: ForegroundServiceStartNotAllowedException) { 62 | if (Build.VERSION.SDK_INT != 34) throw e 63 | val windowManager = getSystemService(WindowManager::class.java) 64 | val view = View(this) 65 | windowManager.addView(view, WindowManager.LayoutParams().apply { 66 | type = WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + 35 67 | format = PixelFormat.TRANSLUCENT 68 | token = UnblockCentral.TileService_mToken.get(this@KillableTileService) as IBinder? 69 | }) 70 | view.doOnPreDraw { 71 | view.post { 72 | view.invalidate() 73 | view.doOnPreDraw { 74 | try { 75 | startForegroundService(service) 76 | } finally { 77 | windowManager.removeView(view) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | typealias TileServiceDismissHandle = WeakReference 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/RangeInput.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | object RangeInput { 4 | fun toString(input: IntArray) = StringBuilder().apply { 5 | if (input.isEmpty()) return@apply 6 | input.sort() 7 | var pending: Int? = null 8 | var last = input[0] 9 | append(last) 10 | for (channel in input.asSequence().drop(1)) { 11 | if (channel == last + 1) pending = channel else { 12 | pending?.let { 13 | append('-') 14 | append(it) 15 | pending = null 16 | } 17 | append(",\u200b") // zero-width space to save space 18 | append(channel) 19 | } 20 | last = channel 21 | } 22 | pending?.let { 23 | append('-') 24 | append(it) 25 | } 26 | }.toString() 27 | fun toString(input: Set?) = input?.run { toString(toIntArray()) } 28 | 29 | fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf().apply { 30 | if (input == null) return@apply 31 | for (unit in input.split(',')) { 32 | if (unit.isBlank()) continue 33 | val blocks = unit.split('-', limit = 2).map { i -> 34 | i.trim { it == '\u200b' || it.isWhitespace() }.toInt() 35 | } 36 | require(blocks[0] in min..max) { "Out of range: ${blocks[0]}" } 37 | if (blocks.size == 2) { 38 | require(blocks[1] in min..max) { "Out of range: ${blocks[1]}" } 39 | addAll(blocks[0]..blocks[1]) 40 | } else add(blocks[0]) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/RootSession.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.util.Log 4 | import be.mygod.librootkotlinx.RootServer 5 | import com.jtun.router.root.RootManager 6 | import com.jtun.router.root.RoutingCommands 7 | import kotlinx.coroutines.runBlocking 8 | import timber.log.Timber 9 | import java.util.* 10 | import java.util.concurrent.locks.ReentrantLock 11 | import kotlin.concurrent.withLock 12 | 13 | class RootSession : AutoCloseable { 14 | companion object { 15 | private val monitor = ReentrantLock() 16 | 17 | fun use(operation: (RootSession) -> T) = monitor.withLock { operation(RootSession()) } 18 | fun beginTransaction(): Transaction { 19 | monitor.lock() 20 | try { 21 | return RootSession().Transaction() 22 | } catch (e: Exception) { 23 | monitor.unlock() 24 | throw e 25 | } 26 | } 27 | } 28 | 29 | private var server: RootServer? = runBlocking { RootManager.acquire() } 30 | override fun close() { 31 | server?.let { runBlocking { RootManager.release(it) } } 32 | server = null 33 | } 34 | 35 | /** 36 | * Don't care about the results, but still sync. 37 | */ 38 | fun submit(command: String) = execQuiet(command).message(listOf(command))?.let { Timber.v(it) } 39 | 40 | fun execQuiet(command: String, redirect: Boolean = false) = runBlocking { 41 | server!!.execute(RoutingCommands.Process(listOf("sh", "-c", command), redirect)) 42 | } 43 | fun exec(command: String) = execQuiet(command).check(listOf(command)) 44 | 45 | /** 46 | * This transaction is different from what you may have in mind since you can revert it after committing it. 47 | */ 48 | inner class Transaction { 49 | private val revertCommands = LinkedList() 50 | 51 | fun exec(command: String, revert: String? = null) = execQuiet(command, revert).check(listOf(command)) 52 | fun execQuiet(command: String, revert: String? = null): RoutingCommands.ProcessResult { 53 | Log.i("RootSession","command: $command ,revert : $revert") 54 | if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails 55 | return this@RootSession.execQuiet(command) 56 | } 57 | 58 | fun commit() = monitor.unlock() 59 | 60 | fun revert() { 61 | var locked = monitor.isHeldByCurrentThread 62 | try { 63 | if (revertCommands.isEmpty()) return 64 | val shell = if (locked) this@RootSession else { 65 | monitor.lock() 66 | locked = true 67 | RootSession() 68 | } 69 | revertCommands.forEach { shell.submit(it) } 70 | } catch (e: Exception) { // if revert fails, it should fail silently 71 | Timber.d(e) 72 | } finally { 73 | revertCommands.clear() 74 | if (locked) monitor.unlock() // commit 75 | } 76 | } 77 | 78 | fun safeguard(work: Transaction.() -> Unit) = try { 79 | work() 80 | commit() 81 | this 82 | } catch (e: Exception) { 83 | revert() 84 | throw e 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/SelfDismissActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | 6 | class SelfDismissActivity : Activity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | finish() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/ServiceForegroundConnector.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import androidx.fragment.app.Fragment 8 | import androidx.lifecycle.DefaultLifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * owner also needs to be Context/Fragment. 14 | */ 15 | class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection, 16 | private val clazz: KClass) : DefaultLifecycleObserver { 17 | init { 18 | owner.lifecycle.addObserver(this) 19 | } 20 | 21 | private val context get() = when (owner) { 22 | is Context -> owner 23 | is Fragment -> owner.requireContext() 24 | else -> throw UnsupportedOperationException("Unsupported owner") 25 | } 26 | 27 | override fun onStart(owner: LifecycleOwner) { 28 | val context = context 29 | context.bindService(Intent(context, clazz.java), connection, Context.BIND_AUTO_CREATE) 30 | } 31 | 32 | override fun onStop(owner: LifecycleOwner) = context.stopAndUnbind(connection) 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/Services.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkRequest 7 | import android.net.wifi.WifiManager 8 | import android.net.wifi.p2p.WifiP2pManager 9 | import android.os.Handler 10 | import android.os.Looper 11 | import androidx.core.content.getSystemService 12 | import timber.log.Timber 13 | 14 | object Services { 15 | private lateinit var contextInit: () -> Context 16 | val context by lazy { contextInit() } 17 | fun init(context: () -> Context) { 18 | contextInit = context 19 | } 20 | 21 | val mainHandler by lazy { Handler(Looper.getMainLooper()) } 22 | val connectivity by lazy { context.getSystemService()!! } 23 | val p2p by lazy { 24 | try { 25 | context.getSystemService() 26 | } catch (e: RuntimeException) { 27 | Timber.w(e) 28 | null 29 | } 30 | } 31 | val wifi by lazy { context.getSystemService()!! } 32 | 33 | val netd by lazy @SuppressLint("WrongConstant") { context.getSystemService("netd")!! } 34 | 35 | fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) = 36 | connectivity.registerNetworkCallback(request, networkCallback, mainHandler) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/TetheringUtil.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.content.Intent 4 | import com.jtun.router.App 5 | import com.jtun.router.TetheringService 6 | import com.jtun.router.control.WifiApControl 7 | 8 | object TetheringUtil { 9 | fun startTetheringService(){ 10 | App.app.startForegroundService( 11 | Intent(App.app, TetheringService::class.java) 12 | .putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(WifiApControl.getInstance().getActiveIFace()))) 13 | } 14 | fun stopTetheringService(){ 15 | App.app.stopService( 16 | Intent(App.app, TetheringService::class.java) 17 | .putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(WifiApControl.getInstance().getActiveIFace()))) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/util/UnblockCentral.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.net.MacAddress 5 | import android.net.wifi.SoftApConfiguration 6 | import android.net.wifi.p2p.WifiP2pConfig 7 | import android.service.quicksettings.TileService 8 | import androidx.annotation.RequiresApi 9 | import org.lsposed.hiddenapibypass.HiddenApiBypass 10 | 11 | /** 12 | * The central object for accessing all the useful blocked APIs. Thanks Google! 13 | * 14 | * Lazy cannot be used directly as it will create inner classes. 15 | */ 16 | @SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi", "SoonBlockedPrivateApi") 17 | object UnblockCentral { 18 | var needInit = true 19 | /** 20 | * Retrieve this property before doing dangerous shit. 21 | */ 22 | private val init by lazy { if (needInit) check(HiddenApiBypass.setHiddenApiExemptions("")) } 23 | 24 | @RequiresApi(33) 25 | fun getCountryCode(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getCountryCode") } 26 | 27 | @RequiresApi(33) 28 | fun setRandomizedMacAddress(clazz: Class<*>) = init.let { 29 | clazz.getDeclaredMethod("setRandomizedMacAddress", MacAddress::class.java) 30 | } 31 | 32 | @get:RequiresApi(31) 33 | val SoftApConfiguration_BAND_TYPES get() = init.let { 34 | SoftApConfiguration::class.java.getDeclaredField("BAND_TYPES").get(null) as IntArray 35 | } 36 | 37 | @RequiresApi(31) 38 | fun getApInstanceIdentifier(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getApInstanceIdentifier") } 39 | 40 | @get:RequiresApi(29) 41 | val WifiP2pConfig_Builder_mNetworkName get() = init.let { 42 | WifiP2pConfig.Builder::class.java.getDeclaredField("mNetworkName").apply { isAccessible = true } 43 | } 44 | 45 | val TileService_mToken get() = init.let { 46 | TileService::class.java.getDeclaredField("mToken").apply { isAccessible = true } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/widget/BaseWebListener.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.widget 2 | 3 | import android.graphics.Bitmap 4 | 5 | abstract class BaseWebListener : AdvancedWebView.Listener { 6 | override fun onPageStarted(url: String?, favicon: Bitmap?) { 7 | } 8 | 9 | override fun onPageFinished(url: String?) { 10 | } 11 | 12 | override fun onPageError(errorCode: Int, description: String?, failingUrl: String?) { 13 | } 14 | 15 | override fun onDownloadRequested( 16 | url: String?, 17 | suggestedFilename: String?, 18 | mimeType: String?, 19 | contentLength: Long, 20 | contentDisposition: String?, 21 | userAgent: String? 22 | ) { 23 | } 24 | 25 | override fun onExternalPageRequest(url: String?) { 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtun/router/widget/SmartSnackbar.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router.widget 2 | 3 | 4 | import android.annotation.SuppressLint 5 | import android.os.Looper 6 | import android.view.View 7 | import android.widget.Toast 8 | import androidx.annotation.StringRes 9 | import androidx.lifecycle.DefaultLifecycleObserver 10 | import androidx.lifecycle.LifecycleOwner 11 | import androidx.lifecycle.findViewTreeLifecycleOwner 12 | import com.jtun.router.App.Companion.app 13 | import com.jtun.router.util.readableMessage 14 | import com.google.android.material.snackbar.Snackbar 15 | import java.util.concurrent.atomic.AtomicReference 16 | 17 | sealed class SmartSnackbar { 18 | companion object { 19 | private val holder = AtomicReference() 20 | 21 | fun make(@StringRes text: Int): SmartSnackbar = make(app.getText(text)) 22 | fun make(text: CharSequence = ""): SmartSnackbar { 23 | val holder = holder.get() 24 | return if (holder == null) @SuppressLint("ShowToast") { 25 | if (Looper.myLooper() == null) Looper.prepare() 26 | ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG)) 27 | } else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG)) 28 | } 29 | fun make(e: Throwable) = make(e.readableMessage) 30 | } 31 | 32 | class Register(private val view: View) : DefaultLifecycleObserver { 33 | init { 34 | view.findViewTreeLifecycleOwner()!!.lifecycle.addObserver(this) 35 | } 36 | 37 | override fun onResume(owner: LifecycleOwner) = holder.set(view) 38 | override fun onPause(owner: LifecycleOwner) { 39 | holder.compareAndSet(view, null) 40 | } 41 | } 42 | 43 | abstract fun show() 44 | open fun action(@StringRes id: Int, listener: (View) -> Unit) { } 45 | open fun shortToast() = this 46 | } 47 | 48 | private class SnackbarWrapper(private val snackbar: Snackbar) : SmartSnackbar() { 49 | override fun show() = snackbar.show() 50 | 51 | override fun action(@StringRes id: Int, listener: (View) -> Unit) { 52 | snackbar.setAction(id, listener) 53 | } 54 | } 55 | 56 | private class ToastWrapper(private val toast: Toast) : SmartSnackbar() { 57 | override fun show() = toast.show() 58 | 59 | override fun shortToast() = apply { toast.duration = Toast.LENGTH_SHORT } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_save.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_settings_ethernet.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_settings_input_antenna.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_content_inbox.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_device_bluetooth.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_device_network_wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_device_usb.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_device_wifi_tethering.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_image_flash_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /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_quick_settings_tile_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_connect.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_download.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_reboot.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_upload.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 19 | 26 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/layout_item_index_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/layout_main_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_confirm_program.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 23 | 24 | 30 | 31 | 32 | 33 | 42 | 43 | 49 | 50 | 51 | 63 | 64 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/icon_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-hdpi/icon_close.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/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 | #F5F5F5 11 | #000000 12 | #4CAF50 13 | #2e7d32 14 | #999999 15 | #EAEBF6 16 | #f5f5f5 17 | #333333 18 | #02a7f0 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14sp 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/test/java/com/jtun/router/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.jtun.router 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @OptIn(ExperimentalStdlibApi::class) 14 | @Test 15 | fun addition_isCorrect() { 16 | val ts = 1733797894 17 | System.out.println("ts" + intToByteArray4(ts).toHexString()) 18 | } 19 | 20 | private fun intToByteArray4(num: Int): ByteArray { 21 | var byteArray = ByteArray(4) 22 | var highH = ((num shr 24) and 0xff).toByte() 23 | var highL = ((num shr 16) and 0xff).toByte() 24 | var LowH = ((num shr 8) and 0xff).toByte() 25 | var LowL = (num and 0xff).toByte() 26 | byteArray[0] = highH 27 | byteArray[1] = highL 28 | byteArray[2] = LowH 29 | byteArray[3] = LowL 30 | return byteArray 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.9.0" 4 | repositories { 5 | mavenCentral() 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.2.0' // Update this line 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | mavenCentral() 20 | google() 21 | jcenter() 22 | maven { url 'https://jitpack.io' } 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtun-coder/JtunRouter/7542269a1239a6dd6027a13dd362520a5c31384a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "RoutingService" 2 | include ':app' 3 | include (":localsocket") 4 | project(":localsocket").projectDir = new File("C:/Users/qlx/AndroidStudioProjects/LocalSocket") --------------------------------------------------------------------------------