├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── feature-request.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rbn │ │ └── qtsettings │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── rbn │ │ │ └── qtsettings │ │ │ ├── MainActivity.kt │ │ │ ├── data │ │ │ ├── DnsHostnameEntry.kt │ │ │ └── PreferencesManager.kt │ │ │ ├── services │ │ │ ├── PrivateDnsTileService.kt │ │ │ └── UsbDebuggingTileService.kt │ │ │ ├── ui │ │ │ ├── composables │ │ │ │ ├── AboutDialog.kt │ │ │ │ ├── AdbInstructionDialog.kt │ │ │ │ ├── CheckboxItem.kt │ │ │ │ ├── DnsSettingsCard.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── PermissionGrantDialog.kt │ │ │ │ └── UsbDebuggingSettingsCard.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── utils │ │ │ ├── Constants.kt │ │ │ └── PermissionUtils.kt │ │ │ └── viewmodel │ │ │ └── MainViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_dns_auto.xml │ │ ├── ic_dns_off.xml │ │ ├── ic_dns_on.xml │ │ ├── ic_dns_on_adguard.xml │ │ ├── ic_dns_on_cloudflare.xml │ │ ├── ic_dns_on_quad9_security.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_usb_off.xml │ │ └── ic_usb_on.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-de │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── rbn │ └── qtsettings │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── de-DE │ ├── changelogs │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── 4.txt │ │ └── 5.txt │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── en-US │ ├── changelogs │ ├── 1.txt │ ├── 2.txt │ ├── 3.txt │ ├── 4.txt │ └── 5.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── 8.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🪲 Bug Report 2 | description: File a bug report to help us improve Quick-Tile Settings. 3 | title: "Bug: [Short description of the bug]" 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! Please provide as much detail as possible. 10 | 11 | - type: checkboxes 12 | id: prerequisites 13 | attributes: 14 | label: Prerequisites 15 | description: Please confirm the following before submitting the bug. 16 | options: 17 | - label: I have searched the [existing issues](https://github.com/RBN-Apps/Quick-Tile-Settings/issues) to make sure this bug has not already been reported. 18 | required: true 19 | - label: I have granted the `android.permission.WRITE_SECURE_SETTINGS` permission via ADB as described in the app's help dialog or README. (If not, this is likely the cause). 20 | required: false 21 | - label: If this bug is related to the USB Debugging tile, I have ensured Developer Options are enabled on my device. 22 | required: false 23 | 24 | - type: textarea 25 | id: bug-description 26 | attributes: 27 | label: Describe the Bug 28 | description: A clear and concise description of what the bug is. 29 | placeholder: "When I tap the Private DNS tile, it doesn't change state..." 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: steps-to-reproduce 35 | attributes: 36 | label: Steps to Reproduce 37 | description: Please provide detailed steps to reproduce the behavior. 38 | placeholder: | 39 | 1. Go to '...' 40 | 2. Click on '....' 41 | 3. Scroll down to '....' 42 | 4. See error 43 | validations: 44 | required: true 45 | 46 | - type: textarea 47 | id: expected-behavior 48 | attributes: 49 | label: Expected Behavior 50 | description: A clear and concise description of what you expected to happen. 51 | placeholder: "The Private DNS tile should cycle to 'Auto' mode." 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | id: actual-behavior 57 | attributes: 58 | label: Actual Behavior 59 | description: A clear and concise description of what actually happened. 60 | placeholder: "The tile icon flickers but the DNS state remains 'Off'." 61 | validations: 62 | required: true 63 | 64 | - type: input 65 | id: app-version 66 | attributes: 67 | label: App Version 68 | description: "Which version of Quick-Tile Settings are you using? (e.g., 1.0.1 - find in app settings or build.gradle if self-built)" 69 | placeholder: "e.g., 1.0.1" 70 | validations: 71 | required: true 72 | 73 | - type: input 74 | id: android-version 75 | attributes: 76 | label: Android Version 77 | description: "What version of Android is your device running?" 78 | placeholder: "e.g., Android 13 (Tiramisu)" 79 | validations: 80 | required: true 81 | 82 | - type: input 83 | id: device-model 84 | attributes: 85 | label: Device Model 86 | description: "What is the model of your Android device?" 87 | placeholder: "e.g., Google Pixel 7 Pro" 88 | validations: 89 | required: true 90 | 91 | - type: dropdown 92 | id: rooted 93 | attributes: 94 | label: Is your device rooted or running a custom ROM? 95 | options: 96 | - "No" 97 | - "Yes, rooted (stock ROM)" 98 | - "Yes, custom ROM (please specify below)" 99 | - "Unsure" 100 | validations: 101 | required: false 102 | 103 | - type: textarea 104 | id: logs 105 | attributes: 106 | label: Logs (Logcat) or Screenshots 107 | description: | 108 | If applicable, add screenshots to help explain your problem. 109 | For crashes or unexpected behavior, please provide relevant Logcat output. 110 | You can capture Logcat using Android Studio or via ADB: `adb logcat > logcat.txt` (then filter for `com.rbn.qtsettings` or errors). 111 | placeholder: "Paste Logcat output or describe screenshot here..." 112 | validations: 113 | required: false 114 | 115 | - type: textarea 116 | id: additional-context 117 | attributes: 118 | label: Additional Context 119 | description: Add any other context about the problem here. 120 | validations: 121 | required: false 122 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 💡Feature Request 2 | description: Suggest an idea for Quick-Tile Settings. 3 | title: "Feature: [Short description of the feature]" 4 | labels: ["feature-request", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting an idea! Please describe your feature request in detail. 10 | 11 | - type: checkboxes 12 | id: prerequisites 13 | attributes: 14 | label: Prerequisites 15 | description: Please confirm the following before submitting the feature request. 16 | options: 17 | - label: I have searched the [existing issues](https://github.com/RBN-Apps/Quick-Tile-Settings/issues) to make sure this feature has not already been requested. 18 | required: true 19 | 20 | - type: textarea 21 | id: solution-description 22 | attributes: 23 | label: Describe the feature you'd like 24 | description: A clear and concise description of what you want to happen. 25 | placeholder: "I would like the app to automatically [...] when [...]" 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: related-problem 31 | attributes: 32 | label: Is your feature request related to a problem? 33 | description: A clear and concise description of what the problem is. 34 | placeholder: "I'm always frustrated when [...]" 35 | validations: 36 | required: false 37 | 38 | - type: textarea 39 | id: importance 40 | attributes: 41 | label: Why is this feature important to you? 42 | description: Explain why this feature would be useful or valuable. 43 | validations: 44 | required: false 45 | 46 | - type: textarea 47 | id: additional-context 48 | attributes: 49 | label: Additional Context 50 | description: Add any other context, mockups, or screenshots about the feature request here. 51 | validations: 52 | required: false 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-and-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | 13 | - name: Extract VersionCode 14 | id: extract_vc 15 | run: | 16 | VERSION_CODE=$(grep "versionCode =" app/build.gradle.kts | sed 's/.*versionCode = \(.*\)/\1/' | tr -d ' ') 17 | echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV 18 | echo "Extracted versionCode: $VERSION_CODE" 19 | 20 | - name: Check changelogs 21 | run: | 22 | for lang in en-US de-DE; do 23 | file="fastlane/metadata/android/$lang/changelogs/${{ env.VERSION_CODE }}.txt" 24 | if [ ! -f "$file" ]; then 25 | echo "::error file=$file::Missing $lang changelog (versionCode ${{ env.VERSION_CODE }})" 26 | exit 1 27 | fi 28 | echo "✔ Found changelog: $file" 29 | done 30 | 31 | - name: Set up JDK 17 32 | uses: actions/setup-java@v4 33 | with: 34 | java-version: '17' 35 | distribution: 'temurin' 36 | 37 | - name: Setup Gradle 38 | uses: gradle/actions/setup-gradle@v4 39 | 40 | - name: Grant execute permission for gradlew 41 | run: chmod +x gradlew 42 | 43 | - name: Run linters 44 | run: ./gradlew lintDebug 45 | 46 | - name: Run unit tests 47 | run: ./gradlew testDebugUnitTest 48 | 49 | - name: Upload Unit Test Results (XML) 50 | if: always() 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: unit-test-results 54 | path: app/build/test-results/testDebugUnitTest/ 55 | retention-days: 7 56 | 57 | - name: Upload Lint Report (HTML) 58 | if: always() 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: lint-report-debug-html 62 | path: app/build/reports/lint-results-debug.html 63 | retention-days: 7 64 | 65 | - name: Upload Lint Report (XML) 66 | if: always() 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: lint-report-debug-xml 70 | path: app/build/reports/lint-results-debug.xml 71 | retention-days: 7 72 | 73 | - name: Build debug APK 74 | run: ./gradlew assembleDebug 75 | 76 | - name: Generate Job Summary 77 | if: always() 78 | run: | 79 | echo "## CI Job Summary" >> $GITHUB_STEP_SUMMARY 80 | echo "" >> $GITHUB_STEP_SUMMARY 81 | 82 | echo "### Lint Report (Debug)" >> $GITHUB_STEP_SUMMARY 83 | if [ -f app/build/reports/lint-results-debug.html ]; then 84 | echo "Lint issues may have been found. Download the HTML report from the artifacts section of this run." >> $GITHUB_STEP_SUMMARY 85 | echo "- [Download Lint Report (HTML)](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)" >> $GITHUB_STEP_SUMMARY 86 | else 87 | echo "No Lint HTML report found or lint task failed to produce one." >> $GITHUB_STEP_SUMMARY 88 | fi 89 | echo "" >> $GITHUB_STEP_SUMMARY 90 | 91 | # Unit Test Report Link 92 | echo "### Unit Test Results" >> $GITHUB_STEP_SUMMARY 93 | echo "Download JUnit XML reports from the artifacts section of this run." >> $GITHUB_STEP_SUMMARY 94 | echo "- [Download Unit Test Results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)" >> $GITHUB_STEP_SUMMARY 95 | echo "" >> $GITHUB_STEP_SUMMARY 96 | 97 | # Changelog Status 98 | echo "### Changelog Status (versionCode: ${{ env.VERSION_CODE }})" >> $GITHUB_STEP_SUMMARY 99 | if [ -f "fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt" ]; then 100 | echo "- ✅ en-US Changelog: Found" >> $GITHUB_STEP_SUMMARY 101 | else 102 | echo "- ❌ en-US Changelog: NOT Found" >> $GITHUB_STEP_SUMMARY 103 | fi 104 | if [ -f "fastlane/metadata/android/de-DE/changelogs/${{ env.VERSION_CODE }}.txt" ]; then 105 | echo "- ✅ de-DE Changelog: Found" >> $GITHUB_STEP_SUMMARY 106 | else 107 | echo "- ❌ de-DE Changelog: NOT Found" >> $GITHUB_STEP_SUMMARY 108 | fi -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-and-release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Checkout current code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | 23 | - name: Setup Gradle 24 | uses: gradle/actions/setup-gradle@v4 25 | 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | 29 | - name: Extract Current VersionName and VersionCode 30 | id: extract_current_versions 31 | run: | 32 | VERSION_NAME=$(grep "versionName =" app/build.gradle.kts | sed 's/.*versionName = "\(.*\)".*/\1/') 33 | VERSION_CODE=$(grep "versionCode =" app/build.gradle.kts | sed 's/.*versionCode = \(.*\)/\1/' | tr -d ' ') 34 | echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV 35 | echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV 36 | echo "Current versionName: $VERSION_NAME, Current versionCode: $VERSION_CODE" 37 | 38 | - name: Validate Tag against VersionName 39 | run: | 40 | TAG_NAME=${{ github.ref_name }} 41 | TAG_VERSION=${TAG_NAME#v} 42 | if [ "$TAG_VERSION" != "${{ env.VERSION_NAME }}" ]; then 43 | echo "::error::Tag version ($TAG_VERSION) does not match versionName (${{ env.VERSION_NAME }}) in app/build.gradle.kts." 44 | exit 1 45 | else 46 | echo "Tag version ($TAG_VERSION) matches versionName (${{ env.VERSION_NAME }})." 47 | fi 48 | 49 | - name: Check VersionCode against previous release 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | CURRENT_VERSION_CODE: ${{ env.VERSION_CODE }} 53 | run: | 54 | PREVIOUS_RELEASE_TAG=$(gh release list --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName') 55 | 56 | if [ -z "$PREVIOUS_RELEASE_TAG" ] || [ "$PREVIOUS_RELEASE_TAG" == "null" ]; then 57 | echo "No previous stable release found. Assuming this is the first release or only pre-releases/drafts exist. Skipping versionCode check against previous." 58 | else 59 | echo "Previous stable release tag: $PREVIOUS_RELEASE_TAG" 60 | 61 | mkdir ../previous_release_code 62 | git clone https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/$GITHUB_REPOSITORY.git ../previous_release_code --branch $PREVIOUS_RELEASE_TAG --depth 1 63 | 64 | if [ ! -f ../previous_release_code/app/build.gradle.kts ]; then 65 | echo "::error::app/build.gradle.kts not found in previous release tag $PREVIOUS_RELEASE_TAG. Cannot compare versionCode." 66 | echo "Continuing without previous versionCode check due to missing file in old tag." 67 | else 68 | PREVIOUS_VERSION_CODE=$(grep "versionCode =" ../previous_release_code/app/build.gradle.kts | sed 's/.*versionCode = \(.*\)/\1/' | tr -d ' ') 69 | echo "Previous versionCode: $PREVIOUS_VERSION_CODE" 70 | echo "Current versionCode: $CURRENT_VERSION_CODE" 71 | 72 | if [ "$PREVIOUS_VERSION_CODE" == "$CURRENT_VERSION_CODE" ]; then 73 | echo "::error::Current versionCode ($CURRENT_VERSION_CODE) is the same as in the previous release ($PREVIOUS_RELEASE_TAG). Please increment versionCode." 74 | exit 1 75 | elif [ "$((PREVIOUS_VERSION_CODE))" -gt "$((CURRENT_VERSION_CODE))" ]; then 76 | echo "::warning::Current versionCode ($CURRENT_VERSION_CODE) is lower than in the previous release ($PREVIOUS_RELEASE_TAG - $PREVIOUS_VERSION_CODE). Please increment versionCode." 77 | exit 1 78 | else 79 | echo "VersionCode ($CURRENT_VERSION_CODE) has been incremented correctly from previous release ($PREVIOUS_VERSION_CODE)." 80 | fi 81 | fi 82 | rm -rf ../previous_release_code 83 | fi 84 | 85 | - name: Check for en-US changelog for release 86 | id: changelog_check_en 87 | run: | 88 | CHANGELOG_FILE="fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt" 89 | if [ ! -f "$CHANGELOG_FILE" ]; then 90 | echo "::error file=$CHANGELOG_FILE::Changelog for en-US (versionCode ${{ env.VERSION_CODE }}) not found at $CHANGELOG_FILE. Cannot create release." 91 | exit 1 92 | else 93 | echo "Changelog $CHANGELOG_FILE found for release." 94 | CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE") 95 | echo "changelog_body<> $GITHUB_OUTPUT 96 | echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT 97 | echo "EOF" >> $GITHUB_OUTPUT 98 | fi 99 | 100 | - name: Check for de-DE changelog for release 101 | run: | 102 | CHANGELOG_FILE="fastlane/metadata/android/de-DE/changelogs/${{ env.VERSION_CODE }}.txt" 103 | if [ ! -f "$CHANGELOG_FILE" ]; then 104 | echo "::warning file=$CHANGELOG_FILE::Changelog for de-DE (versionCode ${{ env.VERSION_CODE }}) not found at $CHANGELOG_FILE. Release will use en-US changelog." 105 | else 106 | echo "Changelog $CHANGELOG_FILE found for release." 107 | fi 108 | 109 | - name: Build Release AAB and APK 110 | run: | 111 | ./gradlew bundleRelease assembleRelease 112 | 113 | - name: Sign APK 114 | id: sign_apk 115 | uses: r0adkll/sign-android-release@v1 116 | with: 117 | releaseDirectory: app/build/outputs/apk/release 118 | signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY_STORE }} 119 | alias: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} 120 | keyStorePassword: ${{ secrets.ANDROID_SIGNING_KEY_STORE_PASSWORD }} 121 | keyPassword: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} 122 | env: 123 | BUILD_TOOLS_VERSION: "35.0.0" 124 | 125 | - name: Rename APK 126 | run: | 127 | echo "Contents of APK release directory:" 128 | ls -la app/build/outputs/apk/release 129 | mv app/build/outputs/apk/release/app-release-unsigned-signed.apk app/build/outputs/apk/release/quick-tile-settings-${{ env.VERSION_NAME }}.apk 130 | 131 | - name: Create GitHub Release 132 | uses: softprops/action-gh-release@v2 133 | with: 134 | files: | 135 | app/build/outputs/apk/release/quick-tile-settings-${{ env.VERSION_NAME }}.apk 136 | body_path: fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt 137 | tag_name: ${{ github.ref_name }} 138 | name: Release ${{ env.VERSION_NAME }} 139 | draft: false 140 | env: 141 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub issues](https://img.shields.io/github/issues/RBN-Apps/Quick-Tile-Settings)](https://github.com/RBN-Apps/Quick-Tile-Settings/issues) 2 | [![GitHub last commit](https://img.shields.io/github/last-commit/RBN-Apps/Quick-Tile-Settings)](https://github.com/RBN-Apps/Quick-Tile-Settings/commits/main) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/RBN-Apps/Quick-Tile-Settings)](https://github.com/RBN-Apps/Quick-Tile-Settings/releases/latest) 4 | [![Android API](https://img.shields.io/badge/API-29%2B-brightgreen.svg)](https://android-arsenal.com/api?level=29) 5 | [![GitHub stars](https://img.shields.io/github/stars/RBN-Apps/Quick-Tile-Settings?style=social)](https://github.com/RBN-Apps/Quick-Tile-Settings/stargazers) 6 | [![GitHub forks](https://img.shields.io/github/forks/RBN-Apps/Quick-Tile-Settings?style=social)](https://github.com/RBN-Apps/Quick-Tile-Settings/network/members) 7 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RBN-Apps/Quick-Tile-Settings) 8 | 9 | # Quick-Tile Settings 10 | 11 | **Quick-Tile Settings** is an Android application that provides customizable Quick Settings tiles 12 | for managing Private DNS and USB Debugging. It allows users to quickly toggle these settings and 13 | configure the behavior of each tile, including an auto-revert feature. 14 | 15 | A significant portion of the Private DNS tile functionality and concept is inspired by Joshua 16 | Wolfsohn's [Private DNS Quick Tile](https://github.com/joshuawolfsohn/Private-DNS-Quick-Tile) app. 17 | This project aims to expand on that idea by adding more features, a USB Debugging tile, and a modern 18 | Jetpack Compose UI. 19 | 20 | ## Features 21 | 22 | * **Private DNS Quick Tile:** 23 | * Cycle through Off, Auto, and user-selected Private DNS hostnames. 24 | * Manage a list of DNS hostnames: 25 | * Includes predefined options: Cloudflare (Performance & Privacy), AdGuard DNS (Ad & Tracker 26 | Blocking), Quad9 (Security Focus). 27 | * Add, edit, and delete custom DNS hostname entries. 28 | * Select which hostnames (along with Off/Auto) are included in the tile's cycle. 29 | * View information about the benefits of each predefined DNS provider directly in the app. 30 | * Optional: Automatically revert to the previous DNS state after a configurable delay. 31 | * **USB Debugging Quick Tile:** 32 | * Toggle USB Debugging On or Off. 33 | * Configure whether to cycle between On/Off or only include specific states. 34 | * Optional: Automatically revert to the previous USB Debugging state after a configurable delay. 35 | * **User-Friendly Configuration:** 36 | * In-app settings screen to customize tile behavior and auto-revert options. 37 | * Tabbed interface for easy navigation between Private DNS and USB Debugging settings. 38 | * **Multiple Permission Granting Options:** 39 | * Clear instructions and an in-app dialog for granting the required `WRITE_SECURE_SETTINGS` 40 | permission via ADB (Android Debug Bridge), Shizuku, or Root. 41 | * Convenient "Copy Command" buttons for ADB commands. 42 | * **Modern UI:** Built with Jetpack Compose, supporting dynamic color (Material You) on Android 12+. 43 | * **Localization:** Available in English and German. 44 | 45 | ## Screenshots 46 | 47 |

48 | DNS Tab 49 | USB Tab 50 | Permission Grant Dialog 51 | Quick Tile Panel 52 |

53 | 54 | ## Requirements 55 | 56 | * Android 9 (Pie, API 29) or higher. 57 | * The `WRITE_SECURE_SETTINGS` permission. This **must** be granted using one of the methods 58 | described below as it's a protected permission not available to regular apps. 59 | 60 | ## Setup and Usage 61 | 62 | ### 1. Granting the `WRITE_SECURE_SETTINGS` Permission 63 | 64 | This app requires the `WRITE_SECURE_SETTINGS` permission to modify system settings for Private DNS 65 | and USB Debugging. This permission cannot be granted by the app itself through the standard Android 66 | permission dialog. You must use one of the following methods. The app provides an in-app dialog ( 67 | accessible via the help icon "?" in the toolbar) that guides you through these options. 68 | 69 | #### Method 1: ADB (Android Debug Bridge) - Recommended for most users without Root/Shizuku 70 | 71 | This method requires a computer with ADB set up. 72 | 73 | 1. **Enable Developer Options and USB Debugging on your Android device:** 74 | * Go to `Settings > About phone`. 75 | * Tap on `Build number` repeatedly (usually 7 times) until you see a message saying "You are now 76 | a developer!". 77 | * Go to `Settings > System > Developer options` (the location might vary slightly by device). 78 | * Enable `USB debugging`. 79 | 2. **Connect your device to your computer** via USB. 80 | 3. **Ensure ADB is installed** on your computer. (Search "install ADB" for instructions specific to 81 | your OS). 82 | 4. **Grant the permission using one of the following ADB commands:** 83 | * **Option A (If app is already installed):** 84 | ```bash 85 | adb shell pm grant com.rbn.qtsettings android.permission.WRITE_SECURE_SETTINGS 86 | ``` 87 | * **Option B (Grant during installation):** 88 | (First, uninstall the app if it's already present without the permission) 89 | ```bash 90 | adb install -g -r path/to/your/app.apk 91 | ``` 92 | 93 | Replace `path/to/your/app.apk` with the actual path to the APK file. The `-g` flag grants all 94 | runtime permissions listed in the manifest, which for this app effectively grants 95 | `WRITE_SECURE_SETTINGS` due to how it's declared. 96 | 97 | The in-app help dialog provides copy buttons for these commands. 98 | 99 | #### Method 2: Shizuku 100 | 101 | [Shizuku](https://shizuku.rikka.app/) allows apps to use system APIs with elevated privileges. If 102 | you have Shizuku installed and running (either via ADB or Root), you can grant the permission 103 | through it. 104 | 105 | 1. **Install and set up Shizuku** on your device. Follow the instructions provided by the Shizuku 106 | app. 107 | 2. Open **Quick-Tile Settings**. If Shizuku is running, the in-app permission dialog will offer a " 108 | Grant using Shizuku" option. 109 | 3. If Quick-Tile Settings doesn't yet have permission to *use* Shizuku, you'll first be prompted to 110 | grant that via a Shizuku system dialog. 111 | 4. Once Quick-Tile Settings has permission to use Shizuku, tapping "Grant using Shizuku" will 112 | attempt to set the `WRITE_SECURE_SETTINGS` permission. 113 | 114 | #### Method 3: Root 115 | 116 | If your device is rooted, the app can attempt to grant the permission using root privileges. 117 | 118 | 1. Ensure your device is properly rooted and a root management app (like Magisk) is installed and 119 | allows root access requests. 120 | 2. Open **Quick-Tile Settings**. The in-app permission dialog will detect root and offer a "Grant 121 | using Root" option. 122 | 3. Tapping this button will prompt your root management app to grant root access to Quick-Tile 123 | Settings for executing the permission grant command. 124 | 125 | **After granting the permission using any method, the app should detect it. You might need to 126 | restart the app or pull down the Quick Settings panel again for the tiles to become fully 127 | functional.** 128 | 129 | ### 2. Configure Tiles in the App 130 | 131 | 1. Open the **Quick-Tile Settings** app. 132 | 2. If the permission hasn't been granted, you'll see a warning. Use the help icon (`?`) in the 133 | toolbar to access the permission granting options. 134 | 3. Navigate to the **"Private DNS"** or **"USB Debugging"** tab. 135 | 4. **For Private DNS:** 136 | * **Select Cycle States:** Check the boxes for "DNS Off" and "DNS Auto" if you want them in the 137 | cycle. 138 | * **Manage Hostnames:** 139 | * A list of DNS providers (predefined and custom) is shown. 140 | * Check the box next to each hostname to include it in the tile's cycle. 141 | * For predefined hostnames, tap the info icon (ⓘ) to learn about their benefits. 142 | * Tap "Add Custom Hostname" to add your own DNS provider (you'll need a display name and the 143 | actual hostname). 144 | * Custom hostnames can be edited or deleted. 145 | 5. **For USB Debugging:** 146 | * **Select Cycle States:** Check "USB Debug On" and/or "USB Debug Off" to include them in the 147 | cycle. 148 | 6. **Auto-Revert (Optional):** 149 | * Enable "Enable auto-revert after a delay". 150 | * Set the "Revert delay (seconds)". The setting will revert to its previous state after this 151 | many seconds. 152 | 7. Tap the **"Apply Settings"** button (checkmark icon at the bottom). A snackbar will confirm that 153 | settings are saved. 154 | 155 | ### 3. Add Tiles to Your Quick Settings Panel 156 | 157 | 1. Pull down your notification shade fully to reveal the Quick Settings panel. 158 | 2. Tap the **"edit" icon** (often a pencil) or an option like "Edit tiles". 159 | 3. Find the **"Private DNS"** and **"USB Debugging"** tiles provided by this app (they will have the 160 | app's icon or a relevant icon for DNS/USB). 161 | 4. Drag and drop them from the "available tiles" area to your active tiles area. 162 | 5. Arrange them as desired and tap "Done" or the checkmark. 163 | 164 | ### 4. Using the Tiles 165 | 166 | * **Tap a tile** in your Quick Settings panel to cycle it to the next configured state. 167 | * **Long-press a tile** to open the Quick-Tile Settings app. 168 | 169 | ## Auto-Revert Feature Explained 170 | 171 | * When auto-revert is enabled for a tile: 172 | * Tapping the tile to change a setting will initiate a countdown timer (visible via a toast 173 | message). 174 | * If no further action is taken, the setting will automatically revert to the state it was in 175 | *before* you tapped the tile once the timer finishes. 176 | * If you tap the tile *again* before the timer finishes, the auto-revert for the *previous* 177 | change is cancelled. The tile will proceed to its next state, and if that new state change 178 | also has auto-revert configured, a *new* timer will begin. 179 | * This feature is useful for temporarily enabling a setting (like USB Debugging for a quick ADB 180 | command or you want to visit an advertisement website on purpose once) and having it automatically 181 | disable itself for security or convenience. 182 | 183 | ## Building from Source 184 | 185 | 1. Clone this repository: 186 | ```bash 187 | git clone https://github.com/RBN-Apps/Quick-Tile-Settings.git 188 | ``` 189 | 2. Open the project in Android Studio. 190 | 3. Let Gradle sync and download dependencies. 191 | 4. Build the project (e.g., `Build > Make Project` or `Run 'app'`). 192 | The APK will typically be found in `app/build/outputs/apk/debug/` or 193 | `app/build/outputs/apk/release/`. 194 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.compose) 5 | } 6 | 7 | android { 8 | namespace = "com.rbn.qtsettings" 9 | compileSdk = 35 10 | 11 | defaultConfig { 12 | applicationId = "com.rbn.qtsettings" 13 | minSdk = 29 14 | targetSdk = 35 15 | versionCode = 5 16 | versionName = "1.1.1" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = true 24 | proguardFiles( 25 | getDefaultProguardFile("proguard-android-optimize.txt"), 26 | "proguard-rules.pro" 27 | ) 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_11 32 | targetCompatibility = JavaVersion.VERSION_11 33 | } 34 | kotlinOptions { 35 | jvmTarget = "11" 36 | } 37 | buildFeatures { 38 | compose = true 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(libs.rikkax.shizuku.api) 44 | implementation(libs.rikkax.shizuku.provider) 45 | 46 | implementation(libs.androidx.core.ktx) 47 | implementation(libs.androidx.lifecycle.runtime.ktx) 48 | implementation(libs.androidx.activity.compose) 49 | implementation(platform(libs.androidx.compose.bom)) 50 | implementation(libs.androidx.ui) 51 | implementation(libs.androidx.ui.graphics) 52 | implementation(libs.androidx.ui.tooling.preview) 53 | implementation(libs.androidx.material3) 54 | implementation(libs.gson) 55 | implementation(libs.androidx.material.icons.core) 56 | implementation(libs.androidx.material.icons.extended) 57 | testImplementation(libs.junit) 58 | androidTestImplementation(libs.androidx.junit) 59 | androidTestImplementation(libs.androidx.espresso.core) 60 | androidTestImplementation(platform(libs.androidx.compose.bom)) 61 | androidTestImplementation(libs.androidx.ui.test.junit4) 62 | debugImplementation(libs.androidx.ui.tooling) 63 | debugImplementation(libs.androidx.ui.test.manifest) 64 | } 65 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/rbn/qtsettings/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.rbn.qtsettings", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | 25 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.provider.Settings 8 | import android.widget.Toast 9 | import androidx.activity.ComponentActivity 10 | import androidx.activity.compose.setContent 11 | import androidx.activity.enableEdgeToEdge 12 | import androidx.activity.viewModels 13 | import androidx.core.view.WindowCompat 14 | import androidx.lifecycle.lifecycleScope 15 | import com.rbn.qtsettings.data.PreferencesManager 16 | import com.rbn.qtsettings.ui.composables.MainScreen 17 | import com.rbn.qtsettings.ui.theme.QuickTileSettingsTheme 18 | import com.rbn.qtsettings.utils.PermissionUtils 19 | import com.rbn.qtsettings.viewmodel.MainViewModel 20 | import com.rbn.qtsettings.viewmodel.ViewModelFactory 21 | import kotlinx.coroutines.launch 22 | import rikka.shizuku.Shizuku 23 | 24 | class MainActivity : ComponentActivity() { 25 | private val viewModel: MainViewModel by viewModels { 26 | ViewModelFactory(PreferencesManager.getInstance(this.applicationContext)) 27 | } 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | WindowCompat.setDecorFitsSystemWindows(window, false) 31 | super.onCreate(savedInstanceState) 32 | enableEdgeToEdge() 33 | 34 | val tileSource = intent.getStringExtra("android.intent.extra.COMPONENT_NAME") 35 | if (tileSource != null) { 36 | if (tileSource.contains("PrivateDnsTileService")) { 37 | viewModel.setInitialTab(0) 38 | } else if (tileSource.contains("UsbDebuggingTileService")) { 39 | viewModel.setInitialTab(1) 40 | } 41 | } 42 | 43 | Shizuku.addRequestPermissionResultListener(shizukuPermissionResultListener) 44 | viewModel.checkSystemStates(this) 45 | 46 | setContent { 47 | QuickTileSettingsTheme { 48 | MainScreen( 49 | viewModel = viewModel, 50 | onOpenAdbSettings = { openUsbDebuggingSettings(this) }, 51 | onRequestShizukuPermission = { requestShizukuPermissionForApp() } 52 | ) 53 | } 54 | } 55 | 56 | lifecycleScope.launch { 57 | viewModel.permissionGrantStatus.collect { message -> 58 | message?.let { 59 | Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() 60 | viewModel.clearPermissionGrantStatus() 61 | viewModel.checkSystemStates(applicationContext) 62 | } 63 | } 64 | } 65 | } 66 | 67 | private fun openUsbDebuggingSettings(context: Context) { 68 | try { 69 | val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) 70 | context.startActivity(intent) 71 | } catch (e: Exception) { 72 | context.startActivity(Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)) 73 | } 74 | } 75 | 76 | override fun onResume() { 77 | super.onResume() 78 | viewModel.checkSystemStates(this) 79 | } 80 | 81 | override fun onDestroy() { 82 | Shizuku.removeRequestPermissionResultListener(shizukuPermissionResultListener) 83 | super.onDestroy() 84 | } 85 | 86 | private val shizukuPermissionResultListener = 87 | Shizuku.OnRequestPermissionResultListener { requestCode, grantResult -> 88 | if (requestCode == PermissionUtils.SHIZUKU_PERMISSION_REQUEST_CODE) { 89 | if (grantResult == PackageManager.PERMISSION_GRANTED) { 90 | Toast.makeText( 91 | this, 92 | getString(R.string.shizuku_permission_granted_to_app), 93 | Toast.LENGTH_SHORT 94 | ).show() 95 | viewModel.checkSystemStates(this) 96 | } else { 97 | Toast.makeText( 98 | this, 99 | getString(R.string.shizuku_permission_denied_to_app), 100 | Toast.LENGTH_SHORT 101 | ).show() 102 | } 103 | } 104 | } 105 | 106 | private fun requestShizukuPermissionForApp() { 107 | if (PermissionUtils.isShizukuAvailableAndReady()) { 108 | PermissionUtils.requestShizukuPermission(this) 109 | } else { 110 | Toast.makeText( 111 | this, 112 | getString(R.string.shizuku_not_available_or_not_ready), 113 | Toast.LENGTH_LONG 114 | ).show() 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/data/DnsHostnameEntry.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.data 2 | 3 | import java.util.UUID 4 | 5 | data class DnsHostnameEntry( 6 | val id: String = UUID.randomUUID().toString(), 7 | val name: String, 8 | val hostname: String, 9 | val isPredefined: Boolean = false, 10 | var isSelectedForCycle: Boolean = true, 11 | val descriptionResId: Int? = null 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/data/PreferencesManager.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.data 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.util.Log 6 | import androidx.core.content.edit 7 | import com.google.gson.Gson 8 | import com.google.gson.reflect.TypeToken 9 | import com.rbn.qtsettings.R 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | 14 | class PreferencesManager private constructor(context: Context) { 15 | 16 | private val gson = Gson() 17 | private val sharedPreferences: SharedPreferences = 18 | context.getSharedPreferences("qt_settings_prefs", Context.MODE_PRIVATE) 19 | 20 | // DNS Settings 21 | private val _dnsToggleOff = 22 | MutableStateFlow(sharedPreferences.getBoolean(KEY_DNS_TOGGLE_OFF, true)) 23 | val dnsToggleOff: StateFlow = _dnsToggleOff.asStateFlow() 24 | 25 | private val _dnsToggleAuto = 26 | MutableStateFlow(sharedPreferences.getBoolean(KEY_DNS_TOGGLE_AUTO, true)) 27 | val dnsToggleAuto: StateFlow = _dnsToggleAuto.asStateFlow() 28 | 29 | private val hostnameEntryListType = object : TypeToken>() {}.type 30 | private val _dnsHostnames = MutableStateFlow>(emptyList()) 31 | val dnsHostnames: StateFlow> = _dnsHostnames.asStateFlow() 32 | 33 | private val _dnsEnableAutoRevert = 34 | MutableStateFlow(sharedPreferences.getBoolean(KEY_DNS_ENABLE_AUTO_REVERT, false)) 35 | val dnsEnableAutoRevert: StateFlow = _dnsEnableAutoRevert.asStateFlow() 36 | 37 | private val _dnsAutoRevertDelaySeconds = 38 | MutableStateFlow(sharedPreferences.getInt(KEY_DNS_AUTO_REVERT_DELAY_SECONDS, 5)) 39 | val dnsAutoRevertDelaySeconds: StateFlow = _dnsAutoRevertDelaySeconds.asStateFlow() 40 | 41 | 42 | // USB Debugging Settings 43 | private val _usbToggleEnable = 44 | MutableStateFlow(sharedPreferences.getBoolean(KEY_USB_TOGGLE_ENABLE, true)) 45 | val usbToggleEnable: StateFlow = _usbToggleEnable.asStateFlow() 46 | 47 | private val _usbToggleDisable = 48 | MutableStateFlow(sharedPreferences.getBoolean(KEY_USB_TOGGLE_DISABLE, true)) 49 | val usbToggleDisable: StateFlow = _usbToggleDisable.asStateFlow() 50 | 51 | private val _usbEnableAutoRevert = 52 | MutableStateFlow(sharedPreferences.getBoolean(KEY_USB_ENABLE_AUTO_REVERT, false)) 53 | val usbEnableAutoRevert: StateFlow = _usbEnableAutoRevert.asStateFlow() 54 | private val _usbAutoRevertDelaySeconds = 55 | MutableStateFlow(sharedPreferences.getInt(KEY_USB_AUTO_REVERT_DELAY_SECONDS, 5)) 56 | val usbAutoRevertDelaySeconds: StateFlow = _usbAutoRevertDelaySeconds.asStateFlow() 57 | 58 | // Help Shown 59 | private val _helpShown = MutableStateFlow(sharedPreferences.getBoolean(KEY_HELP_SHOWN, false)) 60 | val helpShown: StateFlow = _helpShown.asStateFlow() 61 | 62 | init { 63 | loadDnsHostnames() 64 | } 65 | 66 | 67 | fun setDnsToggleOff(enabled: Boolean) { 68 | sharedPreferences.edit { putBoolean(KEY_DNS_TOGGLE_OFF, enabled) } 69 | _dnsToggleOff.value = enabled 70 | } 71 | 72 | fun setDnsToggleAuto(enabled: Boolean) { 73 | sharedPreferences.edit { putBoolean(KEY_DNS_TOGGLE_AUTO, enabled) } 74 | _dnsToggleAuto.value = enabled 75 | } 76 | 77 | fun setDnsEnableAutoRevert(enabled: Boolean) { 78 | sharedPreferences.edit { putBoolean(KEY_DNS_ENABLE_AUTO_REVERT, enabled) } 79 | _dnsEnableAutoRevert.value = enabled 80 | } 81 | 82 | fun setDnsAutoRevertDelaySeconds(delay: Int) { 83 | sharedPreferences.edit { putInt(KEY_DNS_AUTO_REVERT_DELAY_SECONDS, delay) } 84 | _dnsAutoRevertDelaySeconds.value = delay 85 | } 86 | 87 | 88 | fun setUsbToggleEnable(enabled: Boolean) { 89 | sharedPreferences.edit { putBoolean(KEY_USB_TOGGLE_ENABLE, enabled) } 90 | _usbToggleEnable.value = enabled 91 | } 92 | 93 | fun setUsbToggleDisable(enabled: Boolean) { 94 | sharedPreferences.edit { putBoolean(KEY_USB_TOGGLE_DISABLE, enabled) } 95 | _usbToggleDisable.value = enabled 96 | } 97 | 98 | fun setUsbEnableAutoRevert(enabled: Boolean) { 99 | sharedPreferences.edit { putBoolean(KEY_USB_ENABLE_AUTO_REVERT, enabled) } 100 | _usbEnableAutoRevert.value = enabled 101 | } 102 | 103 | fun setUsbAutoRevertDelaySeconds(delay: Int) { 104 | sharedPreferences.edit { putInt(KEY_USB_AUTO_REVERT_DELAY_SECONDS, delay) } 105 | _usbAutoRevertDelaySeconds.value = delay 106 | } 107 | 108 | fun setHelpShown(shown: Boolean) { 109 | sharedPreferences.edit { putBoolean(KEY_HELP_SHOWN, shown) } 110 | _helpShown.value = shown 111 | } 112 | 113 | private fun sortDnsHostnames(hostnames: List): List { 114 | return hostnames.sortedWith( 115 | compareBy( 116 | { !it.isPredefined }, 117 | { it.name } 118 | ) 119 | ) 120 | } 121 | 122 | private fun loadDnsHostnames() { 123 | val json = sharedPreferences.getString(KEY_DNS_HOSTNAMES, null) 124 | val storedHostnames = if (json != null) { 125 | try { 126 | gson.fromJson>(json, hostnameEntryListType) 127 | } catch (e: Exception) { 128 | Log.e("PreferencesManager", "Error parsing stored DNS hostnames", e) 129 | null 130 | } 131 | } else null 132 | 133 | if (storedHostnames.isNullOrEmpty()) { 134 | _dnsHostnames.value = getDefaultDnsHostnames() 135 | saveDnsHostnamesInternal() 136 | } else { 137 | val defaultPredefined = getDefaultDnsHostnames().filter { it.isPredefined } 138 | val customStored = storedHostnames.filter { !it.isPredefined } 139 | val finalPredefined = defaultPredefined.map { defaultEntry -> 140 | val storedPredefined = 141 | storedHostnames.find { it.id == defaultEntry.id && it.isPredefined } 142 | storedPredefined?.copy( 143 | name = defaultEntry.name, 144 | hostname = defaultEntry.hostname, 145 | descriptionResId = defaultEntry.descriptionResId 146 | ) ?: defaultEntry 147 | } 148 | 149 | _dnsHostnames.value = sortDnsHostnames(finalPredefined + customStored) 150 | saveDnsHostnamesInternal() 151 | } 152 | } 153 | 154 | 155 | private fun saveDnsHostnamesInternal() { 156 | val json = gson.toJson(_dnsHostnames.value) 157 | sharedPreferences.edit { putString(KEY_DNS_HOSTNAMES, json) } 158 | } 159 | 160 | fun updateDnsHostnameEntry(updatedEntry: DnsHostnameEntry) { 161 | val currentList = _dnsHostnames.value.toMutableList() 162 | val index = currentList.indexOfFirst { it.id == updatedEntry.id } 163 | if (index != -1) { 164 | currentList[index] = updatedEntry 165 | _dnsHostnames.value = sortDnsHostnames(currentList.toList()) 166 | saveDnsHostnamesInternal() 167 | } 168 | } 169 | 170 | fun addCustomDnsHostname(name: String, hostnameValue: String) { 171 | val newList = _dnsHostnames.value.toMutableList() 172 | newList.add( 173 | DnsHostnameEntry( 174 | name = name, 175 | hostname = hostnameValue, 176 | isPredefined = false, 177 | isSelectedForCycle = true 178 | ) 179 | ) 180 | _dnsHostnames.value = sortDnsHostnames(newList.toList()) 181 | saveDnsHostnamesInternal() 182 | } 183 | 184 | fun deleteCustomDnsHostname(id: String) { 185 | val currentList = _dnsHostnames.value.toMutableList() 186 | currentList.removeAll { it.id == id && !it.isPredefined } 187 | _dnsHostnames.value = sortDnsHostnames(currentList.toList()) 188 | saveDnsHostnamesInternal() 189 | } 190 | 191 | private fun getDefaultDnsHostnames(): List { 192 | return listOf( 193 | DnsHostnameEntry( 194 | id = "adguard_default", 195 | name = "AdGuard DNS", 196 | hostname = "dns.adguard.com", 197 | isPredefined = true, 198 | isSelectedForCycle = true, 199 | descriptionResId = R.string.dns_info_adguard 200 | ), 201 | DnsHostnameEntry( 202 | id = "cloudflare_default", 203 | name = "Cloudflare (1.1.1.1)", 204 | hostname = "one.one.one.one", 205 | isPredefined = true, 206 | isSelectedForCycle = true, 207 | descriptionResId = R.string.dns_info_cloudflare 208 | ), 209 | DnsHostnameEntry( 210 | id = "quad9_default", 211 | name = "Quad9 Security", 212 | hostname = "dns.quad9.net", 213 | isPredefined = true, 214 | isSelectedForCycle = true, 215 | descriptionResId = R.string.dns_info_quad9 216 | ) 217 | ) 218 | } 219 | 220 | fun isDnsToggleOffEnabled(): Boolean = sharedPreferences.getBoolean(KEY_DNS_TOGGLE_OFF, true) 221 | fun isDnsToggleAutoEnabled(): Boolean = sharedPreferences.getBoolean(KEY_DNS_TOGGLE_AUTO, true) 222 | 223 | fun getDnsHostnamesSelectedForCycle(): List { 224 | val json = sharedPreferences.getString(KEY_DNS_HOSTNAMES, null) 225 | val hostnames = if (json != null) { 226 | try { 227 | gson.fromJson(json, hostnameEntryListType) 228 | } catch (e: Exception) { 229 | emptyList() 230 | } 231 | } else getDefaultDnsHostnames() 232 | return hostnames.filter { it.isSelectedForCycle } 233 | } 234 | 235 | fun getAllDnsHostnamesBlocking(): List { 236 | val json = sharedPreferences.getString(KEY_DNS_HOSTNAMES, null) 237 | return if (json != null) { 238 | try { 239 | gson.fromJson(json, hostnameEntryListType) 240 | } catch (e: Exception) { 241 | Log.e( 242 | "PreferencesManager", 243 | "Error parsing stored DNS hostnames for blocking read", 244 | e 245 | ) 246 | getDefaultDnsHostnames() 247 | } 248 | } else { 249 | getDefaultDnsHostnames() 250 | } 251 | } 252 | 253 | fun isDnsAutoRevertEnabled(): Boolean = 254 | sharedPreferences.getBoolean(KEY_DNS_ENABLE_AUTO_REVERT, false) 255 | 256 | fun getDnsAutoRevertDelaySeconds(): Int = 257 | sharedPreferences.getInt(KEY_DNS_AUTO_REVERT_DELAY_SECONDS, 5) 258 | 259 | fun isUsbToggleEnableEnabled(): Boolean = 260 | sharedPreferences.getBoolean(KEY_USB_TOGGLE_ENABLE, true) 261 | 262 | fun isUsbToggleDisableEnabled(): Boolean = 263 | sharedPreferences.getBoolean(KEY_USB_TOGGLE_DISABLE, true) 264 | 265 | fun isUsbAutoRevertEnabled(): Boolean = 266 | sharedPreferences.getBoolean(KEY_USB_ENABLE_AUTO_REVERT, false) 267 | 268 | fun getUsbAutoRevertDelaySeconds(): Int = 269 | sharedPreferences.getInt(KEY_USB_AUTO_REVERT_DELAY_SECONDS, 5) 270 | 271 | companion object { 272 | private const val KEY_DNS_TOGGLE_OFF = "dns_toggle_off" 273 | private const val KEY_DNS_TOGGLE_AUTO = "dns_toggle_auto" 274 | private const val KEY_DNS_ENABLE_AUTO_REVERT = "dns_enable_auto_revert" 275 | private const val KEY_DNS_AUTO_REVERT_DELAY_SECONDS = "dns_auto_revert_delay_seconds" 276 | private const val KEY_DNS_HOSTNAMES = "dns_hostnames_list_v2" 277 | 278 | 279 | private const val KEY_USB_TOGGLE_ENABLE = "usb_toggle_enable" 280 | private const val KEY_USB_TOGGLE_DISABLE = "usb_toggle_disable" 281 | private const val KEY_USB_ENABLE_AUTO_REVERT = "usb_enable_auto_revert" 282 | private const val KEY_USB_AUTO_REVERT_DELAY_SECONDS = "usb_auto_revert_delay_seconds" 283 | 284 | private const val KEY_HELP_SHOWN = "help_shown_v1" 285 | 286 | const val KEY_DNS_PREVIOUS_MODE_FOR_REVERT = "dns_previous_mode_for_revert" 287 | const val KEY_DNS_PREVIOUS_HOSTNAME_FOR_REVERT = "dns_previous_hostname_for_revert" 288 | const val KEY_USB_PREVIOUS_STATE_FOR_REVERT = "usb_previous_state_for_revert" 289 | 290 | 291 | @Volatile 292 | private var INSTANCE: PreferencesManager? = null 293 | 294 | fun getInstance(context: Context): PreferencesManager { 295 | return INSTANCE ?: synchronized(this) { 296 | INSTANCE ?: PreferencesManager(context.applicationContext).also { INSTANCE = it } 297 | } 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/services/UsbDebuggingTileService.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.services 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.graphics.drawable.Icon 6 | import android.os.CountDownTimer 7 | import android.provider.Settings 8 | import android.service.quicksettings.Tile 9 | import android.service.quicksettings.TileService 10 | import android.util.Log 11 | import android.widget.Toast 12 | import androidx.core.content.edit 13 | import com.rbn.qtsettings.R 14 | import com.rbn.qtsettings.data.PreferencesManager 15 | import com.rbn.qtsettings.utils.Constants 16 | import com.rbn.qtsettings.utils.PermissionUtils 17 | 18 | class UsbDebuggingTileService : TileService() { 19 | 20 | private lateinit var prefsManager: PreferencesManager 21 | private var revertTimer: CountDownTimer? = null 22 | private val servicePrefs: SharedPreferences by lazy { 23 | applicationContext.getSharedPreferences("usb_tile_service_state", Context.MODE_PRIVATE) 24 | } 25 | 26 | override fun onCreate() { 27 | super.onCreate() 28 | prefsManager = PreferencesManager.getInstance(applicationContext) 29 | } 30 | 31 | override fun onStartListening() { 32 | super.onStartListening() 33 | updateTile() 34 | } 35 | 36 | private fun savePreviousState(isUsbEnabled: Boolean) { 37 | servicePrefs.edit { 38 | putBoolean(PreferencesManager.KEY_USB_PREVIOUS_STATE_FOR_REVERT, isUsbEnabled) 39 | } 40 | } 41 | 42 | private fun getPreviousState(): Boolean? { 43 | return if (servicePrefs.contains(PreferencesManager.KEY_USB_PREVIOUS_STATE_FOR_REVERT)) { 44 | servicePrefs.getBoolean(PreferencesManager.KEY_USB_PREVIOUS_STATE_FOR_REVERT, false) 45 | } else { 46 | null 47 | } 48 | } 49 | 50 | private fun clearPreviousState() { 51 | servicePrefs.edit { 52 | remove(PreferencesManager.KEY_USB_PREVIOUS_STATE_FOR_REVERT) 53 | } 54 | } 55 | 56 | override fun onClick() { 57 | super.onClick() 58 | cancelRevertTimerWithMessage(getString(R.string.toast_revert_cancelled)) 59 | 60 | if (!PermissionUtils.hasWriteSecureSettingsPermission(this)) { 61 | Toast.makeText(this, R.string.toast_permission_not_granted_adb, Toast.LENGTH_LONG) 62 | .show() 63 | Log.w("UsbDebuggingTile", "WRITE_SECURE_SETTINGS permission not granted.") 64 | return 65 | } 66 | 67 | if (!PermissionUtils.isDeveloperOptionsEnabled(this)) { 68 | Toast.makeText(this, R.string.toast_developer_options_disabled, Toast.LENGTH_LONG) 69 | .show() 70 | Log.w("UsbDebuggingTile", "Developer options are disabled.") 71 | updateTile() 72 | return 73 | } 74 | 75 | val currentUsbDebuggingState = 76 | Settings.Global.getInt(contentResolver, Constants.ADB_ENABLED, 0) == 1 77 | savePreviousState(currentUsbDebuggingState) 78 | 79 | val nextStatesToCycle = mutableListOf() 80 | if (prefsManager.isUsbToggleEnableEnabled()) nextStatesToCycle.add(true) 81 | if (prefsManager.isUsbToggleDisableEnabled()) nextStatesToCycle.add(false) 82 | 83 | if (nextStatesToCycle.isEmpty()) { 84 | Toast.makeText(this, R.string.toast_no_states_enabled_usb, Toast.LENGTH_SHORT).show() 85 | clearPreviousState() 86 | return 87 | } 88 | 89 | var currentConfigIndex = nextStatesToCycle.indexOf(currentUsbDebuggingState) 90 | if (currentConfigIndex == -1) { 91 | currentConfigIndex = -1 92 | } 93 | 94 | val nextConfigIndex = (currentConfigIndex + 1) % nextStatesToCycle.size 95 | val nextStateToSet = nextStatesToCycle[nextConfigIndex] 96 | 97 | try { 98 | Settings.Global.putInt( 99 | contentResolver, 100 | Constants.ADB_ENABLED, 101 | if (nextStateToSet) 1 else 0 102 | ) 103 | 104 | if (prefsManager.isUsbAutoRevertEnabled()) { 105 | val delaySeconds = prefsManager.getUsbAutoRevertDelaySeconds() 106 | if (delaySeconds > 0) { 107 | startRevertTimer(delaySeconds) 108 | val prevEnabledState = getPreviousState() ?: currentUsbDebuggingState 109 | val readablePrevState = 110 | if (prevEnabledState) getString(R.string.on_state) else getString(R.string.off_state) 111 | val toastMsg = 112 | getString(R.string.toast_reverting_usb_to, readablePrevState, delaySeconds) 113 | Toast.makeText(this, toastMsg, Toast.LENGTH_LONG).show() 114 | } else { 115 | clearPreviousState() 116 | } 117 | } else { 118 | clearPreviousState() 119 | } 120 | 121 | } catch (e: Exception) { 122 | Log.e("UsbDebuggingTile", "Error setting USB Debug: ${e.message}", e) 123 | Toast.makeText(this, R.string.toast_error_saving_settings, Toast.LENGTH_SHORT).show() 124 | clearPreviousState() 125 | } 126 | updateTile() 127 | } 128 | 129 | private fun startRevertTimer(delaySeconds: Int) { 130 | revertTimer?.cancel() 131 | revertTimer = object : CountDownTimer(delaySeconds * 1000L, 1000) { 132 | override fun onTick(millisUntilFinished: Long) { 133 | qsTile?.let { tile -> 134 | getPreviousState()?.let { prevUsbState -> 135 | val readablePrevState = 136 | if (prevUsbState) getString(R.string.on_state) else getString(R.string.off_state) 137 | tile.subtitle = getString( 138 | R.string.tile_subtitle_reverting_in_seconds, 139 | readablePrevState, 140 | millisUntilFinished / 1000 141 | ) 142 | tile.updateTile() 143 | } 144 | } 145 | } 146 | 147 | override fun onFinish() { 148 | getPreviousState()?.let { prevUsbState -> 149 | try { 150 | if (PermissionUtils.isDeveloperOptionsEnabled(applicationContext)) { 151 | Settings.Global.putInt( 152 | contentResolver, 153 | Constants.ADB_ENABLED, 154 | if (prevUsbState) 1 else 0 155 | ) 156 | Log.i( 157 | "UsbDebuggingTile", 158 | "Auto-reverted USB Debug to ${if (prevUsbState) "ON" else "OFF"}" 159 | ) 160 | val revertedStateString = 161 | if (prevUsbState) getString(R.string.on_state) else getString(R.string.off_state) 162 | Toast.makeText( 163 | applicationContext, 164 | getString(R.string.usb_state_reverted_to, revertedStateString), 165 | Toast.LENGTH_SHORT 166 | ).show() 167 | } else { 168 | Log.w( 169 | "UsbDebuggingTile", 170 | "Developer options disabled, cannot auto-revert USB debugging." 171 | ) 172 | } 173 | } catch (e: Exception) { 174 | Log.e("UsbDebuggingTile", "Error auto-reverting USB: ${e.message}", e) 175 | } finally { 176 | clearPreviousState() 177 | revertTimer = null 178 | updateTile() 179 | } 180 | } 181 | } 182 | }.start() 183 | } 184 | 185 | private fun cancelRevertTimerWithMessage(message: String?) { 186 | if (revertTimer != null) { 187 | revertTimer?.cancel() 188 | revertTimer = null 189 | clearPreviousState() 190 | if (message != null) { 191 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 192 | } 193 | Log.i("UsbDebuggingTile", "Revert timer cancelled.") 194 | updateTile() 195 | } 196 | } 197 | 198 | private fun updateTile() { 199 | val tile = qsTile ?: return 200 | 201 | if (revertTimer == null || getPreviousState() == null) { 202 | tile.subtitle = "" 203 | } 204 | 205 | if (!PermissionUtils.isDeveloperOptionsEnabled(this)) { 206 | tile.state = Tile.STATE_UNAVAILABLE 207 | tile.label = getString(R.string.usb_dev_options_off) 208 | tile.icon = Icon.createWithResource(this, R.drawable.ic_usb_off) 209 | tile.updateTile() 210 | if (revertTimer != null) { 211 | cancelRevertTimerWithMessage(getString(R.string.toast_developer_options_disabled_revert_cancelled)) 212 | } 213 | return 214 | } 215 | 216 | val adbEnabled = Settings.Global.getInt(contentResolver, Constants.ADB_ENABLED, 0) == 1 217 | 218 | if (adbEnabled) { 219 | tile.state = Tile.STATE_ACTIVE 220 | tile.label = getString(R.string.usb_state_on) 221 | tile.icon = Icon.createWithResource(this, R.drawable.ic_usb_on) 222 | } else { 223 | tile.state = Tile.STATE_INACTIVE 224 | tile.label = getString(R.string.usb_state_off) 225 | tile.icon = Icon.createWithResource(this, R.drawable.ic_usb_off) 226 | } 227 | tile.updateTile() 228 | } 229 | 230 | override fun onStopListening() { 231 | super.onStopListening() 232 | } 233 | 234 | override fun onDestroy() { 235 | super.onDestroy() 236 | revertTimer?.cancel() 237 | revertTimer = null 238 | } 239 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/AboutDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.rememberScrollState 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.automirrored.filled.OpenInNew 15 | import androidx.compose.material.icons.filled.Info 16 | import androidx.compose.material3.AlertDialog 17 | import androidx.compose.material3.ButtonDefaults 18 | import androidx.compose.material3.HorizontalDivider 19 | import androidx.compose.material3.Icon 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.OutlinedButton 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TextButton 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.platform.LocalContext 28 | import androidx.compose.ui.platform.LocalUriHandler 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.text.style.TextAlign 32 | import androidx.compose.ui.tooling.preview.Preview 33 | import androidx.compose.ui.unit.dp 34 | import com.rbn.qtsettings.R 35 | import com.rbn.qtsettings.ui.theme.QuickTileSettingsTheme 36 | 37 | @Composable 38 | fun AboutDialog( 39 | onDismissRequest: () -> Unit, 40 | onOpenPermissionDialog: () -> Unit 41 | ) { 42 | val context = LocalContext.current 43 | val uriHandler = LocalUriHandler.current 44 | val githubUrl = stringResource(R.string.github_repo_url) 45 | 46 | val versionName = try { 47 | context.packageManager.getPackageInfo(context.packageName, 0).versionName 48 | } catch (e: Exception) { 49 | "Unknown" 50 | } 51 | 52 | AlertDialog( 53 | onDismissRequest = onDismissRequest, 54 | icon = { 55 | Icon( 56 | Icons.Default.Info, 57 | contentDescription = null, 58 | tint = MaterialTheme.colorScheme.primary 59 | ) 60 | }, 61 | title = { 62 | Text( 63 | text = stringResource(R.string.app_name), 64 | style = MaterialTheme.typography.headlineSmall, 65 | fontWeight = FontWeight.Bold, 66 | textAlign = TextAlign.Center 67 | ) 68 | }, 69 | text = { 70 | Column( 71 | modifier = Modifier.verticalScroll(rememberScrollState()), 72 | horizontalAlignment = Alignment.CenterHorizontally 73 | ) { 74 | Text( 75 | text = stringResource(R.string.about_version, versionName!!), 76 | style = MaterialTheme.typography.bodyMedium, 77 | color = MaterialTheme.colorScheme.onSurfaceVariant, 78 | textAlign = TextAlign.Center 79 | ) 80 | 81 | Spacer(modifier = Modifier.height(16.dp)) 82 | HorizontalDivider() 83 | Spacer(modifier = Modifier.height(16.dp)) 84 | 85 | Text( 86 | text = stringResource(R.string.about_description), 87 | style = MaterialTheme.typography.bodyMedium, 88 | textAlign = TextAlign.Center, 89 | modifier = Modifier.padding(bottom = 16.dp) 90 | ) 91 | 92 | Text( 93 | text = stringResource(R.string.about_features_title), 94 | style = MaterialTheme.typography.titleMedium, 95 | fontWeight = FontWeight.Bold, 96 | modifier = Modifier.padding(bottom = 8.dp) 97 | ) 98 | 99 | Column( 100 | modifier = Modifier.fillMaxWidth(), 101 | verticalArrangement = Arrangement.spacedBy(4.dp) 102 | ) { 103 | Text( 104 | text = stringResource(R.string.about_feature_private_dns), 105 | style = MaterialTheme.typography.bodySmall, 106 | modifier = Modifier.padding(start = 8.dp) 107 | ) 108 | Text( 109 | text = stringResource(R.string.about_feature_usb_debugging), 110 | style = MaterialTheme.typography.bodySmall, 111 | modifier = Modifier.padding(start = 8.dp) 112 | ) 113 | Text( 114 | text = stringResource(R.string.about_feature_auto_revert), 115 | style = MaterialTheme.typography.bodySmall, 116 | modifier = Modifier.padding(start = 8.dp) 117 | ) 118 | Text( 119 | text = stringResource(R.string.about_feature_custom_dns), 120 | style = MaterialTheme.typography.bodySmall, 121 | modifier = Modifier.padding(start = 8.dp) 122 | ) 123 | } 124 | 125 | Spacer(modifier = Modifier.height(16.dp)) 126 | HorizontalDivider() 127 | Spacer(modifier = Modifier.height(16.dp)) 128 | 129 | Text( 130 | text = stringResource(R.string.about_developer_title), 131 | style = MaterialTheme.typography.titleMedium, 132 | fontWeight = FontWeight.Bold, 133 | modifier = Modifier.padding(bottom = 8.dp) 134 | ) 135 | 136 | Text( 137 | text = stringResource(R.string.about_developer_info), 138 | style = MaterialTheme.typography.bodyMedium, 139 | textAlign = TextAlign.Center, 140 | modifier = Modifier.padding(bottom = 16.dp) 141 | ) 142 | 143 | Row( 144 | horizontalArrangement = Arrangement.spacedBy(8.dp), 145 | modifier = Modifier.fillMaxWidth() 146 | ) { 147 | OutlinedButton( 148 | onClick = { 149 | try { 150 | uriHandler.openUri(githubUrl) 151 | } catch (_: Exception) { 152 | } 153 | }, 154 | modifier = Modifier.weight(1f) 155 | ) { 156 | Text( 157 | text = stringResource(R.string.about_button_github), 158 | maxLines = 1 159 | ) 160 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 161 | Icon( 162 | Icons.AutoMirrored.Filled.OpenInNew, 163 | contentDescription = null, 164 | modifier = Modifier.size(ButtonDefaults.IconSize) 165 | ) 166 | } 167 | } 168 | 169 | Spacer(modifier = Modifier.height(8.dp)) 170 | 171 | OutlinedButton( 172 | onClick = onOpenPermissionDialog, 173 | modifier = Modifier.fillMaxWidth() 174 | ) { 175 | Text(stringResource(R.string.about_button_permission_help)) 176 | } 177 | } 178 | }, 179 | confirmButton = { 180 | TextButton(onClick = onDismissRequest) { 181 | Text(stringResource(R.string.dialog_close)) 182 | } 183 | } 184 | ) 185 | } 186 | 187 | @Preview(showBackground = true, widthDp = 380, heightDp = 800) 188 | @Composable 189 | fun AboutDialogPreview() { 190 | QuickTileSettingsTheme { 191 | AboutDialog( 192 | onDismissRequest = {}, 193 | onOpenPermissionDialog = {} 194 | ) 195 | } 196 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/AdbInstructionDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.rememberScrollState 10 | import androidx.compose.foundation.verticalScroll 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.ContentCopy 13 | import androidx.compose.material3.AlertDialog 14 | import androidx.compose.material3.ButtonDefaults 15 | import androidx.compose.material3.ElevatedButton 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TextButton 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.LinkAnnotation 25 | import androidx.compose.ui.text.SpanStyle 26 | import androidx.compose.ui.text.TextLinkStyles 27 | import androidx.compose.ui.text.buildAnnotatedString 28 | import androidx.compose.ui.text.font.FontFamily 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.text.style.TextDecoration 31 | import androidx.compose.ui.text.withLink 32 | import androidx.compose.ui.tooling.preview.Preview 33 | import androidx.compose.ui.unit.dp 34 | import com.rbn.qtsettings.R 35 | import com.rbn.qtsettings.ui.theme.QuickTileSettingsTheme 36 | 37 | @Composable 38 | fun AdbInstructionDialog( 39 | onDismissRequest: () -> Unit, 40 | onCopyToClipboard: (String) -> Unit 41 | ) { 42 | val context = LocalContext.current 43 | val adbGrantCommand = 44 | "adb shell pm grant ${context.packageName} android.permission.WRITE_SECURE_SETTINGS" 45 | val adbInstallCommand = stringResource(R.string.adb_command_install_g) 46 | val githubReleasesUrl = stringResource(id = R.string.github_releases_url) 47 | 48 | AlertDialog( 49 | onDismissRequest = onDismissRequest, 50 | title = { Text(text = stringResource(R.string.adb_instructions_title)) }, 51 | text = { 52 | Column(modifier = Modifier.verticalScroll(rememberScrollState())) { 53 | Text( 54 | text = stringResource(R.string.adb_instructions_intro), 55 | style = MaterialTheme.typography.bodyMedium 56 | ) 57 | Spacer(modifier = Modifier.height(16.dp)) 58 | 59 | // Method 1: Grant 60 | Text( 61 | text = stringResource(R.string.help_dialog_method_1_title), 62 | style = MaterialTheme.typography.titleSmall, 63 | fontWeight = FontWeight.Bold 64 | ) 65 | Spacer(modifier = Modifier.height(4.dp)) 66 | Text( 67 | text = stringResource(R.string.help_dialog_how_to_grant_via_adb), 68 | style = MaterialTheme.typography.bodyMedium 69 | ) 70 | Spacer(modifier = Modifier.height(8.dp)) 71 | Text( 72 | text = adbGrantCommand, 73 | style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), 74 | modifier = Modifier 75 | .fillMaxWidth() 76 | .padding(vertical = 4.dp) 77 | ) 78 | ElevatedButton( 79 | onClick = { onCopyToClipboard(adbGrantCommand) }, 80 | modifier = Modifier.fillMaxWidth() 81 | ) { 82 | Icon( 83 | Icons.Filled.ContentCopy, 84 | contentDescription = null, 85 | modifier = Modifier.size(ButtonDefaults.IconSize) 86 | ) 87 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 88 | Text(stringResource(R.string.button_copy_grant_command)) 89 | } 90 | Spacer(modifier = Modifier.height(24.dp)) 91 | 92 | // Method 2: Install with -g 93 | Text( 94 | text = stringResource(R.string.help_dialog_method_2_title), 95 | style = MaterialTheme.typography.titleSmall, 96 | fontWeight = FontWeight.Bold 97 | ) 98 | Spacer(modifier = Modifier.height(4.dp)) 99 | Text( 100 | text = stringResource(R.string.help_dialog_how_to_install_via_adb), 101 | style = MaterialTheme.typography.bodyMedium 102 | ) 103 | Spacer(modifier = Modifier.height(8.dp)) 104 | Text( 105 | text = adbInstallCommand, 106 | style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), 107 | modifier = Modifier 108 | .fillMaxWidth() 109 | .padding(vertical = 4.dp) 110 | ) 111 | ElevatedButton( 112 | onClick = { onCopyToClipboard(adbInstallCommand) }, 113 | modifier = Modifier.fillMaxWidth() 114 | ) { 115 | Icon( 116 | Icons.Filled.ContentCopy, 117 | contentDescription = null, 118 | modifier = Modifier.size(ButtonDefaults.IconSize) 119 | ) 120 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 121 | Text(stringResource(R.string.button_copy_install_command)) 122 | } 123 | 124 | Spacer(modifier = Modifier.height(12.dp)) 125 | 126 | val apkNoteFullText = stringResource(R.string.help_dialog_apk_path_note) 127 | val linkText = stringResource(R.string.github_releases_link_text) 128 | 129 | val annotatedString = buildAnnotatedString { 130 | append(apkNoteFullText) 131 | append("\n") 132 | withLink( 133 | LinkAnnotation.Url( 134 | url = githubReleasesUrl, 135 | styles = TextLinkStyles( 136 | style = SpanStyle( 137 | color = MaterialTheme.colorScheme.primary, 138 | textDecoration = TextDecoration.Underline 139 | ) 140 | ) 141 | ) 142 | ) { 143 | append(linkText) 144 | } 145 | } 146 | Text( 147 | text = annotatedString, 148 | style = MaterialTheme.typography.labelSmall.copy( 149 | color = MaterialTheme.colorScheme.onSurfaceVariant 150 | ), 151 | modifier = Modifier.padding(top = 6.dp) 152 | ) 153 | Spacer(modifier = Modifier.height(16.dp)) 154 | Text( 155 | text = stringResource(R.string.help_dialog_after_grant), 156 | style = MaterialTheme.typography.bodyMedium 157 | ) 158 | } 159 | }, 160 | confirmButton = { 161 | TextButton(onClick = onDismissRequest) { 162 | Text(stringResource(R.string.dialog_close)) 163 | } 164 | } 165 | ) 166 | } 167 | 168 | @Preview(showBackground = true, widthDp = 380) 169 | @Composable 170 | fun AdbInstructionDialogPreview() { 171 | QuickTileSettingsTheme { 172 | AdbInstructionDialog(onDismissRequest = {}, onCopyToClipboard = {}) 173 | } 174 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/CheckboxItem.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material3.Checkbox 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.semantics.Role 18 | import androidx.compose.ui.unit.dp 19 | 20 | 21 | @Composable 22 | fun CheckboxItem( 23 | checked: Boolean, 24 | onCheckedChange: (Boolean) -> Unit, 25 | label: String, 26 | enabled: Boolean = true 27 | ) { 28 | val interactionSource = remember { MutableInteractionSource() } 29 | Row( 30 | verticalAlignment = Alignment.CenterVertically, 31 | modifier = Modifier 32 | .fillMaxWidth() 33 | .clickable( 34 | interactionSource = interactionSource, 35 | indication = null, 36 | enabled = enabled, 37 | onClick = { onCheckedChange(!checked) }, 38 | role = Role.Checkbox 39 | ) 40 | .padding(vertical = 4.dp) 41 | ) { 42 | Checkbox( 43 | checked = checked, 44 | onCheckedChange = onCheckedChange, 45 | enabled = enabled 46 | ) 47 | Spacer(modifier = Modifier.width(8.dp)) 48 | Text( 49 | text = label, 50 | style = MaterialTheme.typography.bodyLarge, 51 | color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy( 52 | alpha = 0.38f 53 | ) 54 | ) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.widget.Toast 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.pager.HorizontalPager 15 | import androidx.compose.foundation.pager.rememberPagerState 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.automirrored.filled.HelpOutline 18 | import androidx.compose.material.icons.filled.Check 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.ButtonDefaults 21 | import androidx.compose.material3.Card 22 | import androidx.compose.material3.CardDefaults 23 | import androidx.compose.material3.ExperimentalMaterial3Api 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.IconButton 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Scaffold 28 | import androidx.compose.material3.SnackbarHost 29 | import androidx.compose.material3.SnackbarHostState 30 | import androidx.compose.material3.Tab 31 | import androidx.compose.material3.TabRow 32 | import androidx.compose.material3.Text 33 | import androidx.compose.material3.TopAppBar 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.LaunchedEffect 36 | import androidx.compose.runtime.collectAsState 37 | import androidx.compose.runtime.getValue 38 | import androidx.compose.runtime.mutableStateOf 39 | import androidx.compose.runtime.remember 40 | import androidx.compose.runtime.rememberCoroutineScope 41 | import androidx.compose.runtime.setValue 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.platform.LocalContext 45 | import androidx.compose.ui.res.stringResource 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.dp 48 | import com.rbn.qtsettings.R 49 | import com.rbn.qtsettings.ui.theme.QuickTileSettingsTheme 50 | import com.rbn.qtsettings.utils.PermissionUtils 51 | import com.rbn.qtsettings.viewmodel.MainViewModel 52 | import kotlinx.coroutines.launch 53 | 54 | 55 | @OptIn(ExperimentalMaterial3Api::class) 56 | @Composable 57 | fun MainScreen( 58 | viewModel: MainViewModel, 59 | onOpenAdbSettings: () -> Unit, 60 | onRequestShizukuPermission: () -> Unit 61 | ) { 62 | val context = LocalContext.current 63 | var showPermissionGrantDialog by remember { mutableStateOf(false) } 64 | var showAboutDialog by remember { mutableStateOf(false) } 65 | val hasWriteSecureSettings by viewModel.hasWriteSecureSettings.collectAsState() 66 | 67 | val isDevOptionsEnabled by remember { 68 | mutableStateOf(PermissionUtils.isDeveloperOptionsEnabled(context)) 69 | } 70 | 71 | val helpShown by viewModel.helpShown.collectAsState() 72 | LaunchedEffect(hasWriteSecureSettings, helpShown) { 73 | if (!hasWriteSecureSettings && !helpShown) { 74 | showPermissionGrantDialog = true 75 | viewModel.checkSystemStates(context) 76 | } 77 | } 78 | 79 | 80 | val snackbarHostState = remember { SnackbarHostState() } 81 | val coroutineScope = rememberCoroutineScope() 82 | 83 | val tabTitles = listOf( 84 | stringResource(R.string.tab_title_dns), 85 | stringResource(R.string.tab_title_usb) 86 | ) 87 | val pagerState = rememberPagerState( 88 | initialPage = viewModel.initialTab.collectAsState().value, 89 | pageCount = { tabTitles.size } 90 | ) 91 | 92 | val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 93 | 94 | 95 | Scaffold( 96 | snackbarHost = { SnackbarHost(snackbarHostState) }, 97 | topBar = { 98 | TopAppBar( 99 | title = { Text(stringResource(R.string.app_name)) }, 100 | actions = { 101 | IconButton(onClick = { 102 | if (!hasWriteSecureSettings) { 103 | showPermissionGrantDialog = true 104 | viewModel.checkSystemStates(context) 105 | } else { 106 | showAboutDialog = true 107 | } 108 | }) { 109 | Icon( 110 | Icons.AutoMirrored.Filled.HelpOutline, 111 | contentDescription = stringResource(R.string.help_button_desc) 112 | ) 113 | } 114 | } 115 | ) 116 | }, 117 | modifier = Modifier.fillMaxSize() 118 | ) { innerPadding -> 119 | Column( 120 | modifier = Modifier 121 | .padding(innerPadding) 122 | .padding(horizontal = 16.dp) 123 | .fillMaxSize() 124 | ) { 125 | if (!hasWriteSecureSettings) { 126 | PermissionWarningCard(onGrantPermissionClick = { 127 | showPermissionGrantDialog = true 128 | viewModel.checkSystemStates(context) 129 | }) 130 | } 131 | 132 | TabRow(selectedTabIndex = pagerState.currentPage) { 133 | tabTitles.forEachIndexed { index, title -> 134 | Tab( 135 | selected = pagerState.currentPage == index, 136 | onClick = { 137 | coroutineScope.launch { 138 | pagerState.animateScrollToPage(index) 139 | } 140 | }, 141 | text = { Text(title) } 142 | ) 143 | } 144 | } 145 | 146 | HorizontalPager( 147 | state = pagerState, 148 | modifier = Modifier.weight(1f) 149 | ) { page -> 150 | Column(modifier = Modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp)) { 151 | when (page) { 152 | 0 -> DnsSettingsCard(viewModel = viewModel) 153 | 1 -> UsbDebuggingSettingsCard( 154 | viewModel = viewModel, 155 | isDevOptionsEnabled = isDevOptionsEnabled 156 | ) 157 | } 158 | } 159 | } 160 | 161 | 162 | Spacer(modifier = Modifier.height(16.dp)) 163 | Button( 164 | onClick = { 165 | coroutineScope.launch { 166 | snackbarHostState.showSnackbar(context.getString(R.string.toast_settings_saved_tiles_updated)) 167 | } 168 | }, 169 | modifier = Modifier 170 | .align(Alignment.CenterHorizontally) 171 | .padding(bottom = 16.dp) 172 | ) { 173 | Icon( 174 | Icons.Filled.Check, contentDescription = null, modifier = Modifier.size( 175 | ButtonDefaults.IconSize 176 | ) 177 | ) 178 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 179 | Text(stringResource(R.string.button_save_apply_settings)) 180 | } 181 | } 182 | 183 | val isShizukuAvailable by viewModel.isShizukuAvailable.collectAsState() 184 | val appHasShizukuPermission by viewModel.appHasShizukuPermission.collectAsState() 185 | val isDeviceRooted by viewModel.isDeviceRooted.collectAsState() 186 | 187 | if (showPermissionGrantDialog) { 188 | PermissionGrantDialog( 189 | onDismissRequest = { 190 | showPermissionGrantDialog = false 191 | if (!hasWriteSecureSettings) { 192 | viewModel.setHelpShown(true) 193 | } 194 | }, 195 | onOpenDeveloperOptions = onOpenAdbSettings, 196 | onCopyToClipboard = { textToCopy -> 197 | val clip = ClipData.newPlainText("ADB Command", textToCopy) 198 | clipboardManager.setPrimaryClip(clip) 199 | Toast.makeText( 200 | context, 201 | context.getString(R.string.toast_command_copied), 202 | Toast.LENGTH_SHORT 203 | ) 204 | .show() 205 | }, 206 | onGrantWithShizuku = { viewModel.grantWriteSecureSettingsViaShizuku(context) }, 207 | onGrantWithRoot = { viewModel.grantWriteSecureSettingsViaRoot(context) }, 208 | onRequestShizukuPermission = onRequestShizukuPermission, 209 | isShizukuAvailable = isShizukuAvailable, 210 | appHasShizukuPermission = appHasShizukuPermission, 211 | isDeviceRooted = isDeviceRooted 212 | ) 213 | } 214 | } 215 | 216 | // DNS Hostname Add/Edit Dialog 217 | val editingHostname by viewModel.editingHostname.collectAsState() 218 | if (viewModel.showHostnameEditDialog.collectAsState().value) { 219 | DnsHostnameEditDialog( 220 | entry = editingHostname, 221 | onDismiss = { viewModel.dismissHostnameEditDialog() }, 222 | onSave = { id, name, hostVal -> 223 | if (id == null) { 224 | viewModel.addCustomDnsHostname(name, hostVal) 225 | } else { 226 | viewModel.editCustomDnsHostname(id, name, hostVal) 227 | } 228 | }, 229 | viewModel = viewModel 230 | ) 231 | } 232 | 233 | // Confirm Delete Hostname Dialog 234 | val hostnamePendingDeletion by viewModel.hostnamePendingDeletion.collectAsState() 235 | hostnamePendingDeletion?.let { entry -> 236 | ConfirmDeleteDialog( 237 | hostnameEntry = entry, 238 | onDismiss = { viewModel.setHostnamePendingDeletion(null) }) { 239 | viewModel.deleteCustomDnsHostname(entry.id) 240 | } 241 | } 242 | 243 | // About Dialog 244 | if (showAboutDialog) { 245 | AboutDialog( 246 | onDismissRequest = { showAboutDialog = false }, 247 | onOpenPermissionDialog = { 248 | showAboutDialog = false 249 | showPermissionGrantDialog = true 250 | viewModel.checkSystemStates(context) 251 | } 252 | ) 253 | } 254 | } 255 | 256 | @Composable 257 | fun PermissionWarningCard(onGrantPermissionClick: () -> Unit) { 258 | Card( 259 | modifier = Modifier 260 | .fillMaxWidth() 261 | .padding(vertical = 8.dp), 262 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer), 263 | elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) 264 | ) { 265 | Column(modifier = Modifier.padding(16.dp)) { 266 | Text( 267 | text = stringResource(R.string.warning_permission_missing_title), 268 | style = MaterialTheme.typography.titleMedium, 269 | color = MaterialTheme.colorScheme.onErrorContainer 270 | ) 271 | Spacer(modifier = Modifier.height(8.dp)) 272 | Text( 273 | text = stringResource(R.string.warning_permission_missing_desc), 274 | style = MaterialTheme.typography.bodyMedium, 275 | color = MaterialTheme.colorScheme.onErrorContainer 276 | ) 277 | Spacer(modifier = Modifier.height(8.dp)) 278 | Button( 279 | onClick = onGrantPermissionClick, 280 | colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) 281 | ) { 282 | Text( 283 | stringResource(R.string.button_show_permission_help), 284 | color = MaterialTheme.colorScheme.onError 285 | ) 286 | } 287 | } 288 | } 289 | } 290 | 291 | 292 | @Preview(showBackground = true) 293 | @Composable 294 | fun MainScreenPreview() { 295 | QuickTileSettingsTheme { 296 | val context = LocalContext.current 297 | val fakePrefs = com.rbn.qtsettings.data.PreferencesManager.getInstance(context) 298 | MainScreen( 299 | viewModel = MainViewModel(fakePrefs), 300 | onOpenAdbSettings = {}, 301 | onRequestShizukuPermission = {} 302 | ) 303 | } 304 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/PermissionGrantDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.rememberScrollState 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.automirrored.filled.OpenInNew 15 | import androidx.compose.material.icons.filled.DeveloperMode 16 | import androidx.compose.material3.AlertDialog 17 | import androidx.compose.material3.ButtonDefaults 18 | import androidx.compose.material3.ElevatedButton 19 | import androidx.compose.material3.HorizontalDivider 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.OutlinedButton 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.TextButton 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.Stable 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableStateOf 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.runtime.setValue 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.platform.LocalUriHandler 33 | import androidx.compose.ui.res.stringResource 34 | import androidx.compose.ui.text.font.FontWeight 35 | import androidx.compose.ui.tooling.preview.Preview 36 | import androidx.compose.ui.unit.dp 37 | import com.rbn.qtsettings.R 38 | import com.rbn.qtsettings.ui.theme.QuickTileSettingsTheme 39 | 40 | @Stable 41 | private enum class PermissionMethodType { 42 | ADB, SHIZUKU, ROOT 43 | } 44 | 45 | @Composable 46 | fun PermissionGrantDialog( 47 | onDismissRequest: () -> Unit, 48 | onOpenDeveloperOptions: () -> Unit, 49 | onCopyToClipboard: (String) -> Unit, 50 | onGrantWithShizuku: () -> Unit, 51 | onGrantWithRoot: () -> Unit, 52 | onRequestShizukuPermission: () -> Unit, 53 | isShizukuAvailable: Boolean, 54 | appHasShizukuPermission: Boolean, 55 | isDeviceRooted: Boolean 56 | ) { 57 | var showAdbInstructionsDialog by remember { mutableStateOf(false) } 58 | val uriHandler = LocalUriHandler.current 59 | val shizukuPlayStoreUrl = stringResource(id = R.string.shizuku_play_store_url) 60 | val shizukuGitHubUrl = stringResource(id = R.string.shizuku_github_releases_url) 61 | 62 | val permissionMethods = remember(isDeviceRooted, isShizukuAvailable, appHasShizukuPermission) { 63 | val methods = mutableListOf() 64 | if (isDeviceRooted && isShizukuAvailable) { 65 | methods.add(PermissionMethodType.ROOT) 66 | methods.add(PermissionMethodType.SHIZUKU) 67 | methods.add(PermissionMethodType.ADB) 68 | } else if (isDeviceRooted) { 69 | methods.add(PermissionMethodType.ROOT) 70 | methods.add(PermissionMethodType.ADB) 71 | methods.add(PermissionMethodType.SHIZUKU) 72 | } else if (isShizukuAvailable) { 73 | methods.add(PermissionMethodType.SHIZUKU) 74 | methods.add(PermissionMethodType.ADB) 75 | methods.add(PermissionMethodType.ROOT) 76 | } else { 77 | methods.add(PermissionMethodType.ADB) 78 | methods.add(PermissionMethodType.SHIZUKU) 79 | methods.add(PermissionMethodType.ROOT) 80 | } 81 | methods.distinct() 82 | } 83 | 84 | AlertDialog( 85 | onDismissRequest = onDismissRequest, 86 | title = { Text(text = stringResource(R.string.permission_grant_dialog_title)) }, 87 | text = { 88 | Column(modifier = Modifier.verticalScroll(rememberScrollState())) { 89 | Text( 90 | text = stringResource(R.string.permission_grant_dialog_intro), 91 | style = MaterialTheme.typography.bodyMedium, 92 | modifier = Modifier.padding(bottom = 16.dp) 93 | ) 94 | 95 | permissionMethods.forEachIndexed { index, methodType -> 96 | when (methodType) { 97 | PermissionMethodType.ADB -> { 98 | PermissionMethodCard( 99 | title = stringResource( 100 | R.string.permission_method_adb_title, 101 | index + 1, 102 | if (index == 0) stringResource(R.string.recommended_for_you) else "" 103 | ), 104 | description = stringResource(R.string.permission_method_adb_desc) 105 | ) { 106 | ElevatedButton( 107 | onClick = { showAdbInstructionsDialog = true }, 108 | modifier = Modifier.fillMaxWidth() 109 | ) { 110 | Text(stringResource(R.string.button_show_adb_instructions)) 111 | } 112 | } 113 | } 114 | 115 | PermissionMethodType.SHIZUKU -> { 116 | PermissionMethodCard( 117 | title = stringResource( 118 | R.string.permission_method_shizuku_title, 119 | index + 1, 120 | if (index == 0) stringResource(R.string.recommended_for_you) else "" 121 | ), 122 | description = if (!isShizukuAvailable) { 123 | stringResource(R.string.shizuku_not_available_detailed) 124 | } else if (!appHasShizukuPermission) { 125 | stringResource(R.string.shizuku_permission_not_granted_to_app_detailed) 126 | } else { 127 | stringResource(R.string.shizuku_ready_to_grant_desc) 128 | } 129 | ) { 130 | if (!isShizukuAvailable) { 131 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 132 | OutlinedButton( 133 | onClick = { 134 | try { 135 | uriHandler.openUri(shizukuPlayStoreUrl) 136 | } catch (_: Exception) { 137 | } 138 | }, 139 | modifier = Modifier.weight(1f) 140 | ) { 141 | Text(stringResource(R.string.button_open_shizuku_play_store_short)) 142 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 143 | Icon( 144 | Icons.AutoMirrored.Filled.OpenInNew, 145 | contentDescription = null, 146 | modifier = Modifier.size(ButtonDefaults.IconSize) 147 | ) 148 | } 149 | OutlinedButton( 150 | onClick = { 151 | try { 152 | uriHandler.openUri(shizukuGitHubUrl) 153 | } catch (_: Exception) { 154 | } 155 | }, 156 | modifier = Modifier.weight(1f) 157 | ) { 158 | Text(stringResource(R.string.button_open_shizuku_github_short)) 159 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 160 | Icon( 161 | Icons.AutoMirrored.Filled.OpenInNew, 162 | contentDescription = null, 163 | modifier = Modifier.size(ButtonDefaults.IconSize) 164 | ) 165 | } 166 | } 167 | } else if (!appHasShizukuPermission) { 168 | ElevatedButton( 169 | onClick = onRequestShizukuPermission, 170 | modifier = Modifier.fillMaxWidth() 171 | ) { 172 | Text(stringResource(R.string.button_request_shizuku_permission)) 173 | } 174 | } else { 175 | ElevatedButton( 176 | onClick = onGrantWithShizuku, 177 | modifier = Modifier.fillMaxWidth() 178 | ) { 179 | Text(stringResource(R.string.button_grant_with_shizuku)) 180 | } 181 | } 182 | } 183 | } 184 | 185 | PermissionMethodType.ROOT -> { 186 | PermissionMethodCard( 187 | title = stringResource( 188 | R.string.permission_method_root_title, 189 | index + 1, 190 | if (index == 0) stringResource(R.string.recommended_for_you) else "" 191 | ), 192 | description = if (!isDeviceRooted) { 193 | stringResource(R.string.device_not_rooted_detailed) 194 | } else { 195 | stringResource(R.string.root_ready_to_grant_desc) 196 | } 197 | ) { 198 | if (isDeviceRooted) { 199 | ElevatedButton( 200 | onClick = onGrantWithRoot, 201 | modifier = Modifier.fillMaxWidth() 202 | ) { 203 | Text(stringResource(R.string.button_grant_with_root)) 204 | } 205 | } 206 | } 207 | } 208 | } 209 | if (index < permissionMethods.size - 1) { 210 | Spacer(modifier = Modifier.height(16.dp)) 211 | HorizontalDivider() 212 | Spacer(modifier = Modifier.height(16.dp)) 213 | } 214 | } 215 | Spacer(modifier = Modifier.height(16.dp)) 216 | OutlinedButton( 217 | onClick = onOpenDeveloperOptions, 218 | modifier = Modifier.fillMaxWidth() 219 | ) { 220 | Icon( 221 | Icons.Default.DeveloperMode, 222 | contentDescription = null, 223 | modifier = Modifier.size(ButtonDefaults.IconSize) 224 | ) 225 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 226 | Text(stringResource(R.string.button_open_developer_options)) 227 | } 228 | } 229 | }, 230 | confirmButton = { 231 | TextButton(onClick = onDismissRequest) { 232 | Text(stringResource(R.string.dialog_close)) 233 | } 234 | } 235 | ) 236 | 237 | if (showAdbInstructionsDialog) { 238 | AdbInstructionDialog( 239 | onDismissRequest = { showAdbInstructionsDialog = false }, 240 | onCopyToClipboard = onCopyToClipboard 241 | ) 242 | } 243 | } 244 | 245 | @Composable 246 | fun PermissionMethodCard( 247 | title: String, 248 | description: String, 249 | content: @Composable () -> Unit 250 | ) { 251 | Column { 252 | Text( 253 | text = title, 254 | style = MaterialTheme.typography.titleMedium, 255 | fontWeight = FontWeight.Bold, 256 | modifier = Modifier.padding(bottom = 4.dp) 257 | ) 258 | Text( 259 | text = description, 260 | style = MaterialTheme.typography.bodyMedium, 261 | modifier = Modifier.padding(bottom = 8.dp) 262 | ) 263 | content() 264 | } 265 | } 266 | 267 | @Preview(showBackground = true, widthDp = 380, heightDp = 800) 268 | @Composable 269 | fun PermissionGrantDialogPreview_RootShizuku() { 270 | QuickTileSettingsTheme { 271 | PermissionGrantDialog( 272 | onDismissRequest = {}, 273 | onOpenDeveloperOptions = {}, 274 | onCopyToClipboard = {}, 275 | onGrantWithShizuku = {}, 276 | onGrantWithRoot = {}, 277 | onRequestShizukuPermission = {}, 278 | isShizukuAvailable = true, 279 | appHasShizukuPermission = true, 280 | isDeviceRooted = true 281 | ) 282 | } 283 | } 284 | 285 | @Preview(showBackground = true, widthDp = 380, heightDp = 800) 286 | @Composable 287 | fun PermissionGrantDialogPreview_ShizukuNeedsPerm() { 288 | QuickTileSettingsTheme { 289 | PermissionGrantDialog( 290 | onDismissRequest = {}, 291 | onOpenDeveloperOptions = {}, 292 | onCopyToClipboard = {}, 293 | onGrantWithShizuku = {}, 294 | onGrantWithRoot = {}, 295 | onRequestShizukuPermission = {}, 296 | isShizukuAvailable = true, 297 | appHasShizukuPermission = false, 298 | isDeviceRooted = false 299 | ) 300 | } 301 | } 302 | 303 | @Preview(showBackground = true, widthDp = 380, heightDp = 800) 304 | @Composable 305 | fun PermissionGrantDialogPreview_AdbOnly() { 306 | QuickTileSettingsTheme { 307 | PermissionGrantDialog( 308 | onDismissRequest = {}, 309 | onOpenDeveloperOptions = {}, 310 | onCopyToClipboard = {}, 311 | onGrantWithShizuku = {}, 312 | onGrantWithRoot = {}, 313 | onRequestShizukuPermission = {}, 314 | isShizukuAvailable = false, 315 | appHasShizukuPermission = false, 316 | isDeviceRooted = false 317 | ) 318 | } 319 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/composables/UsbDebuggingSettingsCard.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.composables 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.foundation.text.KeyboardOptions 13 | import androidx.compose.material3.Card 14 | import androidx.compose.material3.CardDefaults 15 | import androidx.compose.material3.Checkbox 16 | import androidx.compose.material3.HorizontalDivider 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.OutlinedTextField 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.input.KeyboardType 28 | import androidx.compose.ui.unit.dp 29 | import com.rbn.qtsettings.R 30 | import com.rbn.qtsettings.viewmodel.MainViewModel 31 | 32 | @Composable 33 | fun UsbDebuggingSettingsCard(viewModel: MainViewModel, isDevOptionsEnabled: Boolean) { 34 | val usbToggleEnable by viewModel.usbToggleEnable.collectAsState() 35 | val usbToggleDisable by viewModel.usbToggleDisable.collectAsState() 36 | val enableAutoRevert by viewModel.usbEnableAutoRevert.collectAsState() 37 | val autoRevertDelay by viewModel.usbAutoRevertDelaySeconds.collectAsState() 38 | 39 | Card( 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .padding(vertical = 8.dp), 43 | elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) 44 | ) { 45 | Column(modifier = Modifier.padding(16.dp)) { 46 | Text( 47 | text = stringResource(R.string.setting_title_usb_debugging), 48 | style = MaterialTheme.typography.titleLarge, 49 | modifier = Modifier.padding(bottom = 8.dp) 50 | ) 51 | Text( 52 | text = stringResource(R.string.setting_desc_tile_cycles), 53 | style = MaterialTheme.typography.bodyMedium, 54 | modifier = Modifier.padding(bottom = 16.dp) 55 | ) 56 | 57 | if (!isDevOptionsEnabled) { 58 | Text( 59 | text = stringResource(R.string.warning_developer_options_disabled_config), 60 | style = MaterialTheme.typography.bodyMedium, 61 | color = MaterialTheme.colorScheme.error, 62 | modifier = Modifier.padding(bottom = 8.dp) 63 | ) 64 | } 65 | 66 | CheckboxItem( 67 | checked = usbToggleEnable, 68 | onCheckedChange = { viewModel.setUsbToggleEnable(it) }, 69 | label = stringResource(R.string.usb_state_on), 70 | enabled = isDevOptionsEnabled 71 | ) 72 | CheckboxItem( 73 | checked = usbToggleDisable, 74 | onCheckedChange = { viewModel.setUsbToggleDisable(it) }, 75 | label = stringResource(R.string.usb_state_off), 76 | enabled = isDevOptionsEnabled 77 | ) 78 | Spacer(modifier = Modifier.height(8.dp)) 79 | if (!usbToggleDisable && !usbToggleEnable && isDevOptionsEnabled) { 80 | Text( 81 | text = stringResource(R.string.warning_at_least_one_usb_option), 82 | style = MaterialTheme.typography.bodySmall, 83 | color = MaterialTheme.colorScheme.error, 84 | modifier = Modifier.padding(top = 4.dp) 85 | ) 86 | } 87 | 88 | Spacer(modifier = Modifier.height(16.dp)) 89 | HorizontalDivider() 90 | Spacer(modifier = Modifier.height(16.dp)) 91 | 92 | // Auto-Revert Section 93 | val interactionSourceAutoRevert = remember { MutableInteractionSource() } 94 | Row( 95 | verticalAlignment = Alignment.CenterVertically, 96 | modifier = Modifier 97 | .fillMaxWidth() 98 | .clickable( 99 | interactionSource = interactionSourceAutoRevert, 100 | indication = null, 101 | onClick = { if (isDevOptionsEnabled) viewModel.setUsbEnableAutoRevert(!enableAutoRevert) }, 102 | enabled = isDevOptionsEnabled 103 | ) 104 | ) { 105 | Checkbox( 106 | checked = enableAutoRevert, 107 | onCheckedChange = { viewModel.setUsbEnableAutoRevert(it) }, 108 | enabled = isDevOptionsEnabled 109 | ) 110 | Spacer(modifier = Modifier.width(8.dp)) 111 | Text( 112 | text = stringResource(R.string.setting_enable_auto_revert), 113 | style = MaterialTheme.typography.titleMedium, 114 | color = if (isDevOptionsEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy( 115 | alpha = 0.38f 116 | ) 117 | ) 118 | } 119 | 120 | 121 | Row( 122 | verticalAlignment = Alignment.CenterVertically, 123 | modifier = Modifier.padding(top = 8.dp) 124 | ) { 125 | Text( 126 | text = stringResource(R.string.setting_auto_revert_delay), 127 | style = MaterialTheme.typography.bodyLarge, 128 | modifier = Modifier.weight(1f), 129 | color = if (enableAutoRevert && isDevOptionsEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy( 130 | alpha = 0.38f 131 | ) 132 | ) 133 | OutlinedTextField( 134 | value = autoRevertDelay.toString(), 135 | onValueChange = { value -> 136 | val newDelay = 137 | value.toIntOrNull() ?: viewModel.usbAutoRevertDelaySeconds.value 138 | viewModel.setUsbAutoRevertDelaySeconds(newDelay) 139 | }, 140 | modifier = Modifier.width(80.dp), 141 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), 142 | singleLine = true, 143 | enabled = enableAutoRevert && isDevOptionsEnabled 144 | ) 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | primary = Purple80, 16 | secondary = PurpleGrey80, 17 | tertiary = Pink80 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = Purple40, 22 | secondary = PurpleGrey40, 23 | tertiary = Pink40 24 | 25 | /* Other default colors to override 26 | background = Color(0xFFFFFBFE), 27 | surface = Color(0xFFFFFBFE), 28 | onPrimary = Color.White, 29 | onSecondary = Color.White, 30 | onTertiary = Color.White, 31 | onBackground = Color(0xFF1C1B1F), 32 | onSurface = Color(0xFF1C1B1F), 33 | */ 34 | ) 35 | 36 | @Composable 37 | fun QuickTileSettingsTheme( 38 | darkTheme: Boolean = isSystemInDarkTheme(), 39 | // Dynamic color is available on Android 12+ 40 | dynamicColor: Boolean = true, 41 | content: @Composable () -> Unit 42 | ) { 43 | val colorScheme = when { 44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 | val context = LocalContext.current 46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 | } 48 | darkTheme -> DarkColorScheme 49 | else -> LightColorScheme 50 | } 51 | 52 | MaterialTheme( 53 | colorScheme = colorScheme, 54 | typography = Typography, 55 | content = content 56 | ) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.utils 2 | 3 | object Constants { 4 | const val PRIVATE_DNS_MODE = "private_dns_mode" 5 | const val PRIVATE_DNS_SPECIFIER = "private_dns_specifier" // Hostname for DNS_MODE_ON 6 | const val ADB_ENABLED = "adb_enabled" // USB Debugging (0 or 1) 7 | const val DEVELOPMENT_SETTINGS_ENABLED = "development_settings_enabled" // Developer Options (0 or 1) 8 | 9 | const val DNS_MODE_OFF = "off" 10 | const val DNS_MODE_AUTO = "opportunistic" 11 | const val DNS_MODE_ON = "hostname" 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/utils/PermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.provider.Settings 7 | import rikka.shizuku.Shizuku 8 | import java.io.File 9 | 10 | object PermissionUtils { 11 | const val SHIZUKU_PERMISSION_REQUEST_CODE = 12345 12 | 13 | fun hasWriteSecureSettingsPermission(context: Context): Boolean { 14 | return context.checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED 15 | } 16 | 17 | fun isDeveloperOptionsEnabled(context: Context): Boolean { 18 | return Settings.Global.getInt( 19 | context.contentResolver, 20 | Constants.DEVELOPMENT_SETTINGS_ENABLED, 21 | 0 22 | ) == 1 23 | } 24 | 25 | fun isShizukuAvailableAndReady(): Boolean { 26 | return try { 27 | Shizuku.pingBinder() 28 | } catch (e: Exception) { 29 | false 30 | } 31 | } 32 | 33 | fun checkShizukuPermission(context: Context): Int { 34 | if (!isShizukuAvailableAndReady()) return PackageManager.PERMISSION_DENIED 35 | 36 | return try { 37 | if (Shizuku.isPreV11()) { 38 | if (context.checkSelfPermission("moe.shizuku.manager.permission.API_V23") == PackageManager.PERMISSION_GRANTED) { 39 | PackageManager.PERMISSION_GRANTED 40 | } else { 41 | PackageManager.PERMISSION_DENIED 42 | } 43 | } else { 44 | Shizuku.checkSelfPermission() 45 | } 46 | } catch (e: IllegalStateException) { 47 | PackageManager.PERMISSION_DENIED 48 | } 49 | } 50 | 51 | fun requestShizukuPermission(activity: Activity) { 52 | if (!isShizukuAvailableAndReady()) return 53 | 54 | try { 55 | if (Shizuku.isPreV11()) { 56 | activity.requestPermissions( 57 | arrayOf("moe.shizuku.manager.permission.API_V23"), 58 | SHIZUKU_PERMISSION_REQUEST_CODE 59 | ) 60 | } else if (!Shizuku.isPreV11()) { 61 | Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) 62 | } 63 | } catch (e: IllegalStateException) { 64 | /* Shizuku service not running */ 65 | } 66 | } 67 | 68 | fun isDeviceRooted(): Boolean { 69 | val suBinaries = arrayOf( 70 | "/sbin/su", "/system/bin/su", "/system/xbin/su", 71 | "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", 72 | "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su" 73 | ) 74 | for (path in suBinaries) { 75 | if (File(path).exists()) { 76 | return true 77 | } 78 | } 79 | var process: Process? = null 80 | return try { 81 | process = Runtime.getRuntime().exec(arrayOf("su", "-c", "id")) 82 | 83 | process.inputStream.bufferedReader().use { it.readText() } 84 | process.errorStream.bufferedReader().use { it.readText() } 85 | val exitValue = process.waitFor() 86 | exitValue == 0 87 | } catch (e: Exception) { 88 | false 89 | } finally { 90 | process?.destroy() 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rbn/qtsettings/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.rbn.qtsettings.viewmodel 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.util.Log 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelProvider 8 | import androidx.lifecycle.viewModelScope 9 | import com.rbn.qtsettings.R 10 | import com.rbn.qtsettings.data.DnsHostnameEntry 11 | import com.rbn.qtsettings.data.PreferencesManager 12 | import com.rbn.qtsettings.utils.PermissionUtils 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.async 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.launch 18 | import rikka.shizuku.Shizuku 19 | 20 | class MainViewModel(private val prefsManager: PreferencesManager) : ViewModel() { 21 | 22 | val dnsToggleOff = prefsManager.dnsToggleOff 23 | val dnsToggleAuto = prefsManager.dnsToggleAuto 24 | val dnsHostnames = prefsManager.dnsHostnames 25 | val dnsEnableAutoRevert = prefsManager.dnsEnableAutoRevert 26 | val dnsAutoRevertDelaySeconds = prefsManager.dnsAutoRevertDelaySeconds 27 | 28 | 29 | val usbToggleEnable = prefsManager.usbToggleEnable 30 | val usbToggleDisable = prefsManager.usbToggleDisable 31 | val usbEnableAutoRevert = prefsManager.usbEnableAutoRevert 32 | val usbAutoRevertDelaySeconds = prefsManager.usbAutoRevertDelaySeconds 33 | 34 | val helpShown = prefsManager.helpShown 35 | 36 | private val _initialTab = MutableStateFlow(0) 37 | val initialTab = _initialTab.asStateFlow() 38 | 39 | private val _showHostnameEditDialog = MutableStateFlow(false) 40 | val showHostnameEditDialog = _showHostnameEditDialog.asStateFlow() 41 | 42 | private val _editingHostname = MutableStateFlow(null) 43 | val editingHostname = _editingHostname.asStateFlow() 44 | 45 | private val _hostnamePendingDeletion = MutableStateFlow(null) 46 | val hostnamePendingDeletion = _hostnamePendingDeletion.asStateFlow() 47 | 48 | private val _hasWriteSecureSettings = MutableStateFlow(false) 49 | val hasWriteSecureSettings = _hasWriteSecureSettings.asStateFlow() 50 | 51 | private val _isShizukuAvailable = MutableStateFlow(false) 52 | val isShizukuAvailable = _isShizukuAvailable.asStateFlow() 53 | 54 | private val _appHasShizukuPermission = MutableStateFlow(false) 55 | val appHasShizukuPermission = _appHasShizukuPermission.asStateFlow() 56 | 57 | private val _isDeviceRooted = MutableStateFlow(false) 58 | val isDeviceRooted = _isDeviceRooted.asStateFlow() 59 | 60 | private val _permissionGrantStatus = MutableStateFlow(null) 61 | val permissionGrantStatus = _permissionGrantStatus.asStateFlow() 62 | 63 | 64 | fun setDnsToggleOff(enabled: Boolean) = prefsManager.setDnsToggleOff(enabled) 65 | fun setDnsToggleAuto(enabled: Boolean) = prefsManager.setDnsToggleAuto(enabled) 66 | fun setDnsEnableAutoRevert(enabled: Boolean) = prefsManager.setDnsEnableAutoRevert(enabled) 67 | fun setDnsAutoRevertDelaySeconds(delay: Int) = prefsManager.setDnsAutoRevertDelaySeconds(delay) 68 | 69 | 70 | fun setUsbToggleEnable(enabled: Boolean) = prefsManager.setUsbToggleEnable(enabled) 71 | fun setUsbToggleDisable(enabled: Boolean) = prefsManager.setUsbToggleDisable(enabled) 72 | fun setUsbEnableAutoRevert(enabled: Boolean) = prefsManager.setUsbEnableAutoRevert(enabled) 73 | fun setUsbAutoRevertDelaySeconds(delay: Int) = prefsManager.setUsbAutoRevertDelaySeconds(delay) 74 | 75 | 76 | fun setHelpShown(shown: Boolean) = prefsManager.setHelpShown(shown) 77 | 78 | fun setInitialTab(tabIndex: Int) { 79 | _initialTab.value = tabIndex 80 | } 81 | 82 | fun updateDnsHostnameEntrySelection(id: String, isSelected: Boolean) { 83 | val entry = dnsHostnames.value.find { it.id == id } 84 | entry?.let { 85 | prefsManager.updateDnsHostnameEntry(it.copy(isSelectedForCycle = isSelected)) 86 | } 87 | } 88 | 89 | fun addCustomDnsHostname(name: String, hostnameValue: String) { 90 | prefsManager.addCustomDnsHostname(name, hostnameValue) 91 | } 92 | 93 | fun editCustomDnsHostname(id: String, newName: String, newHostnameValue: String) { 94 | val entry = dnsHostnames.value.find { it.id == id && !it.isPredefined } 95 | entry?.let { 96 | prefsManager.updateDnsHostnameEntry( 97 | it.copy( 98 | name = newName, 99 | hostname = newHostnameValue 100 | ) 101 | ) 102 | } 103 | } 104 | 105 | fun deleteCustomDnsHostname(id: String) { 106 | prefsManager.deleteCustomDnsHostname(id) 107 | } 108 | 109 | fun startAddingNewHostname() { 110 | _editingHostname.value = null; _showHostnameEditDialog.value = true 111 | } 112 | 113 | fun startEditingHostname(entry: DnsHostnameEntry) { 114 | _editingHostname.value = entry; _showHostnameEditDialog.value = true 115 | } 116 | 117 | fun dismissHostnameEditDialog() { 118 | _showHostnameEditDialog.value = false; _editingHostname.value = null 119 | } 120 | 121 | fun setHostnamePendingDeletion(entry: DnsHostnameEntry?) { 122 | _hostnamePendingDeletion.value = entry 123 | } 124 | 125 | fun checkSystemStates(context: Context) { 126 | _hasWriteSecureSettings.value = PermissionUtils.hasWriteSecureSettingsPermission(context) 127 | _isShizukuAvailable.value = PermissionUtils.isShizukuAvailableAndReady() 128 | if (_isShizukuAvailable.value) { 129 | _appHasShizukuPermission.value = 130 | PermissionUtils.checkShizukuPermission(context) == PackageManager.PERMISSION_GRANTED 131 | } else { 132 | _appHasShizukuPermission.value = false 133 | } 134 | _isDeviceRooted.value = PermissionUtils.isDeviceRooted() 135 | } 136 | 137 | fun grantWriteSecureSettingsViaShizuku(context: Context) { 138 | viewModelScope.launch(Dispatchers.IO) { 139 | if (!_isShizukuAvailable.value) { 140 | _permissionGrantStatus.value = context.getString(R.string.shizuku_not_available) 141 | return@launch 142 | } 143 | if (!_appHasShizukuPermission.value) { 144 | _permissionGrantStatus.value = 145 | context.getString(R.string.shizuku_permission_not_granted_to_app_prompt) 146 | return@launch 147 | } 148 | 149 | try { 150 | val packageName = context.packageName 151 | val command = "pm grant $packageName android.permission.WRITE_SECURE_SETTINGS" 152 | val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null) 153 | 154 | val deferredStdErr = 155 | async { process.errorStream.bufferedReader().use { it.readText() } } 156 | val exitCode = process.waitFor() 157 | 158 | if (exitCode == 0) { 159 | // Re-check permission directly 160 | if (PermissionUtils.hasWriteSecureSettingsPermission(context.applicationContext)) { 161 | _permissionGrantStatus.value = 162 | context.getString(R.string.permission_granted_shizuku_success) 163 | } else { 164 | _permissionGrantStatus.value = 165 | context.getString(R.string.permission_granted_shizuku_check_failed) 166 | } 167 | } else { 168 | val errOutput = deferredStdErr.await() 169 | Log.e( 170 | "ShizukuGrant", 171 | "Shizuku command failed with exit code $exitCode: $errOutput" 172 | ) 173 | _permissionGrantStatus.value = 174 | context.getString(R.string.permission_granted_shizuku_fail, exitCode) 175 | } 176 | } catch (e: Exception) { 177 | Log.e("ShizukuGrant", "Error granting permission via Shizuku", e) 178 | _permissionGrantStatus.value = 179 | context.getString(R.string.permission_granted_shizuku_error, e.message) 180 | } finally { 181 | checkSystemStates(context.applicationContext) 182 | } 183 | } 184 | } 185 | 186 | fun grantWriteSecureSettingsViaRoot(context: Context) { 187 | viewModelScope.launch(Dispatchers.IO) { 188 | if (!_isDeviceRooted.value) { 189 | _permissionGrantStatus.value = context.getString(R.string.device_not_rooted) 190 | return@launch 191 | } 192 | try { 193 | val packageName = context.packageName 194 | val command = "pm grant $packageName android.permission.WRITE_SECURE_SETTINGS" 195 | val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command)) 196 | 197 | val deferredStdErr = 198 | async { process.errorStream.bufferedReader().use { it.readText() } } 199 | val exitCode = process.waitFor() 200 | 201 | if (exitCode == 0) { 202 | if (PermissionUtils.hasWriteSecureSettingsPermission(context.applicationContext)) { 203 | _permissionGrantStatus.value = 204 | context.getString(R.string.permission_granted_root_success) 205 | } else { 206 | _permissionGrantStatus.value = 207 | context.getString(R.string.permission_granted_root_check_failed) 208 | } 209 | } else { 210 | val errOutput = deferredStdErr.await() 211 | Log.e("RootGrant", "Root command failed with exit code $exitCode: $errOutput") 212 | _permissionGrantStatus.value = 213 | context.getString(R.string.permission_granted_root_fail, exitCode) 214 | } 215 | } catch (e: Exception) { 216 | Log.e("RootGrant", "Error granting permission via Root", e) 217 | _permissionGrantStatus.value = 218 | context.getString(R.string.permission_granted_root_error, e.message) 219 | } finally { 220 | checkSystemStates(context.applicationContext) 221 | } 222 | } 223 | } 224 | 225 | fun clearPermissionGrantStatus() { 226 | _permissionGrantStatus.value = null 227 | } 228 | } 229 | 230 | class ViewModelFactory(private val prefsManager: PreferencesManager) : ViewModelProvider.Factory { 231 | override fun create(modelClass: Class): T { 232 | if (modelClass.isAssignableFrom(MainViewModel::class.java)) { 233 | @Suppress("UNCHECKED_CAST") 234 | return MainViewModel(prefsManager) as T 235 | } 236 | throw IllegalArgumentException("Unknown ViewModel class") 237 | } 238 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_auto.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_off.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_on_adguard.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_on_cloudflare.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dns_on_quad9_security.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_usb_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_usb_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RBN-Apps/Quick-Tile-Settings/d329adc4810bbf238ef282e1f7682820c7cd3587/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Quick-Tile Einstellungen 4 | 5 | 6 | Privates DNS 7 | USB-Debugging 8 | DNS auf %1$s zurückgesetzt 9 | USB-Debug auf %1$s zurückgesetzt 10 | Entwickleropt. aus. USB-Zurücksetzung abgebrochen. 11 | Zurück auf %1$s in %2$ds… 12 | 13 | 14 | DNS Aus 15 | DNS Auto 16 | DNS Ein (Hostname) 17 | DNS-Status unbekannt 18 | DNS-Hostname 19 | 20 | 21 | USB-Debug Ein 22 | USB-Debug Aus 23 | Entwickleroptionen Aus 24 | 25 | 26 | Privates DNS Tile 27 | USB-Debugging Tile 28 | Wähle Zustände für den Tile-Zyklus aus: 29 | Einstellungen anwenden 30 | Privates DNS 31 | USB-Debugging 32 | 33 | 34 | Berechtigung fehlt. Bitte via ADB erteilen. 35 | Entwickleroptionen sind deaktiviert. 36 | Fehler beim Speichern der Einstellungen. 37 | Keine DNS-Zustände zum Durchschalten aktiviert. 38 | Keine USB-Zustände zum Durchschalten aktiviert. 39 | Hostname für DNS Ein erforderlich. 40 | Einstellungen gespeichert. Tiles werden beim nächsten Öffnen der QS-Leiste aktualisiert. 41 | Hostname darf nicht leer sein, wenn "DNS Ein" aktiviert ist. 42 | Mindestens ein USB-Status muss aktiviert sein. 43 | Befehl kopiert! 44 | 45 | 46 | Berechtigung erforderlich 47 | Um Systemeinstellungen wie Privates DNS und USB-Debugging zu ändern, benötigt diese App die spezielle Berechtigung WRITE_SECURE_SETTINGS. Bitte wähle unten eine Methode, um sie zu erteilen: 48 | Nachdem die Berechtigung erteilt wurde, starte die App neu oder öffne die Schnelleinstellungsleiste erneut, damit die Kacheln funktionieren. 49 | Hilfe / Berechtigung erteilen 50 | Entwickleroptionen öffnen 51 | Schließen 52 | 53 | 54 | Methode %1$d: ADB (Android Debug Bridge) %2$s 55 | Dies ist die Standardmethode und erfordert einen Computer mit eingerichteter ADB. 56 | ADB-Anleitung anzeigen 57 | ADB-Anleitung 58 | Verbinde dein Gerät mit einem Computer, auf dem ADB eingerichtet ist. Stelle sicher, dass die Entwickleroptionen und USB-Debugging auf deinem Gerät aktiviert sind. 59 | Option A: Bestehender Installation erteilen 60 | Führe folgenden Befehl in deinem Terminal/Eingabeaufforderung aus: 61 | Grant-Befehl kopieren 62 | Option B: Mit Berechtigung installieren 63 | Deinstalliere zuerst die aktuelle Version und verwende dann diesen Befehl (ersetze pfad/zu/deiner/app.apk mit dem tatsächlichen APK-Pfad): 64 | Install-Befehl kopieren 65 | (Hinweis: Du benötigst die APK-Datei für den Installationsbefehl. Wenn du die App selbst kompiliert hast, befindet sie sich normalerweise in app/build/outputs/apk/debug/ oder app/build/outputs/apk/release/. Du kannst die neueste Version auch von GitHub herunterladen.) 66 | Neueste Version von GitHub herunterladen 67 | adb install -g -r pfad/zu/deiner/app.apk 68 | 69 | 70 | Methode %1$d: Shizuku %2$s 71 | Shizuku ist verfügbar und diese App hat die Berechtigung, es zu nutzen. Du kannst versuchen, WRITE_SECURE_SETTINGS zu erteilen. 72 | Shizuku (Play Store) 73 | Shizuku (GitHub) 74 | Shizuku ist nicht verfügbar oder läuft nicht. Bitte installiere und starte Shizuku zuerst. 75 | Diese App benötigt eine Berechtigung von Shizuku, um fortzufahren. Bitte fordere sie an. 76 | Shizuku App-Berechtigung anfordern 77 | Mit Shizuku erteilen 78 | Shizuku ist nicht verfügbar oder läuft nicht. 79 | Shizuku-Berechtigung für diese App nicht erteilt. Bitte erteile sie zuerst über die Shizuku-App oder die Aufforderung. 80 | Shizuku-Berechtigung für diese App erteilt. 81 | Shizuku-Berechtigung für diese App verweigert. 82 | Shizuku ist nicht verfügbar oder nicht bereit. 83 | 84 | 85 | Methode %1$d: Root %2$s 86 | Dein Gerät ist gerootet. Du kannst versuchen, WRITE_SECURE_SETTINGS mit Root-Rechten zu erteilen. 87 | Dein Gerät scheint nicht gerootet zu sein oder Root-Zugriff wurde nicht gewährt. Diese Methode ist nicht verfügbar. 88 | Mit Root erteilen 89 | Gerät ist nicht gerootet. 90 | 91 | 92 | Berechtigung erfolgreich via Shizuku erteilt! 93 | Shizuku-Befehl ausgeführt, aber Berechtigungsprüfung fehlgeschlagen. Bitte starte die App neu. 94 | Fehler beim Erteilen der Berechtigung via Shizuku. Exit-Code: %1$d 95 | Fehler beim Erteilen der Berechtigung via Shizuku: %1$s 96 | Berechtigung erfolgreich via Root erteilt! 97 | Root-Befehl ausgeführt, aber Berechtigungsprüfung fehlgeschlagen. Bitte starte die App neu. 98 | Fehler beim Erteilen der Berechtigung via Root. Exit-Code: %1$d 99 | Fehler beim Erteilen der Berechtigung via Root: %1$s 100 | 101 | 102 | Berechtigung fehlt! 103 | Diese App benötigt die Berechtigung WRITE_SECURE_SETTINGS, um zu funktionieren. Bitte erteile sie über eine der verfügbaren Methoden. 104 | Optionen zum Erteilen anzeigen 105 | Entwickleroptionen sind deaktiviert. USB-Debugging kann nicht konfiguriert werden. 106 | 107 | Automatisches Zurücksetzen nach Verzögerung aktivieren 108 | Verzögerung (Sekunden): 109 | DNS wird in %2$d Sek. auf \n\"%1$s\" zurückgesetzt… 110 | USB-Debug wird in %2$d Sek. auf %1$s zurückgesetzt… 111 | Autom. Zurücksetzen abgebrochen. 112 | Ein 113 | Aus 114 | Auto 115 | 116 | Hostnamen für den Kachel-Zyklus auswählen: 117 | Eigenen Hostnamen hinzufügen 118 | Hostname bearbeiten 119 | Hostname hinzufügen 120 | Anzeigename 121 | Hostname (z.B. dns.example.com) 122 | Speichern 123 | Abbrechen 124 | Bearbeiten 125 | Löschen 126 | Anzeigename darf nicht leer sein. 127 | Hostname darf nicht leer sein. 128 | Hostname löschen? 129 | Möchtest du \"%1$s\" wirklich löschen? 130 | 131 | 132 | Über %1$s 133 | Info über %1$s 134 | Cloudflare DNS (one.one.one.one oder 1.1.1.1) ist bekannt für seine hohe Geschwindigkeit und den Fokus auf Datenschutz. Es filtert keine Inhalte und verspricht, keine Nutzerdaten zu protokollieren. Ideal für Nutzer, die Wert auf Privatsphäre und Performance legen. 135 | AdGuard DNS (dns.adguard.com) blockiert Werbung, Tracker und bekannte schädliche Domains direkt auf DNS-Ebene. Dies sorgt für ein saubereres und sichereres Surferlebnis. Gut für Nutzer, die Werbung reduzieren und ihre Privatsphäre schützen möchten. 136 | Quad9 (dns.quad9.net) konzentriert sich auf Sicherheit, indem es Domains blockiert, die mit Malware, Phishing und Botnetzen in Verbindung stehen. Dies erhöht den Schutz vor Online-Bedrohungen. Geeignet für Nutzer, denen Internetsicherheit besonders wichtig ist. 137 | (Für dich empfohlen) 138 | 139 | 140 | Version %1$s 141 | QuickTile Settings bietet praktische Schnelleinstellungs-Kacheln zum Umschalten von Einstellungen direkt aus dem Benachrichtigungsfeld. 142 | Funktionen 143 | • Private DNS-Kachel mit individueller Hostname-Unterstützung 144 | • USB-Debugging-Kachel für die Entwicklung 145 | • Automatisches Zurücksetzen für temporäre Änderungen 146 | • Integrierte beliebte DNS-Anbieter (Cloudflare, AdGuard, Quad9) 147 | Entwickler 148 | Entwickelt von RBN-Apps\nOpen-Source-Projekt verfügbar auf GitHub 149 | Hilfe zur Berechtigung einrichten 150 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #303231 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | QuickTile Settings 3 | 4 | 5 | Private DNS 6 | USB Debugging 7 | DNS reverted to %1$s 8 | USB Debug reverted to %1$s 9 | Dev options off. USB revert cancelled. 10 | Reverting to %1$s in %2$ds… 11 | 12 | 13 | DNS Off 14 | DNS Auto 15 | DNS On (Hostname) 16 | DNS Status Unknown 17 | DNS Hostname 18 | 19 | 20 | USB Debug On 21 | USB Debug Off 22 | Dev Options Off 23 | 24 | 25 | Private DNS Tile 26 | USB Debugging Tile 27 | Select states for the tile cycle: 28 | Apply Settings 29 | Private DNS 30 | USB Debugging 31 | 32 | 33 | Permission missing. Please grant via ADB. 34 | Developer options are disabled. 35 | Error saving settings. 36 | No DNS states enabled for cycling. 37 | No USB states enabled for cycling. 38 | Hostname required for DNS On. 39 | Settings saved. Tiles will update when you next open the QS panel. 40 | Hostname cannot be empty if "DNS On" is enabled. 41 | At least one USB status must be enabled. 42 | Command copied! 43 | 44 | 45 | 46 | Permission Required 47 | To change system settings like Private DNS and USB Debugging, this app needs the special WRITE_SECURE_SETTINGS permission. Please choose a method below to grant it: 48 | After granting the permission, restart the app or re-open the Quick Settings panel for the tiles to function. 49 | Help / Grant Permission 50 | Open Developer Options 51 | Close 52 | 53 | 54 | Method %1$d: ADB (Android Debug Bridge) %2$s 55 | This is the standard method and requires a computer with ADB set up. 56 | Show ADB Instructions 57 | ADB Instructions 58 | Connect your device to a computer with ADB set up. Ensure Developer Options and USB Debugging are enabled on your device. 59 | Option A: Grant to existing installation 60 | Run the following command in your terminal/command prompt: 61 | Copy Grant Command 62 | Option B: Install with permission 63 | Uninstall the current version first, then use this command (replace path/to/your/app.apk with the actual APK path): 64 | Copy Install Command 65 | (Note: You need the APK file for the install command. If you built the app yourself, it\'s usually in app/build/outputs/apk/debug/ or app/build/outputs/apk/release/. You can also download the latest release from GitHub.) 66 | Download latest release from GitHub 67 | https://github.com/RBN-Apps/Quick-Tile-Settings/releases 68 | https://github.com/RBN-Apps/Quick-Tile-Settings 69 | adb install -g -r path/to/your/app.apk 70 | 71 | 72 | Method %1$d: Shizuku %2$s 73 | Shizuku is available and this app has permission to use it. You can attempt to grant WRITE_SECURE_SETTINGS. 74 | Shizuku (Play Store) 75 | Shizuku (GitHub) 76 | https://github.com/RikkaApps/Shizuku/releases 77 | Shizuku is not available or not running. Please install and start Shizuku first. 78 | This app needs permission from Shizuku to proceed. Please request it. 79 | Request Shizuku App Permission 80 | Grant using Shizuku 81 | Shizuku is not available or not running. 82 | Shizuku permission not granted to this app. Please grant it first via Shizuku app or the prompt. 83 | Shizuku permission granted to this app. 84 | Shizuku permission denied to this app. 85 | Shizuku is not available or not ready. 86 | https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api 87 | 88 | 89 | Method %1$d: Root %2$s 90 | Your device is rooted. You can attempt to grant WRITE_SECURE_SETTINGS using root privileges. 91 | Your device does not appear to be rooted or root access was not granted. This method is unavailable. 92 | Grant using Root 93 | Device is not rooted. 94 | 95 | 96 | Permission granted successfully via Shizuku! 97 | Shizuku command executed, but permission check failed. Please restart the app. 98 | Failed to grant permission via Shizuku. Exit code: %1$d 99 | Error granting permission via Shizuku: %1$s 100 | Permission granted successfully via Root! 101 | Root command executed, but permission check failed. Please restart the app. 102 | Failed to grant permission via Root. Exit code: %1$d 103 | Error granting permission via Root: %1$s 104 | 105 | 106 | Permission Missing! 107 | This app requires the WRITE_SECURE_SETTINGS permission to function. Please grant it using one of the available methods. 108 | Show Grant Options 109 | Developer options are disabled. USB Debugging cannot be configured. 110 | 111 | Enable auto-revert after a delay 112 | Revert delay (seconds): 113 | Reverting DNS to \n\"%1$s\" in %2$d sec… 114 | Reverting USB Debug to %1$s in %2$d sec… 115 | Auto-revert cancelled. 116 | On 117 | Off 118 | Auto 119 | 120 | Select hostnames to include in tile cycle: 121 | Add Custom Hostname 122 | Edit Hostname 123 | Add Hostname 124 | Display Name 125 | Hostname (e.g., dns.example.com) 126 | Save 127 | Cancel 128 | Edit 129 | Delete 130 | Display name cannot be empty. 131 | Hostname cannot be empty. 132 | Delete Hostname? 133 | Are you sure you want to delete \'%1$s\'? 134 | 135 | 136 | About %1$s 137 | Info about %1$s 138 | Cloudflare DNS (one.one.one.one or 1.1.1.1) is known for its high speed and focus on privacy. It does not filter content and promises not to log user data. Ideal for users prioritizing privacy and performance. 139 | AdGuard DNS (dns.adguard.com) blocks ads, trackers, and known malicious domains directly at the DNS level. This provides a cleaner and safer browsing experience. Good for users who want to reduce ads and protect their privacy. 140 | Quad9 (dns.quad9.net) focuses on security by blocking domains associated with malware, phishing, and botnets. This enhances protection against online threats. Suitable for users for whom internet security is a top priority. 141 | (Recommended for you) 142 | 143 | 144 | Version %1$s 145 | QuickTile Settings provides convenient Quick Settings tiles for toggling settings directly from the notification panel. 146 | Features 147 | • Private DNS tile with custom hostname support 148 | • USB Debugging tile for development 149 | • Auto-revert functionality for temporary changes 150 | • Built-in popular DNS providers (Cloudflare, AdGuard, Quad9) 151 | Developer 152 | Developed by RBN-Apps\nOpen source project available on GitHub 153 | GitHub 154 | Permission Setup Help 155 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |