├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feat_req.yml ├── actions │ ├── build-apk │ │ └── action.yml │ ├── gradle-setup │ │ └── action.yml │ ├── run-lints │ │ └── action.yml │ └── run-tests │ │ └── action.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── daily.yml │ └── release.yml ├── .gitignore ├── .idea └── icon.svg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── shadoe │ └── delta │ └── api │ ├── ACLDevice.kt │ ├── ConfigFlag.kt │ ├── LinkAddress.kt │ ├── MacAddress.kt │ ├── ShizukuStates.kt │ ├── SoftApAutoShutdownTimeout.kt │ ├── SoftApCapabilities.kt │ ├── SoftApConfiguration.kt │ ├── SoftApEnabledState.kt │ ├── SoftApRandomizationSetting.kt │ ├── SoftApSecurityType.kt │ ├── SoftApSpeedType.kt │ ├── SoftApStatus.kt │ └── TetheredClient.kt ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── dev │ │ └── shadoe │ │ └── delta │ │ └── settings │ │ └── components │ │ └── AutoShutdownFieldTest.kt │ ├── debug │ └── AndroidManifest.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── dev │ │ └── shadoe │ │ └── delta │ │ ├── Application.kt │ │ ├── MainActivity.kt │ │ ├── SoftApBroadcastReceiver.kt │ │ ├── SoftApTile.kt │ │ ├── common │ │ ├── Nav.kt │ │ ├── NavViewModel.kt │ │ ├── Routes.kt │ │ ├── SoftApScreen.kt │ │ ├── SoftApScreenDestinations.kt │ │ ├── SoftApScreenViewModel.kt │ │ ├── components │ │ │ ├── FadeInExpanded.kt │ │ │ └── FoldableWrapper.kt │ │ ├── shapes │ │ │ └── PolygonShape.kt │ │ └── test │ │ │ └── RecomposeHighlighter.kt │ │ ├── control │ │ ├── ControlScreen.kt │ │ ├── ControlViewModel.kt │ │ └── components │ │ │ ├── AppBarWithDebugAction.kt │ │ │ ├── BlockedClientComponent.kt │ │ │ ├── BlocklistComponent.kt │ │ │ ├── ConnectedClientComponent.kt │ │ │ ├── ConnectedClientsList.kt │ │ │ ├── PassphraseDisplay.kt │ │ │ ├── SoftApControl.kt │ │ │ ├── SoftApControlButton.kt │ │ │ └── SoftApControlViewModel.kt │ │ ├── crash │ │ ├── CrashHandlerActivity.kt │ │ └── CrashHandlerUtils.kt │ │ ├── debug │ │ ├── DebugScreen.kt │ │ └── DebugViewModel.kt │ │ ├── design │ │ ├── AppTheme.kt │ │ ├── Typography.kt │ │ └── VariableFontFamily.kt │ │ ├── settings │ │ ├── SettingsScreen.kt │ │ ├── SettingsViewModel.kt │ │ ├── UpdateResults.kt │ │ └── components │ │ │ ├── AutoShutdownField.kt │ │ │ ├── AutoShutdownTimeoutField.kt │ │ │ ├── FrequencyBandField.kt │ │ │ ├── HiddenHotspotField.kt │ │ │ ├── MacRandomizationField.kt │ │ │ ├── MaxClientLimitField.kt │ │ │ ├── PassphraseField.kt │ │ │ ├── PresetField.kt │ │ │ ├── PresetSaveDialog.kt │ │ │ ├── PresetSheet.kt │ │ │ ├── SecurityTypeField.kt │ │ │ ├── SoftApTileField.kt │ │ │ ├── SsidField.kt │ │ │ ├── TaskerIntegrationField.kt │ │ │ └── TaskerIntegrationInfo.kt │ │ └── setup │ │ ├── CrashHandlerSetupScreen.kt │ │ ├── FirstUseScreen.kt │ │ ├── ShizukuSetupScreen.kt │ │ ├── ShizukuSetupViewModel.kt │ │ └── components │ │ ├── ShizukuConnected.kt │ │ ├── ShizukuNotConnected.kt │ │ ├── ShizukuNotInstalled.kt │ │ └── ShizukuNotRunning.kt │ └── res │ ├── drawable │ ├── ic_launcher_foreground.xml │ ├── ic_qs_tile.xml │ └── shizuku_logo_mono.xml │ ├── font-v26 │ ├── nunitosans_italic_variable.ttf │ └── nunitosans_variable.ttf │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── resources.properties │ ├── values-af-rZA │ └── strings.xml │ ├── values-ar-rSA │ └── strings.xml │ ├── values-ca-rES │ └── strings.xml │ ├── values-cs-rCZ │ └── strings.xml │ ├── values-da-rDK │ └── strings.xml │ ├── values-de-rDE │ └── strings.xml │ ├── values-el-rGR │ └── strings.xml │ ├── values-es-rES │ └── strings.xml │ ├── values-fi-rFI │ └── strings.xml │ ├── values-fr-rFR │ └── strings.xml │ ├── values-hu-rHU │ └── strings.xml │ ├── values-it-rIT │ └── strings.xml │ ├── values-iw-rIL │ └── strings.xml │ ├── values-ja-rJP │ └── strings.xml │ ├── values-ko-rKR │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-nl-rNL │ └── strings.xml │ ├── values-no-rNO │ └── strings.xml │ ├── values-pl-rPL │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt-rPT │ └── strings.xml │ ├── values-ro-rRO │ └── strings.xml │ ├── values-ru-rRU │ └── strings.xml │ ├── values-sr-rSP │ └── strings.xml │ ├── values-sv-rSE │ └── strings.xml │ ├── values-tr-rTR │ └── strings.xml │ ├── values-uk-rUA │ └── strings.xml │ ├── values-vi-rVN │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── crowdin.yml ├── data ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro ├── schema │ └── dev.shadoe.delta.data.database.ConfigDB │ │ ├── 1.json │ │ └── 2.json └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── dev │ │ └── shadoe │ │ └── delta │ │ └── data │ │ ├── FlagsRepository.kt │ │ ├── MacAddressCacheRepository.kt │ │ ├── database │ │ ├── ConfigDB.kt │ │ ├── converters │ │ │ └── MacAddressConverter.kt │ │ ├── dao │ │ │ ├── FlagsDao.kt │ │ │ ├── HostInfoDao.kt │ │ │ └── PresetDao.kt │ │ └── models │ │ │ ├── Flag.kt │ │ │ ├── HostInfo.kt │ │ │ └── Preset.kt │ │ ├── modules │ │ ├── DaoModule.kt │ │ └── SystemServicesModule.kt │ │ ├── qualifiers │ │ ├── TetheringSystemService.kt │ │ └── WifiSystemService.kt │ │ ├── shizuku │ │ └── ShizukuRepository.kt │ │ └── softap │ │ ├── SoftApBlocklistManager.kt │ │ ├── SoftApController.kt │ │ ├── SoftApMonitor.kt │ │ ├── SoftApStateFacade.kt │ │ ├── SoftApStateListener.kt │ │ ├── SoftApStateStore.kt │ │ ├── callbacks │ │ ├── SoftApCallback.kt │ │ └── TetheringEventCallback.kt │ │ ├── internal │ │ ├── Extensions.kt │ │ ├── InternalState.kt │ │ ├── TetheringEventListener.kt │ │ └── Utils.kt │ │ └── validators │ │ ├── PassphraseValidator.kt │ │ └── SsidValidator.kt │ └── test │ └── kotlin │ └── dev │ └── shadoe │ └── delta │ └── data │ └── softap │ └── validators │ ├── PassphraseValidatorTest.kt │ └── SsidValidatorTest.kt ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 15.txt │ ├── 16.txt │ └── 17.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── short_description.txt ├── gradle.properties ├── gradle ├── build-logic │ ├── .gitignore │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── delta │ │ └── buildsrc │ │ ├── Commands.kt │ │ ├── SigningConfig.kt │ │ ├── VersionConfig.kt │ │ ├── delta.app.version.gradle.kts │ │ └── lint │ │ ├── delta.lint.java.gradle.kts │ │ ├── delta.lint.kotlin.gradle.kts │ │ ├── delta.lint.kts.gradle.kts │ │ └── delta.lint.text.gradle.kts ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts └── update_ver.sh ├── settings.gradle.kts └── system-api-stubs ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── aidl └── android │ └── net │ ├── IIntResultListener.aidl │ ├── TetherStatesParcel.aidl │ ├── TetheredClient.aidl │ ├── TetheringConfigurationParcel.aidl │ ├── TetheringInterface.aidl │ ├── TetheringRequestParcel.aidl │ └── wifi │ └── SoftApState.aidl └── java └── android └── net ├── ITetheringConnector.java ├── ITetheringEventCallback.java ├── TetheredClient.java ├── TetheringCallbackStartedParcel.java ├── TetheringInterface.java ├── TetheringManagerHidden.java └── wifi ├── ISoftApCallback.java ├── IStringListener.java ├── IWifiManager.java ├── OuiKeyedData.java ├── SoftApCapability.java ├── SoftApConfigurationHidden.java ├── SoftApInfo.java ├── SoftApState.java └── WifiClient.java /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | max_line_length = 80 8 | 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{yaml,yml}] 15 | indent_size = 2 16 | 17 | [*.{kt,kts}] 18 | indent_size = 2 19 | ij_kotlin_packages_to_use_import_on_demand = unset 20 | ij_kotlin_name_count_to_use_star_import = 65535 21 | ij_kotlin_name_count_to_use_star_import_for_members = 65535 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [supershadoe] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: To report a bug or issue with the app. 3 | labels: ["bug", "user"] 4 | body: 5 | - type: textarea 6 | id: reproduction-steps 7 | attributes: 8 | label: Steps to reproduce 9 | description: Describe the steps to be taken to reproduce the bug. 10 | value: | 11 | 1. 12 | 2. 13 | 3. 14 | 4. 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: expected-behavior 20 | attributes: 21 | label: Expected behavior 22 | description: Describe what should have happened if the bug wasn't there. 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: actual-behavior 28 | attributes: 29 | label: Actual behavior 30 | description: The erroneous behavior that is manifested instead of the expected behavior. 31 | validations: 32 | required: true 33 | 34 | - type: input 35 | id: os-version 36 | attributes: 37 | label: Android version 38 | description: Mostly in Settings -> About phone. 39 | placeholder: | 40 | Example: "Android 15" 41 | validations: 42 | required: true 43 | 44 | - type: input 45 | id: device-model 46 | attributes: 47 | label: Device model 48 | description: Provide your device model. 49 | placeholder: | 50 | Example: "Pixel 6A" 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: crash-logs 56 | attributes: 57 | label: Crash logs 58 | description: Can obtain from adb logcat or through android studio. 59 | placeholder: | 60 | You can either paste the crash log here or attach it as an attachment. 61 | 62 | - type: textarea 63 | id: other-info 64 | attributes: 65 | label: Other info 66 | description: Any other info or images to share with us goes here. 67 | 68 | - type: checkboxes 69 | id: checklist 70 | attributes: 71 | label: Checklist 72 | description: Make sure to tick all the following boxes. 73 | options: 74 | - label: I have searched through the existing issues and am pretty sure that this is not a duplicate. 75 | required: true 76 | - label: I am using the latest version of the app, linked [here](https://github.com/supershadoe/delta/releases/latest) 77 | required: true 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/supershadoe/delta/discussions 5 | about: Ask questions about the app in general or any other help related to the app. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat_req.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: To suggest a new feature for the app. 3 | labels: ["enhancement", "user"] 4 | body: 5 | - type: textarea 6 | id: prob-statement 7 | attributes: 8 | label: Why is this feature needed? 9 | description: Describe what problem(s) would this feature solve. 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: feat-desc 15 | attributes: 16 | label: Potential solution 17 | description: Describe how the problem might be tackled here. 18 | validations: 19 | required: true 20 | 21 | - type: checkboxes 22 | id: checklist 23 | attributes: 24 | label: Checklist 25 | description: Make sure to tick all the following boxes. 26 | options: 27 | - label: I have searched through the existing issues and am pretty sure that this is not a duplicate. 28 | required: true 29 | -------------------------------------------------------------------------------- /.github/actions/build-apk/action.yml: -------------------------------------------------------------------------------- 1 | name: Build APK 2 | description: Builds debug and release APKs 3 | inputs: 4 | cache-encryption-key: 5 | description: 'Encryption key for the configuration cache' 6 | required: false 7 | sign-release: 8 | description: 'Whether to configure signing for a release build' 9 | required: true 10 | default: 'false' 11 | keystore-base64: 12 | description: 'Base64 encoded keystore for signing' 13 | required: false 14 | sign-base64: 15 | description: 'Base64 encoded signing configuration (sign.json)' 16 | required: false 17 | runs: 18 | using: 'composite' 19 | steps: 20 | - name: Custom gradle setup 21 | uses: ./.github/actions/gradle-setup 22 | with: 23 | cache-encryption-key: ${{ inputs.cache-encryption-key || '' }} 24 | - name: Decode signing keystore 25 | if: inputs.sign-release == 'true' 26 | shell: bash 27 | env: 28 | KEYSTORE_BASE64: ${{ inputs.keystore-base64 }} 29 | run: | 30 | echo "Decoding keystore..." 31 | if [ -z "$KEYSTORE_BASE64" ]; then 32 | echo "Error: KEYSTORE_BASE64 input is missing for signed release." 33 | exit 1 34 | fi 35 | echo $KEYSTORE_BASE64 | base64 -d > app/sign.keystore 36 | - name: Deserialize signing configuration 37 | if: inputs.sign-release == 'true' 38 | shell: bash 39 | env: 40 | SIGN_BASE64: ${{ inputs.sign-base64 }} 41 | run: | 42 | echo "Configuring signing key..." 43 | if [ -z "$SIGN_BASE64" ]; then 44 | echo "Error: SIGN_BASE64 input is missing for signed release." 45 | exit 1 46 | fi 47 | echo $SIGN_BASE64 | base64 -d > app/sign.json 48 | - name: Build debug APK 49 | shell: bash 50 | run: ./gradlew assembleDebugKeySignedDebug 51 | - name: Build debug key signed release APK 52 | if: inputs.sign-release == 'false' 53 | shell: bash 54 | run: ./gradlew assembleDebugKeySignedRelease 55 | - name: Build signed release APK 56 | if: inputs.sign-release == 'true' 57 | shell: bash 58 | run: ./gradlew assembleDefaultKeySignedRelease 59 | - name: Move APKs to root 60 | shell: bash 61 | run: | 62 | DEBUG_APK_PATH="app/build/outputs/apk/debugKeySigned/debug/app-debugKeySigned-debug.apk" 63 | UNSIGNED_RELEASE_APK_PATH="app/build/outputs/apk/debugKeySigned/release/app-debugKeySigned-release.apk" 64 | SIGNED_RELEASE_APK_PATH="app/build/outputs/apk/defaultKeySigned/release/app-defaultKeySigned-release.apk" 65 | [ -e "$DEBUG_APK_PATH" ] && mv "$DEBUG_APK_PATH" "app-debug.apk" 66 | [ -e "$UNSIGNED_RELEASE_APK_PATH" ] && mv "$UNSIGNED_RELEASE_APK_PATH" \ 67 | "app-release.apk" 68 | [ -e "$SIGNED_RELEASE_APK_PATH" ] && mv "$SIGNED_RELEASE_APK_PATH" \ 69 | "app-release.apk" 70 | - name: Upload APK 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: app 74 | path: app-*.apk 75 | retention-days: 7 76 | if-no-files-found: error 77 | -------------------------------------------------------------------------------- /.github/actions/gradle-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Custom gradle setup 2 | description: 'Gradle & Java setup for this project' 3 | inputs: 4 | cache-encryption-key: 5 | description: 'Encryption key for the configuration cache' 6 | required: false 7 | gradle-action: 8 | description: 'Gradle action to use for this step' 9 | required: false 10 | default: 'setup-gradle' 11 | read-only-cache: 12 | description: 'Whether to use a read-only cache for this step' 13 | required: false 14 | default: 'false' 15 | runs: 16 | using: 'composite' 17 | steps: 18 | - name: Setup JDK 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: temurin 22 | java-version: 21 23 | - name: Setup composite build caching 24 | uses: actions/cache@v4 25 | with: 26 | path: gradle/build-logic/build 27 | key: build-logic-${{ hashFiles('gradle/build-logic/src/**', 'gradle/build-logic/*.kts') }} 28 | - name: Setup gradle 29 | uses: gradle/actions/setup-gradle@v4 30 | if: inputs.gradle-action == 'setup-gradle' 31 | with: 32 | cache-encryption-key: ${{ inputs.cache-encryption-key }} 33 | cache-read-only: ${{ inputs.read-only-cache }} 34 | - name: Generate and submit dependency graph 35 | uses: gradle/actions/dependency-submission@v4 36 | if: inputs.gradle-action == 'dependency-submission' 37 | with: 38 | cache-encryption-key: ${{ inputs.cache-encryption-key }} 39 | dependency-graph-include-configurations: '.*RuntimeClasspath' 40 | -------------------------------------------------------------------------------- /.github/actions/run-lints/action.yml: -------------------------------------------------------------------------------- 1 | name: Linting/Formatting 2 | description: Runs lint and format checks 3 | inputs: 4 | cache-encryption-key: 5 | description: 'Encryption key for the configuration cache' 6 | required: false 7 | read-only-cache: 8 | description: 'Whether to use a read-only cache for this step' 9 | required: false 10 | default: 'false' 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Custom gradle setup 15 | uses: ./.github/actions/gradle-setup 16 | with: 17 | cache-encryption-key: ${{ inputs.cache-encryption-key || '' }} 18 | - name: Check for formatting issues 19 | shell: bash 20 | run: ./gradlew spotlessCheck 21 | -------------------------------------------------------------------------------- /.github/actions/run-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | description: Runs unit and integration tests 3 | inputs: 4 | cache-encryption-key: 5 | description: 'Encryption key for the configuration cache' 6 | required: false 7 | read-only-cache: 8 | description: 'Whether to use a read-only cache for this step' 9 | required: false 10 | default: 'false' 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Custom gradle setup 15 | uses: ./.github/actions/gradle-setup 16 | with: 17 | cache-encryption-key: ${{ inputs.cache-encryption-key || '' }} 18 | - name: Run unit tests 19 | shell: bash 20 | run: ./gradlew testReleaseUnitTest 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 6 | 7 | ### Related issue(s) 8 | 9 | 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: delta-ci 2 | on: 3 | workflow_call: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '*.md' 9 | - '*.txt' 10 | - '.github/*' 11 | - 'docs/*' 12 | - 'fastlane/*' 13 | pull_request: 14 | types: 15 | - ready_for_review 16 | - review_requested 17 | - synchronize 18 | paths-ignore: 19 | - '*.md' 20 | - '*.txt' 21 | - '.github/*' 22 | - 'docs/*' 23 | - 'fastlane/*' 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | jobs: 28 | lint: 29 | name: Lint check 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout repo 33 | uses: actions/checkout@v4 34 | - name: Run lint check 35 | uses: ./.github/actions/run-lints 36 | with: 37 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 38 | read-only-cache: ${{ github.event_name == 'pull_request' }} 39 | test: 40 | name: Testing 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout repo 44 | uses: actions/checkout@v4 45 | - name: Run unit tests 46 | uses: ./.github/actions/run-tests 47 | with: 48 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 49 | read-only-cache: ${{ github.event_name == 'pull_request' }} 50 | dependency-graph-submission: 51 | name: Dependency graph submission 52 | runs-on: ubuntu-latest 53 | if: github.event_name != 'pull_request' 54 | permissions: 55 | contents: write 56 | steps: 57 | - name: Checkout repo 58 | uses: actions/checkout@v4 59 | - name: Generate dependency graph 60 | uses: ./.github/actions/gradle-setup 61 | with: 62 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 63 | gradle-action: dependency-submission 64 | upload-strings-to-crowdin: 65 | name: Upload string to crowdin 66 | runs-on: ubuntu-latest 67 | if: github.event_name != 'pull_request' 68 | steps: 69 | - name: Checkout repo 70 | uses: actions/checkout@v4 71 | - name: Sync with crowdin 72 | uses: crowdin/github-action@v2 73 | with: 74 | upload_sources: true 75 | upload_translations: false 76 | download_translations: false 77 | create_pull_request: false 78 | env: 79 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 80 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PAT }} 81 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL analysis 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 1' 5 | jobs: 6 | analyze: 7 | name: Analyze java-kotlin 8 | runs-on: ubuntu-latest 9 | permissions: 10 | security-events: write 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v4 14 | - name: Initialize CodeQL 15 | uses: github/codeql-action/init@v3 16 | with: 17 | build-mode: manual 18 | - name: Custom gradle setup 19 | uses: ./.github/actions/gradle-setup 20 | with: 21 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 22 | - name: Build app 23 | run: ./gradlew assembleDebugKeySignedRelease 24 | - name: Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v3 26 | with: 27 | category: "/language:java-kotlin" 28 | -------------------------------------------------------------------------------- /.github/workflows/daily.yml: -------------------------------------------------------------------------------- 1 | name: Daily jobs 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | stale: 7 | name: Mark issues/PRs stale after 60 days 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-stale: 60 16 | days-before-close: 7 17 | stale-issue-message: | 18 | Due to no activity on this issue for over 2 months, this issue has been marked stale. 19 | 20 | Please chime in with a response if the issue still persists. 21 | close-issue-message: | 22 | This issue has been closed by the stale bot as no activity was observed. 23 | 24 | Please create a new issue if the issue still persists. 25 | stale-pr-message: | 26 | Due to no activity on this PR for over 2 months, this PR has been marked stale. 27 | 28 | Stale PRs increase overhead on maintainer's side and also quickly grow outdated as the 29 | codebase changes, thus making merging harder. 30 | 31 | If this PR is still relevant and required, make sure the PR is rebased over the current 32 | code. 33 | close-pr-message: | 34 | This PR has been closed by the stale bot as no activity was observed. 35 | 36 | Please create a new issue if the issue still persists. 37 | stale-issue-label: 'stale' 38 | stale-pr-label: 'stale' 39 | download-translations-from-crowdin: 40 | name: Download translations from crowdin 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: write 44 | pull-requests: write 45 | steps: 46 | - name: Checkout repo 47 | uses: actions/checkout@v4 48 | - name: Sync with crowdin 49 | uses: crowdin/github-action@v2 50 | with: 51 | upload_sources: false 52 | upload_translations: false 53 | download_translations: true 54 | skip_untranslated_strings: true 55 | localization_branch_name: crowdin-sync 56 | create_pull_request: true 57 | commit_message: Sync translations 58 | pull_request_title: Sync translations 59 | pull_request_body: '_Synced by GitHub actions from [Crowdin](https://crowdin.com/project/delta-app)_' 60 | pull_request_reviewers: supershadoe 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 64 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PAT }} 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release tasks 2 | on: 3 | push: 4 | tags-ignore: 5 | - norel-* 6 | workflow_dispatch: 7 | concurrency: 8 | group: release 9 | cancel-in-progress: true 10 | jobs: 11 | # TODO: check if CI passed for tag 12 | build-apk: 13 | name: Build app 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v4 20 | - name: Build APK 21 | uses: ./.github/actions/build-apk 22 | with: 23 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 24 | sign-release: true 25 | keystore-base64: ${{ secrets.KEYSTORE_BASE64 }} 26 | sign-base64: ${{ secrets.SIGN_BASE64 }} 27 | create-release: 28 | name: Create release 29 | runs-on: ubuntu-latest 30 | needs: [build-apk] 31 | permissions: 32 | contents: write 33 | discussions: write 34 | steps: 35 | # TODO: use metadata from fastlane 36 | - name: Download APK 37 | uses: actions/download-artifact@v4 38 | with: 39 | name: app 40 | - name: Calculate checksum and generate release notes 41 | run: | 42 | cat << "EOF" >> release_notes.txt 43 | ### Message 44 | 45 | To be filled. 46 | 47 | ### Checksum 48 | 49 | ```sh 50 | $ sha256sum app-release.apk 51 | EOF 52 | 53 | sha256sum app-release.apk >> release_notes.txt 54 | 55 | cat << "EOF" >> release_notes.txt 56 | ``` 57 | 58 | Check the checksum after downloading the APK using `sha256sum` to 59 | ensure integrity of the file. 60 | EOF 61 | - name: Create release 62 | uses: softprops/action-gh-release@v2 63 | with: 64 | generate_release_notes: true 65 | draft: true 66 | prerelease: false 67 | tag_name: ${{ needs.pre-release-job.outputs.version }} 68 | files: | 69 | app-release.apk 70 | discussion_category_name: Announcements 71 | body_path: release_notes.txt 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea/* 4 | !.idea/icon.svg 5 | /local.properties 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | .kotlin 13 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution guide 2 | 3 | Thank you for taking the time to contribute to Delta! :sparkles: 4 | 5 | ### Steps to follow while reporting a bug 6 | 7 | > [!WARNING] 8 | > Follow the steps in [security policy](https://github.com/supershadoe/delta/security/policy) 9 | > if it is a security vulnerability. 10 | 11 | - Check if a relevant issue exists in the [issue tracker](https://github.com/supershadoe/delta/issues). 12 | - If an issue doesn't exist, choose the relevant [issue form](https://github.com/supershadoe/delta/issues/new/choose) 13 | and open a new issue. 14 | 15 | ### Developer guide 16 | 17 | The project has the following modules. 18 | - `:api` - Contains all the classes and interfaces used by all modules in the 19 | app. 20 | - `:app` - Contains all the UI components and the ViewModels used by the app. 21 | - `:data` - Contains the core logic of the app that interfaces with the hidden 22 | Android API. This is what you want to modify if you want to fix something in 23 | the backend of the app. 24 | - `:system-api-stubs` - These are only to be modified when adding new 25 | functionality by using any other hidden API. 26 | - `:buildSrc` - Contains the shared convention plugins and functions to be 27 | reused in other build scripts. 28 | 29 | ### New contributors 30 | 31 | If you do not have an idea where to start, ping `@supershadoe` on Discord or 32 | mail me at [shadoe@shadoe.dev](mailto:shadoe@shadoe.dev) 33 | 34 | This is only for getting to know what's the need of the hour and what to get 35 | started with, not for asking to be coached through the process of developing. 36 | 37 | Some good resources to start with the project are as follows. 38 | (the links might be broken as Android docs changes up the links often) 39 | 40 | - [About app architecture | Android docs](https://developer.android.com/topic/architecture) 41 | - [Localization | Android docs](https://developer.android.com/guide/topics/resources/localization#creating-alternatives) 42 | - [Flows | Kotlin docs](https://kotlinlang.org/docs/flow.html) 43 | - [Flow | Android docs](https://developer.android.com/kotlin/flow) 44 | - [StateFlow and SharedFlow | Android docs](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) 45 | - [Jetpack compose](https://developer.android.com/compose) 46 | - [Shizuku-API](https://github.com/RikkaApps/Shizuku-API/) 47 | 48 | If you want to utilize some other hidden API for a new feature in the app, make 49 | sure you know what you are doing because there are no specific guides to help 50 | you out with that; the only sources of truth are [Android Code Search](https://cs.android.com/) 51 | and your device. 52 | 53 | ### Steps to send in a patch for an issue 54 | 55 | > [!WARNING] 56 | > - Do NOT create a pull request without creating an issue first. 57 | > - Do NOT make a cosmetic change alone as the formatter setup in the repo will 58 | > take of enforcing formatting and other standards. 59 | 60 | - Create a pull request after pushing your patch to GitHub. 61 | - Mark the PR as ready for review (if opened as draft) to start the automated 62 | testing. 63 | - If all the tests do not pass, fix whatever broke the tests and push again. 64 | - PR will not be accepted without the tests passing. 65 | 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024, 2025 supershadoe 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## delta 2 | 3 | This app allows accessing advanced hotspot settings not exposed through the 4 | system settings app on devices that use Stock Android (Pixel, Moto, etc.) 5 | 6 | It provides various information and features such as: 7 | - Usual info provided by settings app such as hotspot name, passphrase, security 8 | type, frequency bands, quick connect using QR, etc. 9 | - List of connected devices. 10 | - Modifying amount of devices that can connect. 11 | - Changing MAC randomization settings. 12 | - Setting auto turn-off timeout. 13 | - Blocklist to prevent certain devices from connecting. 14 | - Make hotspot's SSID hidden (device doesn't appear on WiFi scans) 15 | - Quick setting tile to easily turn hotspot on/off from notification shade. 16 | - Control hotspot using Tasker/other automation apps. 17 | 18 | It uses Shizuku to obtain hotspot related details and to modify hotspot 19 | settings. It is also useful on devices like Samsung Tabs (WiFi-only) where 20 | hotspot settings are hidden from Settings app and SystemUI. 21 | 22 | ### Installation 23 | 24 | 1. Install Shizuku from [Play Store](https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api) 25 | or other places like GitHub/IzzyOnDroid [(official site)](https://shizuku.rikka.app/download/) 26 | 2. Get Delta from either GitHub releases directly, using [Obtainium](https://github.com/ImranR98/Obtainium) 27 | or from [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.shadoe.delta) 28 | F-Droid repo. 29 | 30 | [Get it on Obtainium](https://github.com/ImranR98/Obtainium) 32 | [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.shadoe.delta) 34 | 35 | ### Localization 36 | 37 | If you wish to translate the project to a language that you are fluent in, you 38 | can contribute through [Crowdin](https://crowdin.com/project/delta-app) 39 | 40 | Open up a discussion on [GitHub](https://github.com/supershadoe/delta/discussions/new?category=ideas) 41 | or [mail me](mailto:shadoe@shadoe.dev) to enable any language on Crowdin other 42 | than the ones already enabled. 43 | 44 | ### Want to contribute? 45 | 46 | Check out this [page](https://github.com/supershadoe/delta/contribute) on how 47 | to start! :hugs: 48 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the most recent stable release of Delta (referred to as "the app") will be maintained and patched in case 6 | of any security issues. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Posting on public channels like issues/discussions may cause bad actors to take advantage of 11 | the vulnerabilities and thus, it is not advisable. 12 | 13 | To report a vulnerability, mail @supershadoe through this [mail ID](mailto:security@shadoe.dev) and not through 14 | any other channels. 15 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("delta.lint.kotlin") 4 | id("delta.lint.kts") 5 | } 6 | 7 | kotlin { jvmToolchain(21) } 8 | 9 | dependencies { implementation(libs.androidx.annotation) } 10 | -------------------------------------------------------------------------------- /api/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershadoe/delta/2cc85f133800794b6056a77b430367e0f1a973bf/api/consumer-rules.pro -------------------------------------------------------------------------------- /api/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/ACLDevice.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | data class ACLDevice(val hostname: String?, val macAddress: MacAddress) 4 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/ConfigFlag.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | /** 4 | * Holds certain flags used to determine the app behavior. 5 | * 6 | * Possible usage: Determining first run, app version migrations, etc. 7 | */ 8 | enum class ConfigFlag { 9 | /** True if the app has been run before. */ 10 | NOT_FIRST_RUN, 11 | 12 | /** 13 | * True if the app uses Room for config. 14 | * 15 | * This is set either after migrating app from Datastore when upgrading from 16 | * older versions or when app is installed afresh. 17 | */ 18 | @Deprecated("Not used anymore", level = DeprecationLevel.HIDDEN) USES_ROOM_DB, 19 | 20 | /** 21 | * Adding a permission to the receiver cannot let arbitrary apps send intents 22 | * to this app. Thus, a setting is added in the app for power users to enable 23 | * insecure receivers. 24 | */ 25 | INSECURE_RECEIVER_ENABLED, 26 | } 27 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/LinkAddress.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import java.net.InetAddress 4 | 5 | data class LinkAddress(val address: InetAddress) 6 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/MacAddress.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | data class MacAddress(val macAddress: String) { 4 | override fun toString() = macAddress 5 | } 6 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/ShizukuStates.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import androidx.annotation.IntDef 4 | 5 | object ShizukuStates { 6 | const val NOT_READY = -1 7 | const val NOT_AVAILABLE = 0 8 | const val NOT_RUNNING = 1 9 | const val NOT_CONNECTED = 2 10 | const val CONNECTED = 3 11 | 12 | @IntDef( 13 | value = [NOT_READY, NOT_AVAILABLE, NOT_RUNNING, NOT_CONNECTED, CONNECTED] 14 | ) 15 | annotation class ShizukuStateType 16 | } 17 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApAutoShutdownTimeout.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import androidx.annotation.LongDef 4 | 5 | object SoftApAutoShutdownTimeout { 6 | const val DEFAULT = -1L 7 | const val FIVE_MINUTES = 5 * 60 * 1000L 8 | const val TEN_MINUTES = 10 * 60 * 1000L 9 | const val TWENTY_MINUTES = 20 * 60 * 1000L 10 | const val THIRTY_MINUTES = 30 * 60 * 1000L 11 | const val ONE_HOUR = 60 * 60 * 1000L 12 | 13 | @Retention(AnnotationRetention.SOURCE) 14 | @LongDef( 15 | value = 16 | [ 17 | DEFAULT, 18 | FIVE_MINUTES, 19 | TEN_MINUTES, 20 | TWENTY_MINUTES, 21 | THIRTY_MINUTES, 22 | ONE_HOUR, 23 | ] 24 | ) 25 | annotation class AutoShutdownTimeoutType 26 | 27 | val supportedShutdownTimeouts = 28 | listOf( 29 | DEFAULT, 30 | FIVE_MINUTES, 31 | TEN_MINUTES, 32 | TWENTY_MINUTES, 33 | THIRTY_MINUTES, 34 | ONE_HOUR, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApCapabilities.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import dev.shadoe.delta.api.SoftApSecurityType.SecurityType 4 | import dev.shadoe.delta.api.SoftApSpeedType.BandType 5 | 6 | data class SoftApCapabilities( 7 | val maxSupportedClients: Int, 8 | val clientForceDisconnectSupported: Boolean, 9 | val isMacAddressCustomizationSupported: Boolean, 10 | @BandType val supportedFrequencyBands: List, 11 | @SecurityType val supportedSecurityTypes: List, 12 | ) 13 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | data class SoftApConfiguration( 4 | val ssid: String?, 5 | val passphrase: String, 6 | @SoftApSecurityType.SecurityType val securityType: Int, 7 | @SoftApRandomizationSetting.RandomizationType 8 | val macRandomizationSetting: Int, 9 | val isHidden: Boolean, 10 | @SoftApSpeedType.BandType val speedType: Int, 11 | val blockedDevices: List, 12 | val allowedClients: List, 13 | val isAutoShutdownEnabled: Boolean, 14 | /** Timeout in milliseconds for auto shutdown. */ 15 | val autoShutdownTimeout: Long, 16 | val maxClientLimit: Int, 17 | ) 18 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApEnabledState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Adopted from source of android.net.wifi.WifiManager 3 | * 4 | * Copyright (C) 2019 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | package dev.shadoe.delta.api 21 | 22 | import androidx.annotation.IntDef 23 | 24 | object SoftApEnabledState { 25 | /** 26 | * Wi-Fi AP is currently being disabled. The state will change to 27 | * {@link #WIFI_AP_STATE_DISABLED} if it finishes successfully. 28 | */ 29 | const val WIFI_AP_STATE_DISABLING = 10 30 | 31 | /** Wi-Fi AP is disabled. */ 32 | const val WIFI_AP_STATE_DISABLED = 11 33 | 34 | /** 35 | * Wi-Fi AP is currently being enabled. The state will change to 36 | * {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully. 37 | */ 38 | const val WIFI_AP_STATE_ENABLING = 12 39 | 40 | /** Wi-Fi AP is enabled. */ 41 | const val WIFI_AP_STATE_ENABLED = 13 42 | 43 | /** 44 | * Wi-Fi AP is in a failed state. This state will occur when an error occurs 45 | * during enabling or disabling 46 | */ 47 | const val WIFI_AP_STATE_FAILED = 14 48 | 49 | @Retention(AnnotationRetention.SOURCE) 50 | @IntDef( 51 | value = 52 | [ 53 | WIFI_AP_STATE_DISABLING, 54 | WIFI_AP_STATE_DISABLED, 55 | WIFI_AP_STATE_ENABLING, 56 | WIFI_AP_STATE_ENABLED, 57 | WIFI_AP_STATE_FAILED, 58 | ] 59 | ) 60 | annotation class EnabledStateType 61 | } 62 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApRandomizationSetting.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import androidx.annotation.IntDef 4 | 5 | object SoftApRandomizationSetting { 6 | const val RANDOMIZATION_NONE = 0 7 | const val RANDOMIZATION_PERSISTENT = 1 8 | const val RANDOMIZATION_NON_PERSISTENT = 2 9 | 10 | @Retention(AnnotationRetention.SOURCE) 11 | @IntDef( 12 | value = 13 | [ 14 | RANDOMIZATION_NONE, 15 | RANDOMIZATION_PERSISTENT, 16 | RANDOMIZATION_NON_PERSISTENT, 17 | ] 18 | ) 19 | annotation class RandomizationType 20 | 21 | val supportedRandomizationSettings = 22 | listOf( 23 | RANDOMIZATION_NONE, 24 | RANDOMIZATION_PERSISTENT, 25 | RANDOMIZATION_NON_PERSISTENT, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApSecurityType.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import androidx.annotation.IntDef 4 | 5 | object SoftApSecurityType { 6 | const val SECURITY_TYPE_OPEN = 0 7 | const val SECURITY_TYPE_WPA2_PSK = 1 8 | const val SECURITY_TYPE_WPA3_SAE_TRANSITION = 2 9 | const val SECURITY_TYPE_WPA3_SAE = 3 10 | const val SECURITY_TYPE_WPA3_OWE_TRANSITION = 4 11 | const val SECURITY_TYPE_WPA3_OWE = 5 12 | 13 | @Retention(AnnotationRetention.SOURCE) 14 | @IntDef( 15 | value = 16 | [ 17 | SECURITY_TYPE_OPEN, 18 | SECURITY_TYPE_WPA2_PSK, 19 | SECURITY_TYPE_WPA3_SAE_TRANSITION, 20 | SECURITY_TYPE_WPA3_SAE, 21 | SECURITY_TYPE_WPA3_OWE_TRANSITION, 22 | SECURITY_TYPE_WPA3_OWE, 23 | ] 24 | ) 25 | annotation class SecurityType 26 | } 27 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApSpeedType.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | import androidx.annotation.IntDef 4 | 5 | object SoftApSpeedType { 6 | /** Wi-Fi hotspot band unknown. */ 7 | const val BAND_UNKNOWN = 0 8 | 9 | const val BAND_2GHZ = 1 shl 0 10 | const val BAND_5GHZ = 1 shl 1 11 | const val BAND_6GHZ = 1 shl 2 12 | const val BAND_60GHZ = 1 shl 3 13 | 14 | @Retention(AnnotationRetention.SOURCE) 15 | @IntDef(value = [BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ]) 16 | annotation class BandType 17 | } 18 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/SoftApStatus.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | data class SoftApStatus( 4 | @SoftApEnabledState.EnabledStateType val enabledState: Int, 5 | val tetheredClients: List, 6 | val capabilities: SoftApCapabilities, 7 | val isSoftApSupported: Boolean, 8 | ) 9 | -------------------------------------------------------------------------------- /api/src/main/kotlin/dev/shadoe/delta/api/TetheredClient.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.api 2 | 3 | data class TetheredClient( 4 | val macAddress: MacAddress, 5 | val address: LinkAddress?, 6 | val hostname: String?, 7 | val tetheringType: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | sign.json -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/dev/shadoe/delta/settings/components/AutoShutdownFieldTest.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.test.assertIsOff 8 | import androidx.compose.ui.test.assertIsOn 9 | import androidx.compose.ui.test.isToggleable 10 | import androidx.compose.ui.test.junit4.createComposeRule 11 | import androidx.compose.ui.test.performClick 12 | import androidx.test.ext.junit.runners.AndroidJUnit4 13 | import kotlin.test.Test 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class AutoShutdownFieldTest { 19 | @get:Rule val composeTestRule = createComposeRule() 20 | 21 | @Test 22 | fun `Check if the switch invokes the callback on tap`() { 23 | composeTestRule.run { 24 | setContent { 25 | var state by remember { mutableStateOf(false) } 26 | AutoShutdownField( 27 | isAutoShutdownEnabled = state, 28 | onAutoShutdownChange = { state = !state }, 29 | ) 30 | } 31 | onNode(isToggleable()).run { 32 | performClick() 33 | assertIsOn() 34 | performClick() 35 | assertIsOff() 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 30 | 31 | 32 | 33 | 35 | 36 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershadoe/delta/2cc85f133800794b6056a77b430367e0f1a973bf/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/Application.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import dagger.hilt.android.HiltAndroidApp 6 | import dev.shadoe.delta.crash.CrashHandlerUtils 7 | import kotlin.system.exitProcess 8 | import org.lsposed.hiddenapibypass.HiddenApiBypass 9 | 10 | @HiltAndroidApp 11 | class Application : Application() { 12 | override fun onCreate() { 13 | super.onCreate() 14 | 15 | Thread.setDefaultUncaughtExceptionHandler { _, throwable -> 16 | Log.e(packageName, "Uncaught exception", throwable) 17 | CrashHandlerUtils.sendCrashNotification(applicationContext, throwable) 18 | exitProcess(1) 19 | } 20 | 21 | HiddenApiBypass.setHiddenApiExemptions("L") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.hilt.navigation.compose.hiltViewModel 8 | import dagger.hilt.android.AndroidEntryPoint 9 | import dev.shadoe.delta.common.Nav 10 | import dev.shadoe.delta.common.NavViewModel 11 | import dev.shadoe.delta.design.AppTheme 12 | 13 | @AndroidEntryPoint 14 | class MainActivity : ComponentActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | enableEdgeToEdge() 17 | super.onCreate(savedInstanceState) 18 | setContent { AppTheme { Nav(hiltViewModel()) } } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/NavViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dev.shadoe.delta.api.ShizukuStates 9 | import dev.shadoe.delta.crash.CrashHandlerUtils 10 | import dev.shadoe.delta.data.FlagsRepository 11 | import dev.shadoe.delta.data.shizuku.ShizukuRepository 12 | import dev.shadoe.delta.data.softap.SoftApStateFacade 13 | import javax.inject.Inject 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.asStateFlow 16 | import kotlinx.coroutines.flow.update 17 | import kotlinx.coroutines.launch 18 | 19 | @HiltViewModel 20 | class NavViewModel 21 | @Inject 22 | constructor( 23 | @ApplicationContext private val applicationContext: Context, 24 | private val shizukuRepository: ShizukuRepository, 25 | private val softApStateFacade: SoftApStateFacade, 26 | private val flagsRepository: FlagsRepository, 27 | ) : ViewModel() { 28 | private val _startScreen = MutableStateFlow(Routes.BlankScreen) 29 | val startScreen = _startScreen.asStateFlow() 30 | 31 | private suspend fun determineStartScreen(): Route { 32 | val isFirstRun = flagsRepository.isFirstRun() 33 | val shizukuConnected = 34 | shizukuRepository.shizukuState.value != ShizukuStates.CONNECTED 35 | val crashHandlerSetup = 36 | CrashHandlerUtils.shouldShowNotificationPermissionRequest( 37 | applicationContext 38 | ) 39 | return when { 40 | isFirstRun -> Routes.Setup.FirstUseScreen 41 | shizukuConnected -> Routes.Setup.ShizukuSetupScreen 42 | crashHandlerSetup -> Routes.Setup.CrashHandlerSetupScreen 43 | else -> Routes.SoftApScreen 44 | } 45 | } 46 | 47 | init { 48 | addCloseable(shizukuRepository.callbackSubscriber) 49 | softApStateFacade.subscribe() 50 | addCloseable { softApStateFacade.unsubscribe() } 51 | viewModelScope.launch { 52 | shizukuRepository.shizukuState.collect { 53 | _startScreen.update { determineStartScreen() } 54 | } 55 | } 56 | } 57 | 58 | fun onSetupStarted() = 59 | viewModelScope.launch { flagsRepository.setNotFirstRun() } 60 | 61 | fun onSetupFinished() = 62 | viewModelScope.launch { _startScreen.update { determineStartScreen() } } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/Routes.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | sealed interface Route {} 6 | 7 | object Routes { 8 | @Serializable object BlankScreen : Route 9 | 10 | @Serializable 11 | object Setup { 12 | @Serializable object FirstUseScreen : Route 13 | 14 | @Serializable object ShizukuSetupScreen : Route 15 | 16 | @Serializable object CrashHandlerSetupScreen : Route 17 | } 18 | 19 | @Serializable object SoftApScreen : Route 20 | 21 | @Serializable object BlocklistScreen : Route 22 | 23 | @Serializable object DebugScreen : Route 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/SoftApScreenDestinations.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.rounded.Dashboard 6 | import androidx.compose.material.icons.rounded.Settings 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import dev.shadoe.delta.R 9 | 10 | enum class SoftApScreenDestinations( 11 | @StringRes val label: Int, 12 | val icon: ImageVector, 13 | @StringRes val contentDescription: Int, 14 | ) { 15 | CONTROL( 16 | label = R.string.controls_icon, 17 | Icons.Rounded.Dashboard, 18 | contentDescription = R.string.controls_icon, 19 | ), 20 | SETTINGS( 21 | label = R.string.settings, 22 | Icons.Rounded.Settings, 23 | contentDescription = R.string.settings, 24 | ), 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/SoftApScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import dev.shadoe.delta.data.softap.SoftApStateStore 6 | import javax.inject.Inject 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.flow.mapLatest 9 | 10 | @HiltViewModel 11 | class SoftApScreenViewModel @Inject constructor(state: SoftApStateStore) : 12 | ViewModel() { 13 | @OptIn(ExperimentalCoroutinesApi::class) 14 | val isSoftApSupported = state.status.mapLatest { it.isSoftApSupported } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/components/FadeInExpanded.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.AnimatedVisibilityScope 5 | import androidx.compose.animation.expandVertically 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.animation.shrinkVertically 9 | import androidx.compose.runtime.Composable 10 | 11 | @Composable 12 | fun FadeInExpanded( 13 | visible: Boolean, 14 | content: @Composable AnimatedVisibilityScope.() -> Unit, 15 | ) { 16 | AnimatedVisibility( 17 | visible = visible, 18 | enter = fadeIn() + expandVertically(), 19 | exit = fadeOut() + shrinkVertically(), 20 | content = content, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/components/FoldableWrapper.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.rounded.KeyboardArrowDown 10 | import androidx.compose.material.icons.rounded.KeyboardArrowUp 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import dev.shadoe.delta.R 20 | 21 | @Composable 22 | fun FoldableWrapper( 23 | text: String, 24 | foldableState: Boolean, 25 | onFoldableToggled: () -> Unit, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | Row( 29 | modifier = 30 | Modifier.fillMaxWidth() 31 | .clickable { onFoldableToggled() } 32 | .padding(vertical = 16.dp) 33 | .then(modifier), 34 | horizontalArrangement = Arrangement.SpaceBetween, 35 | ) { 36 | Text( 37 | text = text, 38 | color = MaterialTheme.colorScheme.onSurfaceVariant, 39 | style = MaterialTheme.typography.titleLarge, 40 | ) 41 | Icon( 42 | modifier = Modifier.align(Alignment.CenterVertically), 43 | imageVector = 44 | if (foldableState) { 45 | Icons.Rounded.KeyboardArrowUp 46 | } else { 47 | Icons.Rounded.KeyboardArrowDown 48 | }, 49 | contentDescription = 50 | if (foldableState) { 51 | stringResource(R.string.collapse_icon) 52 | } else { 53 | stringResource(R.string.expand_icon) 54 | }, 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/common/shapes/PolygonShape.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.common.shapes 2 | 3 | import androidx.compose.ui.geometry.Rect 4 | import androidx.compose.ui.geometry.Size 5 | import androidx.compose.ui.graphics.Matrix 6 | import androidx.compose.ui.graphics.Outline 7 | import androidx.compose.ui.graphics.Shape 8 | import androidx.compose.ui.graphics.asComposePath 9 | import androidx.compose.ui.unit.Density 10 | import androidx.compose.ui.unit.LayoutDirection 11 | import androidx.graphics.shapes.RoundedPolygon 12 | import androidx.graphics.shapes.toPath 13 | import kotlin.math.max 14 | 15 | class PolygonShape(private val polygon: RoundedPolygon) : Shape { 16 | // 4x4 xyzw matrix for transformations on the polygon. 17 | private val matrix = Matrix() 18 | 19 | override fun createOutline( 20 | size: Size, 21 | layoutDirection: LayoutDirection, 22 | density: Density, 23 | ): Outline { 24 | // Generate the cubic path for the polygon to draw at every frame 25 | // during the animation. 26 | val path = polygon.toPath().asComposePath() 27 | 28 | // The polygon generated by graphics lib is not necessarily 29 | // the same size as whatever box we are clipping/drawing in. 30 | // So, we need to find the scaling factor to scale the polygon 31 | // to fit the box without distorting it. 32 | 33 | // calculateBounds() returns the axis-aligned 34 | // bounding box (just the dimensions of the rectangle wrapping the 35 | // polygon) for the polygon. 36 | 37 | // Calculate the bounds of the polygon. 38 | val bounds = 39 | polygon.calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } 40 | 41 | // Take the bigger side out of the two 42 | // (it's usually almost same in this app as it's all circles 43 | // of radius 1f/2f) 44 | val maxDimension = max(bounds.width, bounds.height) 45 | 46 | // Calculate scaling factors 47 | val scaleX = size.width / maxDimension 48 | val scaleY = size.height / maxDimension 49 | 50 | matrix.scale(scaleX, scaleY) 51 | 52 | // The polygon is centered at origin (0, 0) so for a circle of 53 | // radius = 1, you can only see a quarter of the shape in view. 54 | 55 | // The bounds for a circle of radius 1f would be (-1f, -1f, 1f, 1f) 56 | // So, translating by (-bounds.left, -bounds.top) would move the polygon 57 | // to the center of the viewport 58 | matrix.translate(-bounds.left, -bounds.top) 59 | 60 | // Transform the generated path using this matrix. 61 | path.transform(matrix) 62 | 63 | // Generate outline based on the cubic path. 64 | return Outline.Generic(path) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/control/ControlViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.control 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import dev.shadoe.delta.api.ACLDevice 6 | import dev.shadoe.delta.data.softap.SoftApBlocklistManager 7 | import dev.shadoe.delta.data.softap.SoftApStateStore 8 | import javax.inject.Inject 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.flow.mapLatest 11 | 12 | @HiltViewModel 13 | class ControlViewModel 14 | @Inject 15 | constructor( 16 | state: SoftApStateStore, 17 | private val softApBlocklistManager: SoftApBlocklistManager, 18 | ) : ViewModel() { 19 | @OptIn(ExperimentalCoroutinesApi::class) 20 | val enabledState = state.status.mapLatest { it.enabledState } 21 | 22 | @OptIn(ExperimentalCoroutinesApi::class) 23 | val connectedClients = state.status.mapLatest { it.tetheredClients } 24 | 25 | @OptIn(ExperimentalCoroutinesApi::class) 26 | val supportsBlocklist = 27 | state.status.mapLatest { it.capabilities.clientForceDisconnectSupported } 28 | 29 | val blockedClients = softApBlocklistManager.blockedClients 30 | 31 | fun blockDevices(devices: Iterable) = 32 | softApBlocklistManager.blockDevices(devices) 33 | 34 | fun unblockDevices(devices: Iterable) = 35 | softApBlocklistManager.unblockDevices(devices) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/control/components/AppBarWithDebugAction.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.control.components 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.res.stringResource 14 | import dev.shadoe.delta.R 15 | 16 | @Composable 17 | fun AppBarWithDebugAction(onNavigateToDebug: () -> Unit) { 18 | val context = LocalContext.current 19 | var debugTaps by remember { mutableIntStateOf(0) } 20 | val stringTriggeredDebugScreen = 21 | stringResource(R.string.triggered_debug_screen) 22 | Text( 23 | text = stringResource(R.string.app_name), 24 | modifier = 25 | Modifier.clickable { 26 | debugTaps += 1 27 | if (debugTaps % 5 == 0) { 28 | onNavigateToDebug() 29 | Toast.makeText( 30 | context, 31 | stringTriggeredDebugScreen, 32 | Toast.LENGTH_SHORT, 33 | ) 34 | .show() 35 | } 36 | }, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/control/components/BlockedClientComponent.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.control.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Checkbox 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import dev.shadoe.delta.R 15 | import dev.shadoe.delta.api.ACLDevice 16 | 17 | data class BlockedClientComponentState( 18 | val device: ACLDevice, 19 | val isEditingBlocklist: Boolean, 20 | val isChosenForUnblock: Boolean, 21 | ) 22 | 23 | data class BlockedClientComponentActions( 24 | val startEditing: () -> Unit, 25 | val onEditToggled: (Boolean) -> Unit, 26 | ) 27 | 28 | @Composable 29 | fun BlockedClientComponent( 30 | state: BlockedClientComponentState, 31 | actions: BlockedClientComponentActions, 32 | modifier: Modifier = Modifier, 33 | ) { 34 | Row( 35 | modifier = 36 | modifier.then(Modifier.padding(horizontal = 16.dp, vertical = 4.dp)), 37 | verticalAlignment = Alignment.CenterVertically, 38 | ) { 39 | if (state.isEditingBlocklist) { 40 | Checkbox( 41 | checked = state.isChosenForUnblock, 42 | onCheckedChange = actions.onEditToggled, 43 | ) 44 | } 45 | Column(modifier = Modifier.weight(1f)) { 46 | Text( 47 | text = 48 | state.device.hostname ?: stringResource(R.string.no_client_hostname) 49 | ) 50 | Text(text = state.device.macAddress.toString()) 51 | } 52 | if (!state.isEditingBlocklist) { 53 | Button(onClick = actions.startEditing) { 54 | Text(text = stringResource(R.string.unblock_button)) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/control/components/ConnectedClientsList.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.control.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.LazyListScope 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import dev.shadoe.delta.R 16 | import dev.shadoe.delta.api.TetheredClient 17 | 18 | internal data class ConnectedClientsListState( 19 | val tetheredClients: List, 20 | val supportsBlocklist: Boolean, 21 | val devicesToBlock: Set, 22 | ) 23 | 24 | internal data class ConnectedClientsListActions( 25 | val addToBlockList: (TetheredClient) -> Unit, 26 | val removeFromBlockList: (TetheredClient) -> Unit, 27 | val onBlockClients: () -> Unit, 28 | ) 29 | 30 | internal fun LazyListScope.connectedClientsList( 31 | state: ConnectedClientsListState, 32 | actions: ConnectedClientsListActions, 33 | ) { 34 | item { 35 | Text( 36 | text = stringResource(R.string.connected_devices), 37 | color = MaterialTheme.colorScheme.onSurfaceVariant, 38 | style = MaterialTheme.typography.titleLarge, 39 | modifier = Modifier.padding(16.dp), 40 | ) 41 | } 42 | if (state.tetheredClients.isEmpty()) { 43 | item { 44 | Box( 45 | modifier = Modifier.fillMaxWidth(), 46 | contentAlignment = Alignment.Center, 47 | ) { 48 | Text(text = stringResource(R.string.no_connected_devices)) 49 | } 50 | } 51 | } 52 | items(state.tetheredClients) { client -> 53 | ConnectedClientComponent( 54 | state = 55 | ConnectedClientComponentState( 56 | client = client, 57 | isEditingBlocklist = state.devicesToBlock.isNotEmpty(), 58 | supportsBlocklist = state.supportsBlocklist, 59 | isChosenForBlocking = state.devicesToBlock.contains(client), 60 | ), 61 | actions = 62 | ConnectedClientComponentActions( 63 | startEditing = { actions.addToBlockList(client) }, 64 | onEditToggled = { isChecked -> 65 | if (isChecked) { 66 | actions.addToBlockList(client) 67 | } else { 68 | actions.removeFromBlockList(client) 69 | } 70 | }, 71 | ), 72 | ) 73 | } 74 | if (state.devicesToBlock.isNotEmpty()) { 75 | item { 76 | Button( 77 | onClick = actions.onBlockClients, 78 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), 79 | ) { 80 | Text(text = stringResource(R.string.block_button)) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/control/components/PassphraseDisplay.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.control.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.Visibility 9 | import androidx.compose.material.icons.rounded.VisibilityOff 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.LocalTextStyle 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalDensity 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.text.font.FontStyle 22 | import androidx.compose.ui.unit.dp 23 | import dev.shadoe.delta.R 24 | 25 | @Composable 26 | internal fun PassphraseDisplay(passphrase: String?) { 27 | val density = LocalDensity.current 28 | val isPassphraseShown = remember { mutableStateOf(false) } 29 | 30 | Row( 31 | modifier = 32 | Modifier.clickable { isPassphraseShown.value = !isPassphraseShown.value }, 33 | verticalAlignment = Alignment.CenterVertically, 34 | ) { 35 | Text( 36 | text = 37 | if (passphrase == null) { 38 | stringResource(R.string.no_passphrase) 39 | } else if (isPassphraseShown.value) { 40 | passphrase 41 | } else { 42 | stringResource(R.string.passphrase_hidden) 43 | }, 44 | style = 45 | MaterialTheme.typography.bodyMedium.copy( 46 | fontStyle = 47 | if (isPassphraseShown.value && passphrase != null) { 48 | FontStyle.Normal 49 | } else { 50 | FontStyle.Italic 51 | } 52 | ), 53 | ) 54 | Icon( 55 | imageVector = 56 | if (isPassphraseShown.value) { 57 | Icons.Rounded.VisibilityOff 58 | } else { 59 | Icons.Rounded.Visibility 60 | }, 61 | contentDescription = 62 | if (isPassphraseShown.value) { 63 | stringResource(R.string.passphrase_hide) 64 | } else { 65 | stringResource(R.string.passphrase_show) 66 | }, 67 | modifier = 68 | Modifier.padding(start = 8.dp) 69 | .size(with(density) { LocalTextStyle.current.fontSize.toDp() }), 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/debug/DebugViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.debug 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import dev.shadoe.delta.api.ConfigFlag 7 | import dev.shadoe.delta.data.FlagsRepository 8 | import dev.shadoe.delta.data.MacAddressCacheRepository 9 | import dev.shadoe.delta.data.softap.SoftApStateStore 10 | import javax.inject.Inject 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | 16 | @HiltViewModel 17 | class DebugViewModel 18 | @Inject 19 | constructor( 20 | softApStateStore: SoftApStateStore, 21 | private val flagsRepository: FlagsRepository, 22 | private val macAddressCacheRepository: MacAddressCacheRepository, 23 | ) : ViewModel() { 24 | val config = softApStateStore.config 25 | val status = softApStateStore.status 26 | val flags = MutableStateFlow("") 27 | val macAddressCache = MutableStateFlow("") 28 | 29 | init { 30 | viewModelScope.launch { 31 | withContext(Dispatchers.IO) { 32 | flags.value = 33 | flagsRepository 34 | .debugDumpFlags() 35 | .map { ConfigFlag.entries[it.flag].name to it.value } 36 | .toString() 37 | macAddressCache.value = 38 | macAddressCacheRepository.debugDumpCache().toString() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/design/AppTheme.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.design 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.platform.LocalContext 12 | 13 | @Composable 14 | fun AppTheme(content: @Composable () -> Unit) { 15 | val colorScheme = 16 | if (isSystemInDarkTheme()) { 17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 18 | dynamicDarkColorScheme(LocalContext.current) 19 | } else { 20 | darkColorScheme() 21 | } 22 | } else { 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 24 | dynamicLightColorScheme(LocalContext.current) 25 | } else { 26 | lightColorScheme() 27 | } 28 | } 29 | MaterialTheme(colorScheme = colorScheme, typography = Typography.value) { 30 | content() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/design/Typography.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.design 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | object Typography { 6 | private val defaultTypography = Typography() 7 | 8 | private val displayLarge = 9 | defaultTypography.displayLarge.copy(fontFamily = VariableFontFamily) 10 | private val displayMedium = 11 | defaultTypography.displayMedium.copy(fontFamily = VariableFontFamily) 12 | private val displaySmall = 13 | defaultTypography.displaySmall.copy(fontFamily = VariableFontFamily) 14 | private val headlineLarge = 15 | defaultTypography.headlineLarge.copy(fontFamily = VariableFontFamily) 16 | private val headlineMedium = 17 | defaultTypography.headlineMedium.copy(fontFamily = VariableFontFamily) 18 | private val headlineSmall = 19 | defaultTypography.headlineSmall.copy(fontFamily = VariableFontFamily) 20 | private val titleLarge = 21 | defaultTypography.titleLarge.copy(fontFamily = VariableFontFamily) 22 | private val titleMedium = 23 | defaultTypography.titleMedium.copy(fontFamily = VariableFontFamily) 24 | private val titleSmall = 25 | defaultTypography.titleSmall.copy(fontFamily = VariableFontFamily) 26 | private val bodyLarge = 27 | defaultTypography.bodyLarge.copy(fontFamily = VariableFontFamily) 28 | private val bodyMedium = 29 | defaultTypography.bodyMedium.copy(fontFamily = VariableFontFamily) 30 | private val bodySmall = 31 | defaultTypography.bodySmall.copy(fontFamily = VariableFontFamily) 32 | private val labelLarge = 33 | defaultTypography.labelLarge.copy(fontFamily = VariableFontFamily) 34 | private val labelMedium = 35 | defaultTypography.labelMedium.copy(fontFamily = VariableFontFamily) 36 | private val labelSmall = 37 | defaultTypography.labelSmall.copy(fontFamily = VariableFontFamily) 38 | 39 | val value = 40 | Typography( 41 | displayLarge = displayLarge, 42 | displayMedium = displayMedium, 43 | displaySmall = displaySmall, 44 | headlineLarge = headlineLarge, 45 | headlineMedium = headlineMedium, 46 | headlineSmall = headlineSmall, 47 | titleLarge = titleLarge, 48 | titleMedium = titleMedium, 49 | titleSmall = titleSmall, 50 | bodyLarge = bodyLarge, 51 | bodyMedium = bodyMedium, 52 | bodySmall = bodySmall, 53 | labelLarge = labelLarge, 54 | labelMedium = labelMedium, 55 | labelSmall = labelSmall, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/UpdateResults.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings 2 | 3 | import dev.shadoe.delta.data.softap.validators.PassphraseValidator 4 | import dev.shadoe.delta.data.softap.validators.SsidValidator 5 | 6 | data class UpdateResults( 7 | val ssidResult: SsidValidator.Result = SsidValidator.Result.Success, 8 | val passphraseResult: PassphraseValidator.Result = 9 | PassphraseValidator.Result.Success, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/AutoShutdownField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.SettingsPower 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import dev.shadoe.delta.R 19 | 20 | @Composable 21 | internal fun AutoShutdownField( 22 | isAutoShutdownEnabled: Boolean, 23 | onAutoShutdownChange: (Boolean) -> Unit, 24 | ) { 25 | Row( 26 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), 27 | verticalAlignment = Alignment.CenterVertically, 28 | ) { 29 | Icon( 30 | imageVector = Icons.Rounded.SettingsPower, 31 | contentDescription = stringResource(R.string.auto_shutdown_field_icon), 32 | ) 33 | Column( 34 | modifier = Modifier.weight(1f).padding(horizontal = 16.dp), 35 | horizontalAlignment = Alignment.Start, 36 | ) { 37 | Text( 38 | text = stringResource(R.string.auto_shutdown_field_title), 39 | style = MaterialTheme.typography.titleLarge, 40 | ) 41 | Text( 42 | text = stringResource(R.string.auto_shutdown_field_desc), 43 | style = MaterialTheme.typography.bodyMedium, 44 | color = MaterialTheme.colorScheme.onSurfaceVariant, 45 | ) 46 | } 47 | Switch( 48 | checked = isAutoShutdownEnabled, 49 | onCheckedChange = { onAutoShutdownChange(it) }, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/HiddenHotspotField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.WifiFind 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import dev.shadoe.delta.R 19 | 20 | @Composable 21 | internal fun HiddenHotspotField( 22 | isHiddenHotspotEnabled: Boolean, 23 | onHiddenHotspotChange: (Boolean) -> Unit, 24 | ) { 25 | Row( 26 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), 27 | verticalAlignment = Alignment.CenterVertically, 28 | ) { 29 | Icon( 30 | imageVector = Icons.Rounded.WifiFind, 31 | contentDescription = stringResource(R.string.hidden_network_field_icon), 32 | ) 33 | Column( 34 | modifier = Modifier.weight(1f).padding(horizontal = 16.dp), 35 | horizontalAlignment = Alignment.Start, 36 | ) { 37 | Text( 38 | text = stringResource(R.string.hidden_network_field_label), 39 | style = MaterialTheme.typography.titleLarge, 40 | ) 41 | Text( 42 | text = stringResource(R.string.hidden_network_field_desc), 43 | style = MaterialTheme.typography.bodyMedium, 44 | color = MaterialTheme.colorScheme.onSurfaceVariant, 45 | ) 46 | } 47 | Switch( 48 | checked = isHiddenHotspotEnabled, 49 | onCheckedChange = { onHiddenHotspotChange(it) }, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/MaxClientLimitField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.DesktopAccessDisabled 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Slider 12 | import androidx.compose.material3.SliderDefaults 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import dev.shadoe.delta.R 20 | 21 | @Composable 22 | internal fun MaxClientLimitField( 23 | allowedLimit: Int, 24 | maxClient: Int, 25 | onMaxClientChange: (Float) -> Unit, 26 | ) { 27 | Row( 28 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), 29 | verticalAlignment = Alignment.CenterVertically, 30 | ) { 31 | Icon( 32 | imageVector = Icons.Rounded.DesktopAccessDisabled, 33 | contentDescription = stringResource(R.string.max_client_limit_field_icon), 34 | ) 35 | Column( 36 | modifier = Modifier.weight(1f).padding(horizontal = 16.dp), 37 | horizontalAlignment = Alignment.Start, 38 | ) { 39 | Text( 40 | text = stringResource(R.string.max_client_limit_field_label), 41 | style = MaterialTheme.typography.titleLarge, 42 | ) 43 | Slider( 44 | value = allowedLimit.toFloat(), 45 | onValueChange = { onMaxClientChange(it) }, 46 | valueRange = 1f..maxClient.toFloat(), 47 | steps = maxClient.coerceIn(1, 5), 48 | colors = 49 | SliderDefaults.colors( 50 | thumbColor = MaterialTheme.colorScheme.secondary, 51 | activeTrackColor = MaterialTheme.colorScheme.secondary, 52 | inactiveTrackColor = MaterialTheme.colorScheme.secondaryContainer, 53 | ), 54 | ) 55 | Text( 56 | text = 57 | stringResource(R.string.max_client_limit_field_count, allowedLimit), 58 | modifier = Modifier.align(alignment = Alignment.CenterHorizontally), 59 | color = MaterialTheme.colorScheme.onSurfaceVariant, 60 | ) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/PresetField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 6 | import androidx.compose.foundation.layout.FlowRow 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.rounded.History 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import dev.shadoe.delta.R 22 | 23 | @Composable 24 | internal fun PresetField(onShowPresets: () -> Unit, onSaveConfig: () -> Unit) { 25 | Row( 26 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), 27 | verticalAlignment = Alignment.CenterVertically, 28 | ) { 29 | Icon( 30 | imageVector = Icons.Rounded.History, 31 | contentDescription = stringResource(R.string.presets_field_icon), 32 | ) 33 | Column( 34 | modifier = Modifier.weight(1f).padding(horizontal = 16.dp), 35 | horizontalAlignment = Alignment.Start, 36 | ) { 37 | Text( 38 | text = stringResource(R.string.presets_setting), 39 | style = MaterialTheme.typography.titleLarge, 40 | ) 41 | Text( 42 | text = stringResource(R.string.presets_field_desc), 43 | modifier = Modifier.padding(vertical = 4.dp), 44 | style = MaterialTheme.typography.bodyMedium, 45 | color = MaterialTheme.colorScheme.onSurfaceVariant, 46 | ) 47 | @OptIn(ExperimentalLayoutApi::class) 48 | FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 49 | Button(onClick = onShowPresets) { 50 | Text(text = stringResource(R.string.presets_show_button)) 51 | } 52 | Button(modifier = Modifier, onClick = onSaveConfig) { 53 | Text(text = stringResource(R.string.presets_save_config_button)) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/PresetSaveDialog.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.material3.TextField 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.res.stringResource 13 | import dev.shadoe.delta.R 14 | 15 | @Composable 16 | fun PresetSaveDialog(onSave: (String) -> Unit, onDismiss: () -> Unit) { 17 | var presetName by remember { mutableStateOf("") } 18 | AlertDialog( 19 | onDismissRequest = onDismiss, 20 | confirmButton = { 21 | TextButton( 22 | onClick = { 23 | if (presetName.isNotEmpty()) { 24 | onSave(presetName) 25 | } 26 | } 27 | ) { 28 | Text(stringResource(R.string.save_button)) 29 | } 30 | }, 31 | dismissButton = { 32 | TextButton(onClick = onDismiss) { 33 | Text(stringResource(R.string.cancel_button)) 34 | } 35 | }, 36 | title = { Text(stringResource(R.string.preset_save_title)) }, 37 | text = { 38 | TextField( 39 | value = presetName, 40 | onValueChange = { presetName = it }, 41 | singleLine = true, 42 | supportingText = { 43 | if (presetName.isEmpty()) { 44 | Text(stringResource(R.string.preset_save_empty)) 45 | } 46 | }, 47 | isError = presetName.isEmpty(), 48 | ) 49 | }, 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/SsidField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.outlined.Error 10 | import androidx.compose.material.icons.rounded.Wifi 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.input.ImeAction 20 | import androidx.compose.ui.text.input.KeyboardCapitalization 21 | import androidx.compose.ui.text.input.KeyboardType 22 | import androidx.compose.ui.unit.dp 23 | import dev.shadoe.delta.R 24 | import dev.shadoe.delta.data.softap.validators.SsidValidator 25 | 26 | @Composable 27 | internal fun SsidField( 28 | ssid: String, 29 | onSsidChange: (String) -> Unit, 30 | currentResult: SsidValidator.Result, 31 | ) { 32 | Row( 33 | verticalAlignment = Alignment.CenterVertically, 34 | modifier = Modifier.padding(vertical = 8.dp), 35 | ) { 36 | Icon( 37 | imageVector = Icons.Rounded.Wifi, 38 | contentDescription = stringResource(R.string.ssid_field_icon), 39 | ) 40 | Column(modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth()) { 41 | OutlinedTextField( 42 | value = ssid, 43 | onValueChange = { onSsidChange(it) }, 44 | singleLine = true, 45 | keyboardOptions = 46 | KeyboardOptions( 47 | capitalization = KeyboardCapitalization.None, 48 | autoCorrectEnabled = false, 49 | keyboardType = KeyboardType.Text, 50 | imeAction = ImeAction.Next, 51 | ), 52 | modifier = Modifier.fillMaxWidth(), 53 | label = { Text(text = stringResource(R.string.ssid_field_label)) }, 54 | ) 55 | if (currentResult !is SsidValidator.Result.Success) { 56 | Row(modifier = Modifier.padding(top = 8.dp)) { 57 | Icon( 58 | imageVector = Icons.Outlined.Error, 59 | contentDescription = stringResource(R.string.error_icon), 60 | tint = MaterialTheme.colorScheme.error, 61 | ) 62 | Text( 63 | text = 64 | when (currentResult) { 65 | is SsidValidator.Result.SsidTooShort -> 66 | stringResource(R.string.ssid_too_short_warning) 67 | is SsidValidator.Result.SsidTooLong -> 68 | stringResource(R.string.ssid_too_long_warning) 69 | else -> "" 70 | }, 71 | color = MaterialTheme.colorScheme.error, 72 | modifier = Modifier.padding(start = 4.dp), 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/settings/components/TaskerIntegrationField.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.settings.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.Autorenew 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import dev.shadoe.delta.R 20 | 21 | @Composable 22 | internal fun TaskerIntegrationField( 23 | isTaskerIntegrationEnabled: Boolean, 24 | onTaskerIntegrationChange: (Boolean) -> Unit, 25 | onShowTaskerIntegrationInfo: () -> Unit, 26 | ) { 27 | Row( 28 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), 29 | verticalAlignment = Alignment.CenterVertically, 30 | ) { 31 | Icon( 32 | imageVector = Icons.Rounded.Autorenew, 33 | contentDescription = 34 | stringResource(R.string.tasker_integration_field_icon), 35 | ) 36 | Column( 37 | modifier = Modifier.weight(1f).padding(horizontal = 16.dp), 38 | horizontalAlignment = Alignment.Start, 39 | ) { 40 | Text( 41 | text = stringResource(R.string.tasker_integration_field_label), 42 | style = MaterialTheme.typography.titleLarge, 43 | ) 44 | Text( 45 | text = stringResource(R.string.tasker_integration_field_desc), 46 | style = MaterialTheme.typography.bodyMedium, 47 | color = MaterialTheme.colorScheme.onSurfaceVariant, 48 | ) 49 | Button(onClick = onShowTaskerIntegrationInfo) { 50 | Text(text = stringResource(R.string.tasker_integration_info_button)) 51 | } 52 | } 53 | Switch( 54 | checked = isTaskerIntegrationEnabled, 55 | onCheckedChange = onTaskerIntegrationChange, 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/setup/ShizukuSetupViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.setup 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import dev.shadoe.delta.data.shizuku.ShizukuRepository 6 | import javax.inject.Inject 7 | 8 | @HiltViewModel 9 | class ShizukuSetupViewModel 10 | @Inject 11 | constructor(private val shizukuRepository: ShizukuRepository) : ViewModel() { 12 | val shizukuState = shizukuRepository.shizukuState 13 | 14 | fun requestPermission() { 15 | shizukuRepository.requestPermission() 16 | } 17 | 18 | companion object { 19 | const val SHIZUKU_APP_ID = ShizukuRepository.SHIZUKU_APP_ID 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/setup/components/ShizukuConnected.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.setup.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import dev.shadoe.delta.R 17 | 18 | @Composable 19 | internal fun ShizukuConnected( 20 | continueAction: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | ) { 23 | Column(modifier = modifier) { 24 | Column( 25 | modifier = Modifier.weight(0.5f), 26 | verticalArrangement = Arrangement.Center, 27 | ) { 28 | Text( 29 | text = stringResource(R.string.shizuku_setup_title), 30 | style = MaterialTheme.typography.displaySmall, 31 | ) 32 | Text( 33 | text = stringResource(R.string.shizuku_setup_connected), 34 | style = MaterialTheme.typography.titleLarge, 35 | ) 36 | } 37 | Box( 38 | modifier = Modifier.weight(0.7f).fillMaxWidth(), 39 | contentAlignment = Alignment.CenterEnd, 40 | ) { 41 | Button(onClick = continueAction) { 42 | Text(text = stringResource(R.string.setup_continue_button)) 43 | } 44 | } 45 | Text( 46 | text = stringResource(R.string.shizuku_connected_desc), 47 | style = MaterialTheme.typography.bodySmall, 48 | color = MaterialTheme.colorScheme.onSurfaceVariant, 49 | modifier = Modifier.padding(bottom = 32.dp), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/setup/components/ShizukuNotConnected.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.setup.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import dev.shadoe.delta.R 17 | 18 | @Composable 19 | internal fun ShizukuNotConnected( 20 | onRequestPermission: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | ) { 23 | Column(modifier = modifier) { 24 | Column( 25 | modifier = Modifier.weight(0.5f), 26 | verticalArrangement = Arrangement.Center, 27 | ) { 28 | Text( 29 | text = stringResource(R.string.shizuku_setup_title), 30 | style = MaterialTheme.typography.displaySmall, 31 | ) 32 | Text( 33 | text = stringResource(R.string.shizuku_setup_not_connected), 34 | style = MaterialTheme.typography.titleLarge, 35 | ) 36 | } 37 | Box( 38 | modifier = Modifier.weight(0.7f).fillMaxWidth(), 39 | contentAlignment = Alignment.CenterEnd, 40 | ) { 41 | Button(onClick = onRequestPermission) { 42 | Text(text = stringResource(R.string.shizuku_grant_access)) 43 | } 44 | } 45 | Text( 46 | text = stringResource(R.string.setup_note), 47 | style = MaterialTheme.typography.bodySmall, 48 | color = MaterialTheme.colorScheme.onSurfaceVariant, 49 | modifier = Modifier.padding(bottom = 32.dp), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/shadoe/delta/setup/components/ShizukuNotRunning.kt: -------------------------------------------------------------------------------- 1 | package dev.shadoe.delta.setup.components 2 | 3 | import android.content.Intent 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalConfiguration 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import dev.shadoe.delta.R 20 | import dev.shadoe.delta.setup.ShizukuSetupViewModel 21 | 22 | @Composable 23 | internal fun ShizukuNotRunning(modifier: Modifier = Modifier) { 24 | val context = LocalContext.current 25 | val isBigScreen = LocalConfiguration.current.screenWidthDp >= 700 26 | Column(modifier = modifier) { 27 | Column( 28 | modifier = Modifier.weight(0.5f), 29 | verticalArrangement = Arrangement.Center, 30 | ) { 31 | Text( 32 | text = stringResource(R.string.shizuku_setup_title), 33 | style = MaterialTheme.typography.displaySmall, 34 | ) 35 | Text( 36 | text = stringResource(R.string.shizuku_setup_not_running), 37 | style = MaterialTheme.typography.titleLarge, 38 | ) 39 | } 40 | Box( 41 | modifier = Modifier.weight(0.7f).fillMaxWidth(), 42 | contentAlignment = Alignment.CenterEnd, 43 | ) { 44 | Button( 45 | onClick = { 46 | with(context) { 47 | startActivity( 48 | packageManager 49 | .getLaunchIntentForPackage(ShizukuSetupViewModel.SHIZUKU_APP_ID) 50 | ?.apply { 51 | if (isBigScreen) { 52 | addFlags( 53 | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or 54 | Intent.FLAG_ACTIVITY_NEW_TASK 55 | ) 56 | } 57 | } 58 | ) 59 | } 60 | } 61 | ) { 62 | Text(text = stringResource(R.string.shizuku_start)) 63 | } 64 | } 65 | Text( 66 | text = stringResource(R.string.setup_note), 67 | style = MaterialTheme.typography.bodySmall, 68 | color = MaterialTheme.colorScheme.onSurfaceVariant, 69 | modifier = Modifier.padding(bottom = 32.dp), 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qs_tile.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shizuku_logo_mono.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 15 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/font-v26/nunitosans_italic_variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershadoe/delta/2cc85f133800794b6056a77b430367e0f1a973bf/app/src/main/res/font-v26/nunitosans_italic_variable.ttf -------------------------------------------------------------------------------- /app/src/main/res/font-v26/nunitosans_variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershadoe/delta/2cc85f133800794b6056a77b430367e0f1a973bf/app/src/main/res/font-v26/nunitosans_variable.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-af-rZA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar-rSA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-ca-rES/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-cs-rCZ/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-da-rDK/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-el-rGR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-fi-rFI/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr-rFR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-hu-rHU/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-it-rIT/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-iw-rIL/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko-rKR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |