├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── dependabot.yml └── workflows │ └── push.yml ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── sqlite-android-3410200.aar ├── lint.xml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── jacopomii │ │ └── gappsmod │ │ └── ICoreRootService.aidl │ ├── java │ └── com │ │ └── jacopomii │ │ └── gappsmod │ │ ├── application │ │ └── GAppsModApplication.java │ │ ├── data │ │ ├── BooleanFlag.java │ │ ├── Constants.java │ │ ├── PhenotypeDBPackageName.java │ │ └── Version.java │ │ ├── service │ │ └── CoreRootService.java │ │ ├── ui │ │ ├── activity │ │ │ ├── MainActivity.java │ │ │ └── SplashScreenActivity.java │ │ ├── adapter │ │ │ ├── BooleanModsRecyclerViewAdapter.java │ │ │ └── SelectPackageRecyclerViewAdapter.java │ │ ├── fragment │ │ │ ├── BooleanModsFragment.java │ │ │ ├── InformationFragment.java │ │ │ ├── RevertModsFragment.java │ │ │ └── SuggestedModsFragment.java │ │ └── view │ │ │ ├── FilterableSearchView.java │ │ │ ├── ProgrammaticMaterialSwitchView.java │ │ │ ├── SuggestedModsAppHeaderView.java │ │ │ └── SwitchCardView.java │ │ └── util │ │ ├── OnItemClickListener.java │ │ └── Utils.java │ ├── proto │ └── call_screen_i18n.proto │ └── res │ ├── drawable │ ├── ic_arrow_down_24.xml │ ├── ic_arrow_up_24.xml │ ├── ic_beta_24.xml │ ├── ic_error_24.xml │ ├── ic_fail_24.xml │ ├── ic_install_24.xml │ ├── ic_menu_search_24.xml │ ├── ic_nav_drawer_boolean_mods_24.xml │ ├── ic_nav_drawer_delete_24.xml │ ├── ic_nav_drawer_information_24.xml │ ├── ic_nav_drawer_suggested_mods_24.xml │ ├── ic_save_24.xml │ └── ic_success_24.xml │ ├── layouts │ ├── activities │ │ └── layout │ │ │ ├── activity_main.xml │ │ │ └── activity_splash_screen.xml │ ├── dialogs │ │ └── layout │ │ │ └── dialog_select_package.xml │ ├── fragments │ │ └── layout │ │ │ ├── fragment_boolean_mods.xml │ │ │ ├── fragment_information.xml │ │ │ ├── fragment_revert_mods.xml │ │ │ └── fragment_suggested_mods.xml │ └── items │ │ └── layout │ │ ├── filterable_searchview.xml │ │ ├── nav_drawer_header.xml │ │ ├── package_row.xml │ │ ├── suggested_mods_app_header.xml │ │ └── switch_card.xml │ ├── menu │ ├── nav_drawer.xml │ └── search_menu.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── navigation │ └── mobile_navigation.xml │ ├── raw │ └── silent_wav.wav │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-notnight-v23 │ └── themes.xml │ ├── values-notnight-v27 │ └── themes.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jacopotediosi 2 | custom: https://paypal.me/jacopotediosi 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report something that isn't working 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Overview 11 | [NOTE]: # ( Give a BRIEF summary about your problem ) 12 | 13 | 14 | ## Steps to Reproduce 15 | [NOTE]: # ( Provide a simple set of steps to reproduce this bug. ) 16 | 1. 17 | 2. 18 | 3. 19 | 20 | ## Expected Behavior 21 | [NOTE]: # ( Tell us what you expected to happen ) 22 | 23 | 24 | ## Actual Behavior 25 | [NOTE]: # ( Tell us what actually happens ) 26 | 27 | 28 | ## Screenshots 29 | [NOTE]: # ( If applicable, add screenshots to help explain your problem. ) 30 | 31 | 32 | ## System Information 33 | - Device and model: 34 | 35 | - ROM and Android version: 36 | 37 | - Is the Google app you are trying to tweak (e.g., Phone by Google) installed as system app: yes/no 38 | 39 | - Installed Magisk / other SU Manager version: 40 | 41 | [NOTE]: # ( Paste below the output of the `adb shell "dumpsys package com.jacopomii.gappsmod | grep version"` command ) 42 | - Installed GAppsMod version: 43 | 44 | 45 | [NOTE]: # ( Paste below the output of the `adb shell "dumpsys package REPLACE_WITH_PACKAGENAME | grep version"` command ) 46 | - The version of the Google app you are trying to tweak (e.g., Phone by Google): 47 | 48 | 49 | [NOTE]: # ( Paste below the output of the `adb shell "getprop | grep locale"` command ) 50 | - Your device language (locale): 51 | 52 | 53 | [NOTE]: # ( Paste below the output of the `adb shell "getprop | grep iso-country"` command ) 54 | - Your location (country of the SIM and country where you are): 55 | 56 | 57 | ## Logcat 58 | [NOTE]: # ( 59 | Launch the Google app you are trying to tweak (e.g., Phone by Google) in Debug mode using the `adb shell "am start -D REPLACE_WITH_PACKAGENAME"` command. 60 | Open another terminal and use the `adb logcat > logs.txt` command to start capturing logs. 61 | Perform the necessary steps to replicate the bug, then press CTRL+C to stop capturing logs. 62 | Attach below the resulting logs.txt file. 63 | ) 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for a new feature you want 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | [NOTE]: # ( Provide a brief overview of what the new feature is all about ) 12 | 13 | 14 | ## Solution 15 | [NOTE]: # ( A clear and concise description of what you want to happen ) 16 | 17 | 18 | ## Examples 19 | [NOTE]: # ( Show us a picture or mock-up of your proposal ) 20 | 21 | 22 | ## Alternatives 23 | [NOTE]: # ( A clear and concise description of any alternative solutions or features you've considered ) 24 | 25 | 26 | ## Context 27 | [NOTE]: # ( Why does this feature matter to you? What unique circumstances do you have? ) 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Assemble on push 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '.github/**' 8 | - '**.md' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ ubuntu-latest ] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | with: 24 | submodules: 'recursive' 25 | fetch-depth: 0 26 | 27 | - name: Gradle wrapper validation 28 | uses: gradle/wrapper-validation-action@v1 29 | 30 | - name: Set up JDK 17 31 | uses: actions/setup-java@v3 32 | with: 33 | distribution: 'temurin' 34 | java-version: '17' 35 | 36 | - name: Write keystore parameters 37 | run: | 38 | echo keystore.password='${{ secrets.KEYSTORE_PASSWORD }}' >> local.properties 39 | echo keystore.alias='${{ secrets.KEYSTORE_ALIAS }}' >> local.properties 40 | echo keystore.alias_password='${{ secrets.KEYSTORE_ALIAS_PASSWORD }}' >> local.properties 41 | echo keystore.path=`pwd`/keystore.jks >> local.properties 42 | echo "${{ secrets.KEYSTORE_KEY }}" | base64 --decode > keystore.jks 43 | 44 | - name: Gradle Dependency Submission 45 | uses: mikepenz/gradle-dependency-submission@v0.8.6 46 | with: 47 | gradle-build-module: ":app" 48 | 49 | - name: Assemble 50 | uses: gradle/gradle-build-action@v2 51 | with: 52 | arguments: assemble 53 | 54 | - name: Get short commit hash 55 | run: | 56 | echo "LATEST_COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 57 | 58 | - name: Upload debug 59 | if: success() 60 | uses: actions/upload-artifact@v3 61 | with: 62 | name: ${{ github.event.repository.name }}-${{ env.LATEST_COMMIT_HASH }}-debug.apk 63 | path: "app/build/outputs/apk/debug/app-debug.apk" 64 | 65 | - name: Upload release 66 | if: success() 67 | uses: actions/upload-artifact@v3 68 | with: 69 | name: ${{ github.event.repository.name }}-${{ env.LATEST_COMMIT_HASH }}-release.apk 70 | path: "app/build/outputs/apk/release/app-release.apk" 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | /build 6 | local.properties 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | ## !!! This project is no longer maintained !!! 3 | 4 | I had started researching how Phenotype DB works as a hobby, and this application was only meant to be a proof of concept. 5 | 6 | Making things work required a huge amount of reverse engineering of Google applications, and today I no longer have time to dedicate to it. 7 | 8 | I've also been told that the structure of Phenotype DB has been recently changed, so I expect this project to no longer work, or to stop working soon. 9 | 10 | If you are looking for a replacement, I suggest you take a look at [GMS-Flags](https://github.com/polodarb/GMS-Flags), a still maintained application created by other users who were part of the community of this project itself. 11 | 12 | # GAppsMod (ex GoogleDialerMod) 13 | The ultimate All-In-One Utility to tweak Google applications. 14 | 15 | 16 | ## Downloads: 17 | - Please visit the [GAppsMod Release Page](https://github.com/jacopotediosi/GAppsMod/releases) 18 | 19 | 20 | ## How do I use it? 21 | - Always make sure you're using the latest beta version of the Google apps you want to tweak to take advantage of the latest features 22 | - Allow root access to GAppsMod, apply any mods you want, then force close and reopen Google apps a few times for them to take effect 23 | - There is no need to keep GAppsMod installed after applying the desired mods, because they (should) survive Google applications updates / reinstalls over time 24 | 25 | 26 | ## How does it work? 27 | In every Android device there is a database, called Phenotype.db, managed by Google Play Services, containing "flags" that affect the behavior of all installed Google applications. 28 | 29 | Some of those flags concern applications core functionalities, while others pertain to hidden or upcoming features that have not yet been released. 30 | 31 | What GAppsMod does is execute SQLite queries on that database and override the configuration files of Google applications to enable or modify their functionality at will. 32 | 33 | 34 | ## Features: 35 | - Supports all arm / arm64 / x86 / x86_64 devices and all Android versions from 5.0 (Lollipop) 36 | - Enable / disable hidden features for all users at once when Android "multiple users" mode is in use 37 | - Allows users to list and change all Phenotype DB boolean flags for all installed Google applications 38 | - A convenient home screen brings together the suggested mods for the most used Google applications 39 | 40 | 41 | ## Currently suggested mods 42 | - For the **Phone** application ([link](https://play.google.com/store/apps/details?id=com.google.android.dialer)): 43 | - Force **enable call recording** feature, even on unsupported devices or in unsupported countries ([ref](https://support.google.com/phoneapp/answer/9803950)) 44 | - Enable also **automatic call recording** ("always record") feature based on caller (otherwise only available in India) 45 | - Silence the annoying "registration has started / ended" **call recording announcements** (only on Phone version <= 94.x) 46 | - Force **enable call screening** and "revelio" (advanced automatic call screening) features, even on unsupported devices or in unsupported countries ([ref](https://support.google.com/phoneapp/answer/9118387)) 47 | - Allows users to choose the language for call screening 48 | - For the **Messages** application ([link](https://play.google.com/store/apps/details?id=com.google.android.apps.messaging)): 49 | - Force **enable debug menu** (it can also be enabled without mods by entering `*xyzzy*` in the application's search field) 50 | - Force **enable message organization** ("supersort") 51 | - Force **enable marking conversations as unread** 52 | - Force **enable verified SMS** settings menu ([ref](https://support.google.com/messages/answer/9326240)) 53 | - Force **enable always sending images by Google Photos links in SMS** ([ref](https://9to5google.com/2022/02/19/messages-google-photos/)) 54 | - Force **enable nudges and birthday reminders** ([ref](https://support.google.com/messages/answer/11555591)) 55 | - Force **enable Bard AI draft suggestions** ("magic compose") ([ref](https://9to5google.com/2023/05/05/google-messages-magic-compose-ai/)) 56 | - Force enable smart features: **spotlights suggestions** ([ref](https://9to5google.com/2023/02/02/google-messages-assistant/)), **stickers suggestions**, **smart compose** ([ref](https://9to5google.com/2020/06/30/gboard-android-smart-compose-google-messages/)), **smart actions (smart reply) in notifications** 57 | 58 | And much more coming soon :) 59 | 60 | 61 | ## Demo 62 | ![Demo GIF](https://github.com/jacopotediosi/GAppsMod/assets/20026524/5b13c935-4b12-46ac-b67d-0182004c8ac0) 63 | 64 | 65 | ## Troubleshooting: 66 | - After enabling / disabling any mod, please force close and reopen a few times the Google application you are trying to mod. You may also need to reboot for the changes to take effect. 67 | - Before to report an issue try to delete Google apps data, to reboot your phone and to try again what didn't work 68 | 69 | 70 | ## Donations 71 | If you really like my work, please consider a donation via [Paypal](https://paypal.me/jacopotediosi) or [Github Sponsor](https://github.com/sponsors/jacopotediosi). Even a small amount will be appreciated. 72 | 73 | 74 | ## Credits: 75 | - Thanks to [Gabriele Rizzo aka shmykelsa](https://github.com/shmykelsa), [Jen94](https://github.com/jen94) and [SAAX by agentdr8](https://gitlab.com/agentdr8/saax) for their [AA-Tweaker](https://github.com/shmykelsa/AA-Tweaker) app, which inspired me making GAppsMod 76 | - [Libsu](https://github.com/topjohnwu/libsu) by [topjohnwu](https://github.com/topjohnwu) 77 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "com.google.protobuf" 4 | id "com.likethesalad.stem" 5 | } 6 | 7 | def keyfile 8 | def keystorePSW 9 | def keystoreAlias 10 | def keystoreAliasPSW 11 | 12 | Properties properties = new Properties() 13 | properties.load(project.rootProject.file("local.properties").newDataInputStream()) 14 | def keystoreFilepath = properties.getProperty("keystore.path") 15 | 16 | if (keystoreFilepath) { 17 | keyfile = file(keystoreFilepath) 18 | keystorePSW = properties.getProperty("keystore.password") 19 | keystoreAlias = properties.getProperty("keystore.alias") 20 | keystoreAliasPSW = properties.getProperty("keystore.alias_password") 21 | } else { 22 | // Remember to config your keystore settings in local.properties or in the below lines 23 | keyfile = file("C:/keystore.jks") 24 | keystorePSW = "CHANGEME" 25 | keystoreAlias = "CHANGEME" 26 | keystoreAliasPSW = "CHANGEME" 27 | } 28 | 29 | android { 30 | namespace "com.jacopomii.gappsmod" 31 | compileSdk 33 32 | 33 | defaultConfig { 34 | applicationId "com.jacopomii.gappsmod" 35 | minSdk 21 36 | targetSdk 33 37 | versionCode 400 38 | versionName "4.00" 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_1_8 43 | targetCompatibility JavaVersion.VERSION_1_8 44 | } 45 | 46 | signingConfigs { 47 | release { 48 | storeFile keyfile 49 | storePassword keystorePSW 50 | keyAlias keystoreAlias 51 | keyPassword keystoreAliasPSW 52 | } 53 | } 54 | 55 | buildTypes { 56 | release { 57 | minifyEnabled true 58 | shrinkResources true 59 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 60 | signingConfig signingConfigs.release 61 | } 62 | } 63 | 64 | buildFeatures { 65 | viewBinding true 66 | aidl true 67 | buildConfig true 68 | } 69 | 70 | sourceSets { 71 | main { 72 | res.srcDirs = ["src/main/res", 73 | "src/main/res/layouts/activities", 74 | "src/main/res/layouts/dialogs", 75 | "src/main/res/layouts/fragments", 76 | "src/main/res/layouts/items"] 77 | } 78 | } 79 | } 80 | 81 | dependencies { 82 | // Libsu 83 | def libsuVersion = "5.0.5" 84 | implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" 85 | implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" 86 | implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}" 87 | 88 | // Official SQLite Java Bindings, downloaded from https://www.sqlite.org/download.html 89 | implementation files("libs/sqlite-android-3410200.aar") 90 | 91 | // Advanced toast 92 | implementation "com.github.GrenderG:Toasty:1.5.2" 93 | 94 | // HTTP 95 | implementation "com.android.volley:volley:1.2.1" 96 | 97 | // Protobuf 98 | implementation "com.google.protobuf:protobuf-javalite:3.23.1" 99 | 100 | // Apache Commons 101 | //noinspection GradleDependency 102 | implementation "commons-io:commons-io:2.12.0" 103 | implementation "org.apache.commons:commons-lang3:3.12.0" 104 | 105 | // Navigation drawer 106 | def navigationVersion = "2.5.3" 107 | implementation "androidx.navigation:navigation-fragment:${navigationVersion}" 108 | implementation "androidx.navigation:navigation-ui:${navigationVersion}" 109 | 110 | // FastScroller for RecyclerView 111 | implementation "io.github.l4digital:fastscroll:2.1.0" 112 | 113 | // Other UI Components 114 | implementation "com.google.android.material:material:1.9.0" 115 | implementation "androidx.appcompat:appcompat:1.6.1" 116 | implementation "androidx.constraintlayout:constraintlayout:2.1.4" 117 | } 118 | 119 | protobuf { 120 | protoc { 121 | artifact = "com.google.protobuf:protoc:3.23.1" 122 | } 123 | generateProtoTasks { 124 | all().each { task -> 125 | task.builtins { 126 | java { 127 | option "lite" 128 | } 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /app/libs/sqlite-android-3410200.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotediosi/GAppsMod/6c63c56da89a2be20349e113dc3167b793cfe885/app/libs/sqlite-android-3410200.aar -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 22 | 23 | # Keep protobuf autogenerated classes 24 | -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } 25 | 26 | # Keep SQLite Java Bindings classes 27 | -keep class org.sqlite.** { *; } 28 | -keep class org.sqlite.database.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/jacopomii/gappsmod/ICoreRootService.aidl: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod; 2 | 3 | interface ICoreRootService { 4 | IBinder getFileSystemService(); 5 | 6 | /** 7 | * Query the GMS and Vending Phenotype DBs to get a list of all package names that have at least 8 | * one Flag set. 9 | * 10 | * @return a {@code HashMap} in the format "Phenotype package name" => "Android package name". 11 | */ 12 | Map phenotypeDBGetAllPackageNames(); 13 | 14 | /** 15 | * Query the GMS and Vending Phenotype DBs to get a list of all package names that have at least 16 | * one Flag overridden. 17 | * 18 | * @return a {@code HashMap} in the format "Phenotype package name" => "Android package name". 19 | */ 20 | Map phenotypeDBGetAllOverriddenPackageNames(); 21 | 22 | /** 23 | * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}) to get the 24 | * Android package name corresponding to a given {@code phenotypePackageName}. 25 | * 26 | * @param phenotypePackageName the Phenotype package name for which the corresponding Android 27 | * package name is to be returned. 28 | * @return the Android package name corresponding to the specified {@code phenotypePackageName}. 29 | * An empty string if the phenotypePackageName couldn't be found. 30 | */ 31 | String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(in String phenotypePackageName); 32 | 33 | /** 34 | * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}), selecting all 35 | * the boolean flags for a given {@code phenotypePackageName}. 36 | * 37 | * @param phenotypePackageName the Phenotype (not Android) package name whose flags are to be 38 | * returned. 39 | * @return a {@code HashMap} that uses the ({@code String}) flag name as the key.
40 | * For performance reasons, the value of this HashMap is a {@code List} structured as follows:
41 | * - Position 0 contains the {@code Boolean} value of the flag, giving priority to the value 42 | * overridden in the FlagOverrides table, if present, over the one contained in the Flags table.
43 | * - Position 1 contains the {@code Boolean} value "changed", which is {@code true} if and only 44 | * if the returned flag is overwritten in the FlagOverrides table and has a different value 45 | * than the one contained in the Flags table; {@code false} otherwise. 46 | */ 47 | Map phenotypeDBGetBooleanFlagsOrOverridden(in String phenotypePackageName); 48 | 49 | /** 50 | * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}) to find out if 51 | * all given {@code flags} are overridden for a given {@code phenotypePackageName}. 52 | * Please note that the fact that a flag is overridden only implies that it is present in the 53 | * FlagOverrides table, and not that its value is different from what is indicated in the Flags 54 | * table. 55 | * 56 | * @param phenotypePackageName the Phenotype (not Android) package name for which to check. 57 | * @param flags the flags to check. 58 | * @return {@code true} if all given {@code flags} are overridden for the given 59 | * {@code phenotypePackageName}; {@code false} otherwise. 60 | */ 61 | boolean phenotypeDBAreAllFlagsOverridden(in String phenotypePackageName, in List flags); 62 | 63 | /** 64 | * Remove all flag overrides from the GMS and Vending Phenotype DBs by truncating the 65 | * FlagOverrides table. 66 | * It also clears from the filesystem the Phenotype cache of all applications for which at least 67 | * one flag was overridden. 68 | */ 69 | void phenotypeDBDeleteAllFlagOverrides(); 70 | 71 | /** 72 | * Delete all flag overrides from the GMS/Vending Phenotype DB (based on the 73 | * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. 74 | * It also clears from the filesystem the Phenotype cache of the application corresponding to 75 | * the given {@code phenotypePackageName}. 76 | * 77 | * @param phenotypePackageName the Phenotype (not Android) package name for which to delete the 78 | * flag overrides. 79 | */ 80 | void phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(in String phenotypePackageName); 81 | 82 | /** 83 | * Delete a given list of flag overrides from the GMS/Vending Phenotype DB (based on the 84 | * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. 85 | * It also clears from the filesystem the Phenotype cache of the application corresponding to 86 | * the given {@code phenotypePackageName}. 87 | * 88 | * @param phenotypePackageName the Phenotype (not Android) package name for which to delete the 89 | * flag overrides. 90 | * @param flags the list of flags to delete. 91 | */ 92 | void phenotypeDBDeleteFlagOverrides(in String phenotypePackageName, in List flags); 93 | 94 | /** 95 | * Override the value of a boolean flag in the GMS/Vending Phenotype DB (based on the 96 | * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. 97 | * It also clears from the filesystem the Phenotype cache of the application corresponding to 98 | * the given {@code phenotypePackageName}. 99 | * 100 | * @param phenotypePackageName the Phenotype (not Android) package name for which to override 101 | * the flag. 102 | * @param flag the name of the flag to override. 103 | * @param value the value to override the flag with. 104 | */ 105 | void phenotypeDBOverrideBooleanFlag(in String phenotypePackageName, in String flag, in boolean value); 106 | 107 | /** 108 | * Override the value of an extension flag in the GMS/Vending Phenotype DB (based on the 109 | * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. 110 | * It also clears from the filesystem the Phenotype cache of the application corresponding to 111 | * the given {@code phenotypePackageName}. 112 | * 113 | * @param phenotypePackageName the Phenotype (not Android) package name for which to override 114 | * the flag. 115 | * @param flag the name of the flag to override. 116 | * @param value the value to override the flag with. 117 | */ 118 | void phenotypeDBOverrideExtensionFlag(in String phenotypePackageName, in String flag, in byte[] value); 119 | 120 | /** 121 | * Override the value of a string flag in the GMS/Vending Phenotype DB (based on the 122 | * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. 123 | * It also clears from the filesystem the Phenotype cache of the application corresponding to 124 | * the given {@code phenotypePackageName}. 125 | * 126 | * @param phenotypePackageName the Phenotype (not Android) package name for which to override 127 | * the flag. 128 | * @param flag the name of the flag to override. 129 | * @param value the value to override the flag with. 130 | */ 131 | void phenotypeDBOverrideStringFlag(in String phenotypePackageName, in String flag, in String value); 132 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/application/GAppsModApplication.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.application; 2 | 3 | import android.app.Application; 4 | 5 | import com.google.android.material.color.DynamicColors; 6 | 7 | public class GAppsModApplication extends Application { 8 | @Override 9 | public void onCreate() { 10 | super.onCreate(); 11 | DynamicColors.applyToActivitiesIfAvailable(this); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/data/BooleanFlag.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.data; 2 | 3 | public class BooleanFlag { 4 | private final String mFlagName; 5 | private boolean mFlagValue; 6 | private boolean mFlagOverriddenAndChanged; 7 | 8 | public BooleanFlag(String flagName, boolean flagValue, boolean flagOverriddenAndChanged) { 9 | mFlagName = flagName; 10 | mFlagValue = flagValue; 11 | mFlagOverriddenAndChanged = flagOverriddenAndChanged; 12 | } 13 | 14 | public String getFlagName() { 15 | return mFlagName; 16 | } 17 | 18 | public boolean getFlagValue() { 19 | return mFlagValue; 20 | } 21 | 22 | public void setFlagValue(boolean flagValue) { 23 | mFlagValue = flagValue; 24 | } 25 | 26 | public void setFlagOverriddenAndChanged(boolean flagOverriddenAndChanged) { 27 | mFlagOverriddenAndChanged = flagOverriddenAndChanged; 28 | } 29 | 30 | public boolean getFlagOverriddenAndChanged() { 31 | return mFlagOverriddenAndChanged; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/data/Constants.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.data; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | @SuppressLint("SdCardPath") 6 | public interface Constants { 7 | // Android package names 8 | String GMS_ANDROID_PACKAGE_NAME = "com.google.android.gms"; 9 | String VENDING_ANDROID_PACKAGE_NAME = "com.android.vending"; 10 | String DIALER_ANDROID_PACKAGE_NAME = "com.google.android.dialer"; 11 | String MESSAGES_ANDROID_PACKAGE_NAME = "com.google.android.apps.messaging"; 12 | 13 | // Phenotype package names 14 | String DIALER_PHENOTYPE_PACKAGE_NAME = "com.google.android.dialer"; 15 | String MESSAGES_PHENOTYPE_PACKAGE_NAME = "com.google.android.apps.messaging#com.google.android.apps.messaging"; 16 | 17 | // Google Play links 18 | String GOOGLE_PLAY_DETAILS_LINK = "https://play.google.com/store/apps/details?id="; 19 | String GOOGLE_PLAY_BETA_LINK = "https://play.google.com/apps/testing/"; 20 | 21 | // Data / data folders 22 | String DATA_DATA_PREFIX = "/data/data/"; 23 | String DIALER_CALLRECORDINGPROMPT = DATA_DATA_PREFIX + DIALER_ANDROID_PACKAGE_NAME + "/files/callrecordingprompt"; 24 | String GMS_PHENOTYPE_DB = DATA_DATA_PREFIX + GMS_ANDROID_PACKAGE_NAME + "/databases/phenotype.db"; 25 | String VENDING_PHENOTYPE_DB = DATA_DATA_PREFIX + VENDING_ANDROID_PACKAGE_NAME + "/databases/phenotype.db"; 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/data/PhenotypeDBPackageName.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.data; 2 | 3 | public class PhenotypeDBPackageName { 4 | private final String mPhenotypePackageName; 5 | private final String mAndroidPackageName; 6 | 7 | public PhenotypeDBPackageName(String phenotypePackageName, String androidPackageName) { 8 | mPhenotypePackageName = phenotypePackageName; 9 | mAndroidPackageName = androidPackageName; 10 | } 11 | 12 | public String getPhenotypePackageName() { 13 | return mPhenotypePackageName; 14 | } 15 | 16 | public String getAndroidPackageName() { 17 | return mAndroidPackageName; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/data/Version.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.data; 2 | 3 | public class Version implements Comparable { 4 | private final String mVersion; 5 | 6 | public Version(String version) { 7 | if (version == null) 8 | throw new IllegalArgumentException("Version can not be null"); 9 | if (!version.matches("\\d+(\\.\\d+)*")) 10 | throw new IllegalArgumentException("Invalid version format"); 11 | mVersion = version; 12 | } 13 | 14 | public final String getVersion() { 15 | return mVersion; 16 | } 17 | 18 | @Override 19 | public int compareTo(Version that) { 20 | if(that == null) 21 | return 1; 22 | String[] thisParts = this.getVersion().split("\\."); 23 | String[] thatParts = that.getVersion().split("\\."); 24 | int length = Math.max(thisParts.length, thatParts.length); 25 | for(int i = 0; i < length; i++) { 26 | int thisPart = i < thisParts.length ? 27 | Integer.parseInt(thisParts[i]) : 0; 28 | int thatPart = i < thatParts.length ? 29 | Integer.parseInt(thatParts[i]) : 0; 30 | if(thisPart < thatPart) 31 | return -1; 32 | if(thisPart > thatPart) 33 | return 1; 34 | } 35 | return 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.activity; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Intent; 5 | import android.content.ServiceConnection; 6 | import android.os.Bundle; 7 | import android.os.IBinder; 8 | import android.os.RemoteException; 9 | 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.core.graphics.Insets; 12 | import androidx.core.view.ViewCompat; 13 | import androidx.core.view.WindowCompat; 14 | import androidx.core.view.WindowInsetsCompat; 15 | import androidx.drawerlayout.widget.DrawerLayout; 16 | import androidx.navigation.NavController; 17 | import androidx.navigation.Navigation; 18 | import androidx.navigation.fragment.NavHostFragment; 19 | import androidx.navigation.ui.AppBarConfiguration; 20 | import androidx.navigation.ui.NavigationUI; 21 | 22 | import com.jacopomii.gappsmod.ICoreRootService; 23 | import com.jacopomii.gappsmod.R; 24 | import com.jacopomii.gappsmod.databinding.ActivityMainBinding; 25 | import com.jacopomii.gappsmod.service.CoreRootService; 26 | import com.topjohnwu.superuser.ipc.RootService; 27 | import com.topjohnwu.superuser.nio.FileSystemManager; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | private AppBarConfiguration mAppBarConfiguration; 31 | private ActivityMainBinding mBinding; 32 | 33 | private boolean mCoreRootServiceBound = false; 34 | private ServiceConnection mCoreRootServiceConnection; 35 | private ICoreRootService mCoreRootServiceIpc; 36 | private FileSystemManager mCoreRootServiceFSManager; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | // The savedInstanceState must not be used, otherwise the views (and the fragments contained 41 | // by this activity) are restored before the RootService is started, causing NPE. 42 | super.onCreate(null); 43 | 44 | // Enable edge-to-edge: allows drawing under system bars, preventing Android from 45 | // automatically applying the fitSystemWindows property to the root view. 46 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false); 47 | 48 | // Start CoreRootService connection 49 | Intent intent = new Intent(this, CoreRootService.class); 50 | mCoreRootServiceConnection = new ServiceConnection() { 51 | @Override 52 | public void onServiceConnected(ComponentName name, IBinder service) { 53 | try { 54 | // Set references to the remote coreRootService 55 | mCoreRootServiceBound = true; 56 | mCoreRootServiceIpc = ICoreRootService.Stub.asInterface(service); 57 | mCoreRootServiceFSManager = FileSystemManager.getRemote(mCoreRootServiceIpc.getFileSystemService()); 58 | 59 | // Inflate the activity layout and set the content view 60 | mBinding = ActivityMainBinding.inflate(getLayoutInflater()); 61 | setContentView(mBinding.getRoot()); 62 | 63 | // Set the toolbar 64 | setSupportActionBar(mBinding.toolbar); 65 | 66 | // Set the drawer 67 | DrawerLayout drawer = mBinding.drawerLayout; 68 | mAppBarConfiguration = new AppBarConfiguration.Builder( 69 | R.id.nav_suggested_mods, 70 | R.id.nav_boolean_mods, 71 | R.id.nav_revert_mods, 72 | R.id.nav_information 73 | ).setOpenableLayout(drawer).build(); 74 | 75 | // Pass through the window insets to the navHostFragment child views, except the top system bar 76 | ViewCompat.setOnApplyWindowInsetsListener(mBinding.navHostFragment, (view, insets) -> { 77 | WindowInsetsCompat insetsCompat = new WindowInsetsCompat.Builder(insets) 78 | .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of( 79 | insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).left, 80 | 0, 81 | insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).right, 82 | insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).bottom)) 83 | .build(); 84 | return ViewCompat.onApplyWindowInsets(view, insetsCompat); 85 | }); 86 | 87 | // Set the navigation controller 88 | NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(mBinding.navHostFragment.getId()); 89 | if (navHostFragment != null) { 90 | NavController navController = navHostFragment.getNavController(); 91 | NavigationUI.setupActionBarWithNavController(MainActivity.this, navController, mAppBarConfiguration); 92 | NavigationUI.setupWithNavController(mBinding.navView, navController); 93 | } 94 | } catch (RemoteException e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | 99 | @Override 100 | public void onServiceDisconnected(ComponentName name) { 101 | mCoreRootServiceBound = false; 102 | mCoreRootServiceIpc = null; 103 | mCoreRootServiceFSManager = null; 104 | } 105 | }; 106 | RootService.bind(intent, mCoreRootServiceConnection); 107 | } 108 | 109 | public FileSystemManager getCoreRootServiceFSManager() { 110 | return mCoreRootServiceFSManager; 111 | } 112 | 113 | public ICoreRootService getCoreRootServiceIpc() { 114 | return mCoreRootServiceIpc; 115 | } 116 | 117 | @Override 118 | public boolean onSupportNavigateUp() { 119 | NavController navController = Navigation.findNavController(this, mBinding.navHostFragment.getId()); 120 | return NavigationUI.navigateUp(navController, mAppBarConfiguration) 121 | || super.onSupportNavigateUp(); 122 | } 123 | 124 | @Override 125 | public void onBackPressed() { 126 | if (mBinding.drawerLayout.isOpen()) 127 | mBinding.drawerLayout.close(); 128 | else 129 | finishAffinity(); 130 | } 131 | 132 | @Override 133 | protected void onDestroy() { 134 | if (mCoreRootServiceBound) 135 | RootService.unbind(mCoreRootServiceConnection); 136 | super.onDestroy(); 137 | } 138 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/activity/SplashScreenActivity.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.activity; 2 | 3 | import static com.jacopomii.gappsmod.data.Constants.GMS_ANDROID_PACKAGE_NAME; 4 | import static com.jacopomii.gappsmod.data.Constants.GOOGLE_PLAY_DETAILS_LINK; 5 | import static com.jacopomii.gappsmod.data.Constants.GMS_PHENOTYPE_DB; 6 | import static com.jacopomii.gappsmod.util.Utils.checkUpdateAvailable; 7 | import static com.jacopomii.gappsmod.util.Utils.openGooglePlay; 8 | 9 | import android.annotation.SuppressLint; 10 | import android.content.ComponentName; 11 | import android.content.Intent; 12 | import android.content.ServiceConnection; 13 | import android.net.Uri; 14 | import android.os.Bundle; 15 | import android.os.IBinder; 16 | import android.os.RemoteException; 17 | import android.view.View; 18 | import android.widget.ImageView; 19 | 20 | import androidx.appcompat.app.AppCompatActivity; 21 | 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 23 | import com.google.android.material.progressindicator.CircularProgressIndicator; 24 | import com.jacopomii.gappsmod.BuildConfig; 25 | import com.jacopomii.gappsmod.ICoreRootService; 26 | import com.jacopomii.gappsmod.R; 27 | import com.jacopomii.gappsmod.databinding.ActivitySplashScreenBinding; 28 | import com.jacopomii.gappsmod.service.CoreRootService; 29 | import com.topjohnwu.superuser.Shell; 30 | import com.topjohnwu.superuser.ipc.RootService; 31 | import com.topjohnwu.superuser.nio.FileSystemManager; 32 | 33 | import java.util.concurrent.CountDownLatch; 34 | 35 | @SuppressLint("CustomSplashScreen") 36 | public class SplashScreenActivity extends AppCompatActivity { 37 | static { 38 | // Set Libsu settings before creating the main shell 39 | Shell.enableVerboseLogging = BuildConfig.DEBUG; 40 | Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); 41 | } 42 | 43 | private ActivitySplashScreenBinding mBinding; 44 | 45 | private final CountDownLatch mRootCheckPassed = new CountDownLatch(1); 46 | private final CountDownLatch mCoreRootServiceConnected = new CountDownLatch(1); 47 | private final CountDownLatch mGMSPhenotypeCheckPassed = new CountDownLatch(1); 48 | private final CountDownLatch mUpdateCheckFinished = new CountDownLatch(1); 49 | 50 | private boolean mCoreRootServiceBound = false; 51 | private ServiceConnection mCoreRootServiceConnection; 52 | private FileSystemManager mCoreRootServiceFSManager; 53 | 54 | @Override 55 | protected void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | 58 | // Inflate the activity layout and set the content view 59 | mBinding = ActivitySplashScreenBinding.inflate(getLayoutInflater()); 60 | setContentView(R.layout.activity_splash_screen); 61 | 62 | // Start CoreRootService connection 63 | Intent intent = new Intent(this, CoreRootService.class); 64 | mCoreRootServiceConnection = new ServiceConnection() { 65 | @Override 66 | public void onServiceConnected(ComponentName name, IBinder service) { 67 | // Set references to the remote coreRootService 68 | mCoreRootServiceBound = true; 69 | ICoreRootService ipc = ICoreRootService.Stub.asInterface(service); 70 | try { 71 | mCoreRootServiceFSManager = FileSystemManager.getRemote(ipc.getFileSystemService()); 72 | mCoreRootServiceConnected.countDown(); 73 | } catch (RemoteException e) { 74 | e.printStackTrace(); 75 | } 76 | 77 | // Update the UI 78 | setCheckUIDone(mBinding.circularRootService.getId(), mBinding.doneRootService.getId(), mCoreRootServiceConnected.getCount() == 0); 79 | } 80 | 81 | @Override 82 | public void onServiceDisconnected(ComponentName name) { 83 | mCoreRootServiceBound = false; 84 | mCoreRootServiceFSManager = null; 85 | } 86 | }; 87 | RootService.bind(intent, mCoreRootServiceConnection); 88 | 89 | // Root permission check 90 | new Thread() { 91 | @Override 92 | public void run() { 93 | // Check root 94 | if (checkRoot()) { 95 | mRootCheckPassed.countDown(); 96 | } else { 97 | runOnUiThread(() -> 98 | new MaterialAlertDialogBuilder(SplashScreenActivity.this) 99 | .setCancelable(false) 100 | .setMessage(R.string.root_access_denied) 101 | .setPositiveButton(R.string.exit, (dialog, i) -> System.exit(0)) 102 | .show()); 103 | } 104 | 105 | // Update the UI 106 | setCheckUIDone(mBinding.circularRoot.getId(), mBinding.doneRoot.getId(), mRootCheckPassed.getCount() == 0); 107 | } 108 | }.start(); 109 | 110 | // GMS Phenotype DB check 111 | new Thread() { 112 | @Override 113 | public void run() { 114 | try { 115 | // Wait for root check to pass 116 | mRootCheckPassed.await(); 117 | // Wait for coreRootService to connect 118 | mCoreRootServiceConnected.await(); 119 | 120 | // Check the GMS Phenotype DB 121 | if (checkGMSPhenotypeDB()) { 122 | mGMSPhenotypeCheckPassed.countDown(); 123 | } else { 124 | runOnUiThread(() -> 125 | new MaterialAlertDialogBuilder(SplashScreenActivity.this) 126 | .setCancelable(false) 127 | .setMessage(getString(R.string.phenotype_db_does_not_exist_gms)) 128 | .setPositiveButton(R.string.install, (dialogInterface, i) -> openGooglePlay(SplashScreenActivity.this, GOOGLE_PLAY_DETAILS_LINK + GMS_ANDROID_PACKAGE_NAME)) 129 | .setNegativeButton(R.string.exit, (dialog, which) -> System.exit(0)) 130 | .setNeutralButton(R.string.continue_anyway, (dialogInterface, i) -> mGMSPhenotypeCheckPassed.countDown()) 131 | .show()); 132 | } 133 | 134 | // Update the UI 135 | setCheckUIDone(mBinding.circularPhenotypeGms.getId(), mBinding.donePhenotypeGms.getId(), mGMSPhenotypeCheckPassed.getCount() == 0); 136 | } catch (InterruptedException e) { 137 | e.printStackTrace(); 138 | } 139 | } 140 | }.start(); 141 | 142 | // Update available check 143 | new Thread() { 144 | @Override 145 | public void run() { 146 | // Check if updates are available 147 | if (!checkUpdateAvailable(SplashScreenActivity.this)) { 148 | mUpdateCheckFinished.countDown(); 149 | } else { 150 | runOnUiThread(() -> 151 | new MaterialAlertDialogBuilder(SplashScreenActivity.this) 152 | .setCancelable(false) 153 | .setMessage(R.string.new_version_alert) 154 | .setPositiveButton( 155 | R.string.github, 156 | (dialogInterface, i) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github_link) + "/releases"))) 157 | ) 158 | .setNegativeButton(R.string.continue_anyway, (dialogInterface, i) -> mUpdateCheckFinished.countDown()) 159 | .show()); 160 | } 161 | 162 | // Update the UI 163 | setCheckUIDone(mBinding.circularUpdates.getId(), mBinding.doneUpdates.getId(), mUpdateCheckFinished.getCount() == 0); 164 | } 165 | }.start(); 166 | 167 | // End splash screen and go to the main activity 168 | new Thread() { 169 | @Override 170 | public void run() { 171 | try { 172 | // Wait for all checks to pass and for all operations to finish 173 | mRootCheckPassed.await(); 174 | mCoreRootServiceConnected.await(); 175 | mGMSPhenotypeCheckPassed.await(); 176 | mUpdateCheckFinished.await(); 177 | 178 | // This is just for aesthetics: I don't want the splashscreen to be too fast 179 | Thread.sleep(1000); 180 | 181 | // Start the main activity 182 | Intent intent = new Intent(SplashScreenActivity.this, MainActivity.class); 183 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 184 | startActivity(intent); 185 | } catch (InterruptedException e) { 186 | e.printStackTrace(); 187 | } 188 | } 189 | }.start(); 190 | } 191 | 192 | private boolean checkRoot() { 193 | return Shell.getShell().isRoot(); 194 | } 195 | 196 | private boolean checkGMSPhenotypeDB() { 197 | return mCoreRootServiceFSManager.getFile(GMS_PHENOTYPE_DB).exists(); 198 | } 199 | 200 | private void setCheckUIDone(int circularID, int doneImageID, boolean success) { 201 | CircularProgressIndicator circular = findViewById(circularID); 202 | ImageView doneImage = findViewById(doneImageID); 203 | runOnUiThread(() -> { 204 | circular.setVisibility(View.GONE); 205 | doneImage.setImageResource(success ? R.drawable.ic_success_24 : R.drawable.ic_fail_24); 206 | doneImage.setVisibility(View.VISIBLE); 207 | }); 208 | } 209 | 210 | @Override 211 | protected void onDestroy() { 212 | if (mCoreRootServiceBound) 213 | RootService.unbind(mCoreRootServiceConnection); 214 | super.onDestroy(); 215 | } 216 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/adapter/BooleanModsRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.adapter; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Color; 7 | import android.os.RemoteException; 8 | import android.view.LayoutInflater; 9 | import android.view.ViewGroup; 10 | import android.widget.Filter; 11 | import android.widget.Filterable; 12 | import android.widget.TextView; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.recyclerview.widget.RecyclerView; 16 | 17 | import com.google.android.material.card.MaterialCardView; 18 | import com.jacopomii.gappsmod.ICoreRootService; 19 | import com.jacopomii.gappsmod.R; 20 | import com.jacopomii.gappsmod.data.BooleanFlag; 21 | import com.jacopomii.gappsmod.databinding.SwitchCardBinding; 22 | import com.jacopomii.gappsmod.ui.view.ProgrammaticMaterialSwitchView; 23 | import com.l4digital.fastscroll.FastScroller; 24 | 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.TreeMap; 32 | 33 | @SuppressWarnings("unchecked") 34 | public class BooleanModsRecyclerViewAdapter extends RecyclerView.Adapter implements Filterable, FastScroller.SectionIndexer { 35 | private final Context mContext; 36 | 37 | private List mFlagsList = new ArrayList<>(); 38 | private List mFlagsListFiltered = new ArrayList<>(); 39 | private String mPhenotypePackageName = null; 40 | private CharSequence mLastFilterPerformed = null; 41 | 42 | private final ICoreRootService mCoreRootServiceIpc; 43 | 44 | public BooleanModsRecyclerViewAdapter(Context context, ICoreRootService coreRootServiceIpc) { 45 | mContext = context; 46 | mCoreRootServiceIpc = coreRootServiceIpc; 47 | } 48 | 49 | @SuppressLint("NotifyDataSetChanged") 50 | public void selectPhenotypePackageName(String phenotypePackageName) { 51 | mPhenotypePackageName = phenotypePackageName; 52 | 53 | try { 54 | mFlagsList = new ArrayList<>(); 55 | TreeMap> map = new TreeMap>(mCoreRootServiceIpc.phenotypeDBGetBooleanFlagsOrOverridden(phenotypePackageName)); 56 | for (Map.Entry> flag : map.entrySet()) { 57 | String flagName = flag.getKey(); 58 | List flagData = flag.getValue(); 59 | Boolean flagValue = (Boolean) flagData.get(0); 60 | Boolean flagOverriddenAndChanged = (Boolean) flagData.get(1); 61 | mFlagsList.add(new BooleanFlag(flagName, flagValue, flagOverriddenAndChanged)); 62 | } 63 | 64 | if (mLastFilterPerformed != null) { 65 | getFilter().filter(mLastFilterPerformed); 66 | } else { 67 | mFlagsListFiltered = mFlagsList; 68 | notifyDataSetChanged(); 69 | } 70 | } catch (RemoteException e) { 71 | e.printStackTrace(); 72 | } 73 | } 74 | 75 | @NonNull 76 | @Override 77 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 78 | // Initialize binding and viewHolder 79 | SwitchCardBinding binding = SwitchCardBinding.inflate(LayoutInflater.from(mContext), parent, false); 80 | ViewHolder viewHolder = new ViewHolder(binding); 81 | 82 | // Set setOnCheckedChangeListener on list items 83 | viewHolder.mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 84 | int checkedPosition = viewHolder.getAdapterPosition(); 85 | BooleanFlag checkedBooleanFlag = mFlagsListFiltered.get(checkedPosition); 86 | String checkedBooleanFlagName = checkedBooleanFlag.getFlagName(); 87 | 88 | checkedBooleanFlag.setFlagValue(isChecked); 89 | try { 90 | mCoreRootServiceIpc.phenotypeDBOverrideBooleanFlag(mPhenotypePackageName, checkedBooleanFlagName, isChecked); 91 | } catch (RemoteException e) { 92 | e.printStackTrace(); 93 | } 94 | checkedBooleanFlag.setFlagOverriddenAndChanged(!checkedBooleanFlag.getFlagOverriddenAndChanged()); 95 | 96 | notifyItemChanged(checkedPosition); 97 | }); 98 | 99 | // Return viewHolder 100 | return viewHolder; 101 | } 102 | 103 | @Override 104 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 105 | // Get the boolean flag 106 | BooleanFlag booleanFlag = mFlagsListFiltered.get(position); 107 | 108 | // Update switch text 109 | holder.mTextView.setText(booleanFlag.getFlagName()); 110 | 111 | // Update the switch checked status without triggering any existing listener 112 | holder.mSwitch.setCheckedProgrammatically(booleanFlag.getFlagValue()); 113 | 114 | // Change background color for cards containing overridden and changed flags 115 | TypedArray typedArray = mContext.getTheme().obtainStyledAttributes(R.styleable.ViewStyle); 116 | int colorSurface = typedArray.getColor(R.styleable.ViewStyle_colorSurface, Color.WHITE); 117 | int colorSurfaceVariant = typedArray.getColor(R.styleable.ViewStyle_colorSecondaryContainer, Color.LTGRAY); 118 | typedArray.recycle(); 119 | int cardBackgroundColor = booleanFlag.getFlagOverriddenAndChanged() ? colorSurfaceVariant : colorSurface; 120 | ((MaterialCardView) holder.itemView).setCardBackgroundColor(cardBackgroundColor); 121 | } 122 | 123 | @Override 124 | public int getItemCount() { 125 | return mFlagsListFiltered.size(); 126 | } 127 | 128 | @Override 129 | public Filter getFilter() { 130 | return new Filter() { 131 | @Override 132 | protected FilterResults performFiltering(CharSequence charSequence) { 133 | mLastFilterPerformed = charSequence; 134 | 135 | try { 136 | JSONObject filterConfig = new JSONObject(charSequence.toString()); 137 | String key = filterConfig.getString("key"); 138 | boolean enabled = filterConfig.getBoolean("enabled"); 139 | boolean disabled = filterConfig.getBoolean("disabled"); 140 | boolean changed = filterConfig.getBoolean("changed"); 141 | boolean unchanged = filterConfig.getBoolean("unchanged"); 142 | 143 | List flagsListFiltered = new ArrayList<>(); 144 | for (BooleanFlag booleanFlag : mFlagsList) { 145 | if (booleanFlag.getFlagName().toLowerCase().contains(key.toLowerCase())) { 146 | boolean flagValue = booleanFlag.getFlagValue(); 147 | boolean flagChanged = booleanFlag.getFlagOverriddenAndChanged(); 148 | if (((enabled && flagValue) || (disabled && !flagValue)) && ((changed && flagChanged) || (unchanged && !flagChanged))) 149 | flagsListFiltered.add(booleanFlag); 150 | } 151 | } 152 | mFlagsListFiltered = flagsListFiltered; 153 | } catch (JSONException e) { 154 | e.printStackTrace(); 155 | } 156 | 157 | FilterResults filterResults = new FilterResults(); 158 | filterResults.values = mFlagsListFiltered; 159 | filterResults.count = mFlagsListFiltered.size(); 160 | return filterResults; 161 | } 162 | 163 | @SuppressLint("NotifyDataSetChanged") 164 | @Override 165 | protected void publishResults(CharSequence charSequence, FilterResults filterResults) { 166 | mFlagsListFiltered = (List) filterResults.values; 167 | notifyDataSetChanged(); 168 | } 169 | }; 170 | } 171 | 172 | @Override 173 | public CharSequence getSectionText(int position) { 174 | return mFlagsListFiltered.get(position).getFlagName().substring(0, 1); 175 | } 176 | 177 | public static class ViewHolder extends RecyclerView.ViewHolder { 178 | private final TextView mTextView; 179 | private final ProgrammaticMaterialSwitchView mSwitch; 180 | 181 | public ViewHolder(SwitchCardBinding binding) { 182 | super(binding.getRoot()); 183 | mTextView = binding.switchCardTextview; 184 | mSwitch = binding.switchCardSwitch; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/adapter/SelectPackageRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.adapter; 2 | 3 | import static com.jacopomii.gappsmod.util.Utils.getApplicationLabelOrUnknown; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | import android.content.res.TypedArray; 9 | import android.graphics.Color; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.RemoteException; 12 | import android.view.LayoutInflater; 13 | import android.view.ViewGroup; 14 | import android.widget.Filter; 15 | import android.widget.Filterable; 16 | import android.widget.ImageView; 17 | import android.widget.LinearLayout; 18 | import android.widget.TextView; 19 | 20 | import androidx.annotation.NonNull; 21 | import androidx.appcompat.content.res.AppCompatResources; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | 24 | import com.jacopomii.gappsmod.ICoreRootService; 25 | import com.jacopomii.gappsmod.R; 26 | import com.jacopomii.gappsmod.data.PhenotypeDBPackageName; 27 | import com.jacopomii.gappsmod.databinding.PackageRowBinding; 28 | import com.jacopomii.gappsmod.util.OnItemClickListener; 29 | import com.l4digital.fastscroll.FastScroller; 30 | 31 | import org.apache.commons.lang3.StringUtils; 32 | 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.TreeMap; 37 | 38 | @SuppressWarnings("unchecked") 39 | public class SelectPackageRecyclerViewAdapter extends RecyclerView.Adapter implements Filterable, FastScroller.SectionIndexer { 40 | private final Context mContext; 41 | 42 | private List mPackageList = new ArrayList<>(); 43 | private List mPackageListFiltered = new ArrayList<>(); 44 | 45 | private final ICoreRootService mCoreRootServiceIpc; 46 | 47 | private final OnItemClickListener mOnItemClickListener; 48 | 49 | @SuppressLint("NotifyDataSetChanged") 50 | public SelectPackageRecyclerViewAdapter(Context context, ICoreRootService coreRootServiceIpc, OnItemClickListener onItemClickListener) { 51 | mContext = context; 52 | mCoreRootServiceIpc = coreRootServiceIpc; 53 | mOnItemClickListener = onItemClickListener; 54 | 55 | try { 56 | mPackageList = new ArrayList<>(); 57 | TreeMap map = new TreeMap(mCoreRootServiceIpc.phenotypeDBGetAllPackageNames()); 58 | for (Map.Entry packageName : map.entrySet()) 59 | mPackageList.add(new PhenotypeDBPackageName(packageName.getKey(), packageName.getValue())); 60 | 61 | mPackageListFiltered = mPackageList; 62 | 63 | notifyDataSetChanged(); 64 | } catch (RemoteException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | @NonNull 70 | @Override 71 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 72 | // Initialize binding and viewHolder 73 | PackageRowBinding binding = PackageRowBinding.inflate(LayoutInflater.from(mContext), parent, false); 74 | ViewHolder viewHolder = new ViewHolder(binding); 75 | 76 | // Set onClickListener on list rows 77 | viewHolder.mRow.setOnClickListener(v -> { 78 | int position = viewHolder.getAdapterPosition(); 79 | mOnItemClickListener.onItemClick(mPackageListFiltered.get(position).getPhenotypePackageName()); 80 | }); 81 | 82 | // Return viewHolder 83 | return viewHolder; 84 | } 85 | 86 | @Override 87 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 88 | String phenotypePackageName = mPackageListFiltered.get(position).getPhenotypePackageName(); 89 | String androidPackageName = mPackageListFiltered.get(position).getAndroidPackageName(); 90 | 91 | PackageManager packageManager = mContext.getPackageManager(); 92 | 93 | Drawable packageIcon; 94 | try { 95 | packageIcon = packageManager.getApplicationIcon(androidPackageName); 96 | } catch (PackageManager.NameNotFoundException e) { 97 | packageIcon = AppCompatResources.getDrawable(mContext, R.drawable.ic_error_24); 98 | if (packageIcon != null) { 99 | TypedArray typedArray = mContext.getTheme().obtainStyledAttributes(R.styleable.ViewStyle); 100 | int colorError = typedArray.getColor(R.styleable.ViewStyle_colorError, Color.RED); 101 | typedArray.recycle(); 102 | packageIcon.mutate().setTint(colorError); 103 | } 104 | } 105 | holder.mPackageIcon.setImageDrawable(packageIcon); 106 | 107 | String appName = getApplicationLabelOrUnknown(mContext, androidPackageName); 108 | holder.mAppName.setText(appName); 109 | 110 | holder.mPhenotypePackageName.setText(phenotypePackageName); 111 | } 112 | 113 | @Override 114 | public int getItemCount() { 115 | return mPackageListFiltered.size(); 116 | } 117 | 118 | @Override 119 | public Filter getFilter() { 120 | return new Filter() { 121 | @Override 122 | protected FilterResults performFiltering(CharSequence charSequence) { 123 | String keyLowercase = charSequence.toString().toLowerCase(); 124 | 125 | List packageListFiltered = new ArrayList<>(); 126 | for (PhenotypeDBPackageName phenotypeDBPackageName : mPackageList) { 127 | String phenotypePackageNameLowercase = phenotypeDBPackageName.getPhenotypePackageName().toLowerCase(); 128 | String appNameLowercase = getApplicationLabelOrUnknown(mContext, phenotypeDBPackageName.getAndroidPackageName()).toLowerCase(); 129 | if (phenotypePackageNameLowercase.contains(keyLowercase) || appNameLowercase.contains(keyLowercase)) 130 | packageListFiltered.add(phenotypeDBPackageName); 131 | } 132 | mPackageListFiltered = packageListFiltered; 133 | 134 | FilterResults filterResults = new FilterResults(); 135 | filterResults.values = mPackageListFiltered; 136 | filterResults.count = mPackageListFiltered.size(); 137 | return filterResults; 138 | } 139 | 140 | @SuppressLint("NotifyDataSetChanged") 141 | @Override 142 | protected void publishResults(CharSequence charSequence, FilterResults filterResults) { 143 | mPackageListFiltered = (List) filterResults.values; 144 | notifyDataSetChanged(); 145 | } 146 | }; 147 | } 148 | 149 | @Override 150 | public CharSequence getSectionText(int position) { 151 | // Get raw Phenotype package name 152 | String phenotypePackageName = mPackageListFiltered.get(position).getPhenotypePackageName(); 153 | 154 | // Initialize indexEnd 155 | int indexEnd = 0; 156 | 157 | // Try to split package name by dot character: look for the first 4 parts, if there aren't try 3, 2 and so on 158 | for (int i = 4; i >= 0; i--) { 159 | int indexEndTmp = StringUtils.ordinalIndexOf(phenotypePackageName, ".", i); 160 | if (indexEndTmp != -1) { 161 | indexEnd = indexEndTmp; 162 | break; 163 | } 164 | } 165 | 166 | // Increment indexEnd by 2 to show two more characters beyond the found parts 167 | if (indexEnd + 2 <= phenotypePackageName.length()) 168 | indexEnd += 2; 169 | else 170 | indexEnd = phenotypePackageName.length(); 171 | 172 | // Return the phenotypePackageName parsed substring 173 | return phenotypePackageName.substring(0, indexEnd); 174 | } 175 | 176 | public static class ViewHolder extends RecyclerView.ViewHolder { 177 | private final LinearLayout mRow; 178 | private final ImageView mPackageIcon; 179 | private final TextView mAppName; 180 | private final TextView mPhenotypePackageName; 181 | 182 | public ViewHolder(PackageRowBinding binding) { 183 | super(binding.getRoot()); 184 | mRow = binding.row; 185 | mPackageIcon = binding.packageIcon; 186 | mAppName = binding.appName; 187 | mPhenotypePackageName = binding.phenotypePackageName; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/fragment/BooleanModsFragment.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.fragment; 2 | 3 | import static com.jacopomii.gappsmod.util.Utils.showSelectPackageDialog; 4 | 5 | import android.app.Activity; 6 | import android.graphics.Rect; 7 | import android.os.Bundle; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuInflater; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.ArrayAdapter; 15 | import android.widget.TextView; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.annotation.Nullable; 19 | import androidx.appcompat.widget.SearchView; 20 | import androidx.core.view.MenuProvider; 21 | import androidx.fragment.app.Fragment; 22 | import androidx.lifecycle.Lifecycle; 23 | import androidx.recyclerview.widget.LinearLayoutManager; 24 | import androidx.recyclerview.widget.RecyclerView; 25 | 26 | import com.jacopomii.gappsmod.ICoreRootService; 27 | import com.jacopomii.gappsmod.R; 28 | import com.jacopomii.gappsmod.databinding.FragmentBooleanModsBinding; 29 | import com.jacopomii.gappsmod.ui.activity.MainActivity; 30 | import com.jacopomii.gappsmod.ui.adapter.BooleanModsRecyclerViewAdapter; 31 | import com.jacopomii.gappsmod.ui.view.FilterableSearchView; 32 | import com.l4digital.fastscroll.FastScrollRecyclerView; 33 | 34 | import org.json.JSONException; 35 | import org.json.JSONObject; 36 | 37 | import java.util.concurrent.atomic.AtomicBoolean; 38 | 39 | public class BooleanModsFragment extends Fragment { 40 | private FragmentBooleanModsBinding mBinding; 41 | 42 | private BooleanModsRecyclerViewAdapter mFlagsRecyclerViewAdapter; 43 | 44 | private ICoreRootService mCoreRootServiceIpc; 45 | 46 | private String flagsFilterKey = ""; 47 | private boolean flagsFilterEnabled = true; 48 | private boolean flagsFilterDisabled = true; 49 | private boolean flagsFilterChanged = true; 50 | private boolean flagsFilterUnchanged = true; 51 | 52 | public BooleanModsFragment() { 53 | } 54 | 55 | @Override 56 | public void onCreate(@Nullable Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | 59 | Activity activity = getActivity(); 60 | if (activity instanceof MainActivity) 61 | mCoreRootServiceIpc = ((MainActivity) activity).getCoreRootServiceIpc(); 62 | else 63 | throw new RuntimeException("SuggestedModsFragment can be attached only to the MainActivity"); 64 | } 65 | 66 | @Override 67 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 68 | // View bindings 69 | mBinding = FragmentBooleanModsBinding.inflate(getLayoutInflater()); 70 | 71 | 72 | // Setup menu 73 | setupMenu(); 74 | 75 | 76 | // Select package 77 | TextView selectPackage = mBinding.selectPackage; 78 | 79 | // Initialize the selectPackageDialogOpened 80 | AtomicBoolean selectPackageDialogOpened = new AtomicBoolean(false); 81 | 82 | // Select package onClick 83 | selectPackage.setOnClickListener(v -> { 84 | // Clear focus from other views 85 | View currentFocus = requireActivity().getCurrentFocus(); 86 | if (currentFocus != null) currentFocus.clearFocus(); 87 | 88 | // If the select package dialog isn't already opened 89 | if (!selectPackageDialogOpened.get()) { 90 | // Set the selectPackageDialogOpened to true 91 | selectPackageDialogOpened.set(true); 92 | 93 | // Show the select package dialog 94 | showSelectPackageDialog(getContext(), mCoreRootServiceIpc, item -> { 95 | // The item received by the listener here is the Phenotype package name chosen by the user 96 | 97 | // Update the select package textview 98 | selectPackage.setText((String) item); 99 | 100 | // Update the selectPackageRecyclerView adapter 101 | mFlagsRecyclerViewAdapter.selectPhenotypePackageName((String) item); 102 | 103 | // Set the selectPackageDialogOpened to false 104 | selectPackageDialogOpened.set(false); 105 | }, dialog -> { 106 | // Set the selectPackageDialogOpened to false dismissing the dialog 107 | selectPackageDialogOpened.set(false); 108 | }); 109 | } 110 | }); 111 | 112 | 113 | // Flags recyclerview 114 | FastScrollRecyclerView flagsRecyclerView = mBinding.recyclerview; 115 | 116 | // Initialize the flagsRecyclerView adapter 117 | mFlagsRecyclerViewAdapter = new BooleanModsRecyclerViewAdapter(getContext(), mCoreRootServiceIpc); 118 | 119 | // Disable fast scroll if the flagsRecyclerView is empty or changes to empty 120 | flagsRecyclerView.setFastScrollEnabled(mFlagsRecyclerViewAdapter.getItemCount() != 0); 121 | mFlagsRecyclerViewAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 122 | @Override 123 | public void onChanged() { 124 | super.onChanged(); 125 | flagsRecyclerView.setFastScrollEnabled(mFlagsRecyclerViewAdapter.getItemCount() != 0); 126 | } 127 | }); 128 | 129 | // Set flagsRecyclerView items padding 130 | flagsRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 131 | @Override 132 | public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 133 | int padding = (int) getResources().getDimension(R.dimen.margin_generic); 134 | 135 | int itemPosition = parent.getChildAdapterPosition(view); 136 | 137 | if (itemPosition == 0) outRect.top = padding; 138 | else if (itemPosition == mFlagsRecyclerViewAdapter.getItemCount() - 1) 139 | outRect.bottom = padding; 140 | 141 | outRect.left = padding; 142 | outRect.right = padding; 143 | } 144 | }); 145 | 146 | // Set the flagsRecyclerView LayoutManager and Adapter 147 | flagsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 148 | flagsRecyclerView.setAdapter(mFlagsRecyclerViewAdapter); 149 | 150 | 151 | // Return the fragment view 152 | return mBinding.getRoot(); 153 | } 154 | 155 | private void setupMenu() { 156 | requireActivity().addMenuProvider(new MenuProvider() { 157 | @Override 158 | public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { 159 | // Inflate the menu layout 160 | menuInflater.inflate(R.menu.search_menu, menu); 161 | 162 | // Initialize the menuSearchIcon 163 | MenuItem menuSearchIcon = menu.findItem(R.id.menu_search_icon); 164 | 165 | // Initialize filterableSearchView and the additional filter container 166 | FilterableSearchView filterableSearchView = (FilterableSearchView) menuSearchIcon.getActionView(); 167 | filterableSearchView.setQueryHint(getString(R.string.search_by_flag)); 168 | filterableSearchView.setFilterContainer(mBinding.filterContainer, false); 169 | 170 | // Initialize the filterEnabledStatusSpinner 171 | String[] filterEnabledStatusSpinnerChoices = new String[]{getString(R.string.enabled_and_disabled), getString(R.string.enabled_only), getString(R.string.disabled_only)}; 172 | mBinding.filterEnabledStatusSpinner.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterEnabledStatusSpinnerChoices)); 173 | mBinding.filterEnabledStatusSpinner.setOnItemClickListener((parent, view, position, id) -> { 174 | flagsFilterEnabled = position == 0 || position == 1; 175 | flagsFilterDisabled = position == 0 || position == 2; 176 | applyFlagsFilters(); 177 | }); 178 | 179 | // Initialize the filterChangedStatusSpinner 180 | String[] filterChangedStatusSpinnerChoices = new String[]{getString(R.string.changed_and_unchanged), getString(R.string.changed_only), getString(R.string.unchanged_only)}; 181 | mBinding.filterChangedStatusSpinner.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterChangedStatusSpinnerChoices)); 182 | mBinding.filterChangedStatusSpinner.setOnItemClickListener((parent, view, position, id) -> { 183 | flagsFilterChanged = position == 0 || position == 1; 184 | flagsFilterUnchanged = position == 0 || position == 2; 185 | applyFlagsFilters(); 186 | }); 187 | 188 | // Set flags filters to default values 189 | resetFlagsFilters(); 190 | 191 | // Handle menuSearchIcon expand / collapse actions 192 | menuSearchIcon.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { 193 | @Override 194 | public boolean onMenuItemActionExpand(MenuItem item) { 195 | return true; 196 | } 197 | 198 | @Override 199 | public boolean onMenuItemActionCollapse(MenuItem item) { 200 | // When the search view is collapsed, flag filters need to be reset and applied 201 | resetFlagsFilters(); 202 | applyFlagsFilters(); 203 | return true; 204 | } 205 | }); 206 | 207 | // Handle filterableSearchView search query changes 208 | filterableSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 209 | @Override 210 | public boolean onQueryTextSubmit(String query) { 211 | return false; 212 | } 213 | 214 | @Override 215 | public boolean onQueryTextChange(String newText) { 216 | flagsFilterKey = newText; 217 | applyFlagsFilters(); 218 | return false; 219 | } 220 | }); 221 | } 222 | 223 | @Override 224 | public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { 225 | return false; 226 | } 227 | }, getViewLifecycleOwner(), Lifecycle.State.RESUMED); 228 | } 229 | 230 | private void resetFlagsFilters() { 231 | flagsFilterKey = ""; 232 | flagsFilterEnabled = true; 233 | flagsFilterDisabled = true; 234 | flagsFilterChanged = true; 235 | flagsFilterUnchanged = true; 236 | mBinding.filterEnabledStatusSpinner.setText(mBinding.filterEnabledStatusSpinner.getAdapter().getItem(0).toString(), false); 237 | mBinding.filterChangedStatusSpinner.setText(mBinding.filterChangedStatusSpinner.getAdapter().getItem(0).toString(), false); 238 | } 239 | 240 | private void applyFlagsFilters() { 241 | try { 242 | JSONObject filterConfig = new JSONObject(); 243 | 244 | filterConfig.put("key", flagsFilterKey); 245 | filterConfig.put("enabled", flagsFilterEnabled); 246 | filterConfig.put("disabled", flagsFilterDisabled); 247 | filterConfig.put("changed", flagsFilterChanged); 248 | filterConfig.put("unchanged", flagsFilterUnchanged); 249 | 250 | mFlagsRecyclerViewAdapter.getFilter().filter(filterConfig.toString()); 251 | } catch (JSONException e) { 252 | e.printStackTrace(); 253 | } 254 | } 255 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/fragment/InformationFragment.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.text.method.LinkMovementMethod; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.fragment.app.Fragment; 11 | 12 | import com.jacopomii.gappsmod.databinding.FragmentInformationBinding; 13 | 14 | public class InformationFragment extends Fragment { 15 | FragmentInformationBinding mBinding; 16 | 17 | public InformationFragment() {} 18 | 19 | @Override 20 | public void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | } 23 | 24 | @Override 25 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 26 | mBinding = FragmentInformationBinding.inflate(getLayoutInflater()); 27 | 28 | // Links aren't clickable workaround 29 | mBinding.madeWithLoveByJacopoTediosi.setMovementMethod(LinkMovementMethod.getInstance()); 30 | 31 | return mBinding.getRoot(); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/fragment/RevertModsFragment.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.fragment; 2 | 3 | import static com.jacopomii.gappsmod.data.Constants.DIALER_CALLRECORDINGPROMPT; 4 | import static com.jacopomii.gappsmod.data.Constants.DIALER_PHENOTYPE_PACKAGE_NAME; 5 | import static com.jacopomii.gappsmod.util.Utils.showSelectPackageDialog; 6 | 7 | import android.app.Activity; 8 | import android.os.Bundle; 9 | import android.os.RemoteException; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | 16 | import androidx.annotation.NonNull; 17 | import androidx.fragment.app.Fragment; 18 | 19 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 20 | import com.jacopomii.gappsmod.ICoreRootService; 21 | import com.jacopomii.gappsmod.R; 22 | import com.jacopomii.gappsmod.databinding.FragmentRevertModsBinding; 23 | import com.jacopomii.gappsmod.ui.activity.MainActivity; 24 | import com.topjohnwu.superuser.nio.ExtendedFile; 25 | import com.topjohnwu.superuser.nio.FileSystemManager; 26 | 27 | import java.util.concurrent.atomic.AtomicBoolean; 28 | 29 | import es.dmoral.toasty.Toasty; 30 | 31 | public class RevertModsFragment extends Fragment { 32 | FragmentRevertModsBinding mBinding; 33 | 34 | private ICoreRootService mCoreRootServiceIpc; 35 | private FileSystemManager mCoreRootServiceFSManager; 36 | 37 | public RevertModsFragment() { 38 | } 39 | 40 | @Override 41 | public void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | 44 | Activity activity = getActivity(); 45 | if (activity instanceof MainActivity) { 46 | mCoreRootServiceIpc = ((MainActivity) activity).getCoreRootServiceIpc(); 47 | mCoreRootServiceFSManager = ((MainActivity) activity).getCoreRootServiceFSManager(); 48 | } else { 49 | throw new RuntimeException("RevertModsFragment can be attached only to the MainActivity"); 50 | } 51 | } 52 | 53 | @Override 54 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 55 | // View bindings 56 | mBinding = FragmentRevertModsBinding.inflate(getLayoutInflater()); 57 | 58 | 59 | // Select package for revert mods for specific package 60 | TextView selectPackage = mBinding.selectPackage; 61 | 62 | // Initialize the selectPackageDialogOpened 63 | AtomicBoolean selectPackageDialogOpened = new AtomicBoolean(false); 64 | 65 | // Select package onClick 66 | selectPackage.setOnClickListener(v -> { 67 | // If the select package dialog isn't already opened 68 | if (!selectPackageDialogOpened.get()) { 69 | // Set the selectPackageDialogOpened to true 70 | selectPackageDialogOpened.set(true); 71 | 72 | // Show the select package dialog 73 | showSelectPackageDialog( 74 | getContext(), 75 | mCoreRootServiceIpc, 76 | item -> { 77 | // The item received by the listener here is the Phenotype package name chosen by the user 78 | 79 | // Update the select package textview 80 | selectPackage.setText((String) item); 81 | 82 | // Enable the revertModsSelectedPackageButton 83 | mBinding.revertModsSelectedPackageButton.setEnabled(true); 84 | 85 | }, 86 | dialog -> { 87 | // Set the selectPackageDialogOpened to false dismissing the dialog 88 | selectPackageDialogOpened.set(false); 89 | }); 90 | } 91 | }); 92 | 93 | 94 | // Revert mods for the selected package button 95 | mBinding.revertModsSelectedPackageButton.setOnClickListener(v -> { 96 | String selectedPhenotypePackageName = selectPackage.getText().toString(); 97 | new MaterialAlertDialogBuilder(requireContext()) 98 | .setMessage(String.format(getResources().getString(R.string.revert_mods_for_the_selected_package_confirm), selectedPhenotypePackageName)) 99 | .setNegativeButton(getString(R.string.no), (dialog, which) -> { 100 | }) 101 | .setPositiveButton(getString(R.string.yes), (dialog, which) -> { 102 | try { 103 | // Delete all flag overrides for the selected package from Phenotype DB 104 | mCoreRootServiceIpc.phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(selectedPhenotypePackageName); 105 | 106 | // If the selected package was the Dialer 107 | if (selectedPhenotypePackageName.equals(DIALER_PHENOTYPE_PACKAGE_NAME)) { 108 | // Delete the com.google.android.dialer callrecordingprompt folder (if it exists) 109 | ExtendedFile callRecordingPromptFolder = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); 110 | if (callRecordingPromptFolder.exists()) { 111 | //noinspection ResultOfMethodCallIgnored 112 | callRecordingPromptFolder.delete(); 113 | } 114 | } 115 | 116 | // UI confirmation to the user 117 | Toasty.success(requireContext(), getString(R.string.done), Toast.LENGTH_LONG, true).show(); 118 | } catch (RemoteException e) { 119 | e.printStackTrace(); 120 | Toasty.error(requireContext(), getString(R.string.an_error_has_occurred), Toast.LENGTH_LONG, true).show(); 121 | } 122 | }).show(); 123 | } 124 | ); 125 | 126 | 127 | // Revert all mods button 128 | mBinding.revertAllModsButton.setOnClickListener(v -> 129 | new MaterialAlertDialogBuilder(requireContext()) 130 | .setMessage(R.string.revert_mods_for_all_packages_confirm) 131 | .setNegativeButton(getString(R.string.no), (dialog, which) -> { 132 | }) 133 | .setPositiveButton(getString(R.string.yes), (dialog, which) -> { 134 | try { 135 | // Delete all flag overrides from Phenotype DB 136 | mCoreRootServiceIpc.phenotypeDBDeleteAllFlagOverrides(); 137 | 138 | // Delete the com.google.android.dialer callrecordingprompt folder 139 | ExtendedFile callRecordingPromptFolder = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); 140 | if (callRecordingPromptFolder.exists()) { 141 | //noinspection ResultOfMethodCallIgnored 142 | callRecordingPromptFolder.delete(); 143 | } 144 | 145 | // UI confirmation to the user 146 | Toasty.success(requireContext(), getString(R.string.done), Toast.LENGTH_LONG, true).show(); 147 | } catch (RemoteException e) { 148 | e.printStackTrace(); 149 | Toasty.error(requireContext(), getString(R.string.an_error_has_occurred), Toast.LENGTH_LONG, true).show(); 150 | } 151 | }).show() 152 | ); 153 | 154 | return mBinding.getRoot(); 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/view/FilterableSearchView.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.util.AttributeSet; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.widget.LinearLayout; 9 | 10 | import androidx.appcompat.view.CollapsibleActionView; 11 | import androidx.appcompat.widget.SearchView; 12 | import androidx.core.content.res.ResourcesCompat; 13 | 14 | import com.jacopomii.gappsmod.R; 15 | import com.jacopomii.gappsmod.databinding.FilterableSearchviewBinding; 16 | 17 | /** 18 | * A View, containing a {@link SearchView}, to which an additional View can optionally be connected 19 | * as a container to contain components for filtering the search, via the 20 | * {@link #setFilterContainer} method. When a filterContainer is set, a button appears next to the 21 | * SearchView that allows the user to manually show / hide the filterContainer. 22 | */ 23 | // Here I use the deprecated CollapsibleActionView interface because otherwise the 24 | // onActionViewExpanded and onActionViewCollapsed methods are never called, idk why 25 | @SuppressWarnings("deprecation") 26 | public class FilterableSearchView extends LinearLayout implements CollapsibleActionView { 27 | FilterableSearchviewBinding mBinding; 28 | private final Context mContext; 29 | 30 | private View mFilterContainer; 31 | private boolean mIsFilterContainerVisible; 32 | private boolean mFilterContainerAutoExpand; 33 | 34 | public FilterableSearchView(Context context) { 35 | super(context); 36 | mContext = context; 37 | init(); 38 | } 39 | 40 | public FilterableSearchView(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | mContext = context; 43 | init(); 44 | } 45 | 46 | private void init() { 47 | mBinding = FilterableSearchviewBinding.inflate(LayoutInflater.from(mContext), this, true); 48 | mBinding.collapseFilterContainerButton.setOnClickListener(v -> { 49 | if (mFilterContainer != null) setFilterContainerVisibility(!mIsFilterContainerVisible); 50 | }); 51 | } 52 | 53 | /** 54 | * Connects a filter container to the SearchView, which can be shown / hidden by the user 55 | * using a special button. 56 | * 57 | * @param filterContainer the filter container to attach. 58 | * @param filterContainerAutoExpand whether the filter container should open itself when the 59 | * SearchView is expanded. 60 | */ 61 | public void setFilterContainer(View filterContainer, boolean filterContainerAutoExpand) { 62 | mFilterContainer = filterContainer; 63 | mFilterContainerAutoExpand = filterContainerAutoExpand; 64 | mBinding.collapseFilterContainerButton.setVisibility(VISIBLE); 65 | } 66 | 67 | /** 68 | * Sets the hint text to display in the query text field of the SearchView. 69 | * 70 | * @param hint the hint text to display or {@code null}. 71 | */ 72 | public void setQueryHint(CharSequence hint) { 73 | mBinding.searchView.setQueryHint(hint); 74 | } 75 | 76 | /** 77 | * Sets a listener for user actions within the SearchView. 78 | * 79 | * @param listener the listener object that receives callbacks when the user performs actions 80 | * in the SearchView such as clicking on buttons or typing a query. 81 | */ 82 | public void setOnQueryTextListener(SearchView.OnQueryTextListener listener) { 83 | mBinding.searchView.setOnQueryTextListener(listener); 84 | } 85 | 86 | @Override 87 | public void onActionViewExpanded() { 88 | if (mFilterContainer != null && mFilterContainerAutoExpand) 89 | setFilterContainerVisibility(true); 90 | mBinding.searchView.onActionViewExpanded(); 91 | } 92 | 93 | @Override 94 | public void onActionViewCollapsed() { 95 | if (mFilterContainer != null) setFilterContainerVisibility(false); 96 | mBinding.searchView.onActionViewCollapsed(); 97 | } 98 | 99 | private void setFilterContainerVisibility(boolean visible) { 100 | mIsFilterContainerVisible = visible; 101 | 102 | int newFilterContainerVisibility; 103 | int newCollapseFilterButtonDrawableID; 104 | 105 | if (visible) { 106 | newFilterContainerVisibility = View.VISIBLE; 107 | newCollapseFilterButtonDrawableID = R.drawable.ic_arrow_up_24; 108 | } else { 109 | newFilterContainerVisibility = View.GONE; 110 | newCollapseFilterButtonDrawableID = R.drawable.ic_arrow_down_24; 111 | } 112 | 113 | mFilterContainer.setVisibility(newFilterContainerVisibility); 114 | 115 | Drawable newCollapseFilterButtonDrawable = ResourcesCompat.getDrawable(getResources(), newCollapseFilterButtonDrawableID, null); 116 | mBinding.collapseFilterContainerButton.setImageDrawable(newCollapseFilterButtonDrawable); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/view/ProgrammaticMaterialSwitchView.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | 9 | import com.google.android.material.materialswitch.MaterialSwitch; 10 | 11 | /** 12 | * A {@link MaterialSwitch} that allows to programmatically set the checked / unchecked state 13 | * without triggering the onCheckedChangeListener. 14 | */ 15 | public class ProgrammaticMaterialSwitchView extends MaterialSwitch { 16 | private OnCheckedChangeListener mOnCheckedChangeListener = null; 17 | 18 | public ProgrammaticMaterialSwitchView(@NonNull Context context) { 19 | super(context); 20 | } 21 | 22 | public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | } 29 | 30 | @Override 31 | public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) { 32 | mOnCheckedChangeListener = listener; 33 | super.setOnCheckedChangeListener(listener); 34 | } 35 | 36 | /** 37 | * Programmatically change the checked state of the switch without calling any 38 | * onCheckedChangeListener. Please note that any previously set onCheckedChangeListener will be 39 | * preserved, even if this method does not call it. 40 | * 41 | * @param checked {@code true} to check the switch, {@code false} to uncheck it. 42 | */ 43 | public void setCheckedProgrammatically(boolean checked) { 44 | super.setOnCheckedChangeListener(null); 45 | super.setChecked(checked); 46 | super.setOnCheckedChangeListener(mOnCheckedChangeListener); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/view/SuggestedModsAppHeaderView.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.view.LayoutInflater; 7 | import android.widget.LinearLayout; 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | import com.google.android.material.button.MaterialButton; 12 | import com.jacopomii.gappsmod.R; 13 | import com.jacopomii.gappsmod.databinding.SuggestedModsAppHeaderBinding; 14 | 15 | /** 16 | * The application name header used by the "Suggested Mods" fragment. 17 | * It includes a large title for the app name and two localized buttons "Beta" and "Install". 18 | */ 19 | public class SuggestedModsAppHeaderView extends LinearLayout { 20 | final SuggestedModsAppHeaderBinding mBinding; 21 | 22 | public SuggestedModsAppHeaderView(Context context) { 23 | super(context); 24 | 25 | mBinding = SuggestedModsAppHeaderBinding.inflate(LayoutInflater.from(context), this, true); 26 | } 27 | 28 | public SuggestedModsAppHeaderView(Context context, @Nullable AttributeSet attrs) { 29 | super(context, attrs); 30 | 31 | mBinding = SuggestedModsAppHeaderBinding.inflate(LayoutInflater.from(context), this, true); 32 | 33 | final TypedArray xmlAttrs = context.obtainStyledAttributes(attrs, R.styleable.SuggestedModsAppHeaderView); 34 | final String appName = xmlAttrs.getString(R.styleable.SuggestedModsAppHeaderView_app_name); 35 | xmlAttrs.recycle(); 36 | 37 | mBinding.appName.setText(appName); 38 | } 39 | 40 | public MaterialButton getBetaButton() { 41 | return mBinding.betaButton; 42 | } 43 | 44 | public MaterialButton getInstallButton() { 45 | return mBinding.installButton; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/ui/view/SwitchCardView.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.ui.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.view.LayoutInflater; 7 | import android.widget.LinearLayout; 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | import com.jacopomii.gappsmod.R; 12 | import com.jacopomii.gappsmod.databinding.SwitchCardBinding; 13 | 14 | /** 15 | * A card that contains a {@link android.widget.TextView} and a 16 | * {@link ProgrammaticMaterialSwitchView} on a single line. 17 | * The text will be rendered in a separate textview from the switch to prevent accidentally 18 | * clicking on the text from triggering the switch. 19 | */ 20 | public class SwitchCardView extends LinearLayout { 21 | final SwitchCardBinding mBinding; 22 | 23 | public SwitchCardView(Context context) { 24 | super(context); 25 | 26 | mBinding = SwitchCardBinding.inflate(LayoutInflater.from(context), this, true); 27 | } 28 | 29 | public SwitchCardView(Context context, @Nullable AttributeSet attrs) { 30 | super(context, attrs); 31 | 32 | mBinding = SwitchCardBinding.inflate(LayoutInflater.from(context), this, true); 33 | 34 | final TypedArray xmlAttrs = context.obtainStyledAttributes(attrs, R.styleable.SwitchCardView); 35 | final String text = xmlAttrs.getString(R.styleable.SwitchCardView_text); 36 | final boolean enabled = xmlAttrs.getBoolean(R.styleable.SwitchCardView_enabled, true); 37 | xmlAttrs.recycle(); 38 | 39 | mBinding.switchCardTextview.setText(text); 40 | mBinding.switchCardSwitch.setEnabled(enabled); 41 | } 42 | 43 | public ProgrammaticMaterialSwitchView getSwitch() { 44 | return mBinding.switchCardSwitch; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/util/OnItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.util; 2 | 3 | /** 4 | * A generic interface to handle clicks on recyclerview rows. 5 | */ 6 | public interface OnItemClickListener { 7 | void onItemClick(Object item); 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jacopomii/gappsmod/util/Utils.java: -------------------------------------------------------------------------------- 1 | package com.jacopomii.gappsmod.util; 2 | 3 | import static com.jacopomii.gappsmod.data.Constants.VENDING_ANDROID_PACKAGE_NAME; 4 | 5 | import android.content.ActivityNotFoundException; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.content.pm.ApplicationInfo; 10 | import android.content.pm.PackageManager; 11 | import android.net.Uri; 12 | import android.view.LayoutInflater; 13 | import android.view.ViewGroup; 14 | import android.view.WindowManager; 15 | import android.widget.CheckBox; 16 | 17 | import androidx.appcompat.app.AlertDialog; 18 | import androidx.appcompat.widget.SearchView; 19 | import androidx.recyclerview.widget.DividerItemDecoration; 20 | import androidx.recyclerview.widget.LinearLayoutManager; 21 | import androidx.recyclerview.widget.RecyclerView; 22 | 23 | import com.android.volley.Request; 24 | import com.android.volley.RequestQueue; 25 | import com.android.volley.toolbox.JsonObjectRequest; 26 | import com.android.volley.toolbox.RequestFuture; 27 | import com.android.volley.toolbox.Volley; 28 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 29 | import com.jacopomii.gappsmod.BuildConfig; 30 | import com.jacopomii.gappsmod.ICoreRootService; 31 | import com.jacopomii.gappsmod.R; 32 | import com.jacopomii.gappsmod.data.Version; 33 | import com.jacopomii.gappsmod.databinding.DialogSelectPackageBinding; 34 | import com.jacopomii.gappsmod.ui.adapter.SelectPackageRecyclerViewAdapter; 35 | import com.l4digital.fastscroll.FastScrollRecyclerView; 36 | 37 | import org.json.JSONObject; 38 | 39 | import java.io.IOException; 40 | import java.io.InputStream; 41 | import java.io.OutputStream; 42 | 43 | public class Utils { 44 | public static void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException { 45 | byte[] buffer = new byte[1024]; 46 | int read; 47 | while ((read = inputStream.read(buffer)) != -1) { 48 | outputStream.write(buffer, 0, read); 49 | } 50 | inputStream.close(); 51 | outputStream.flush(); 52 | outputStream.close(); 53 | } 54 | 55 | public static void openGooglePlay(Context context, String googlePlayLink) { 56 | try { 57 | Intent appStoreIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(googlePlayLink)); 58 | appStoreIntent.setPackage(VENDING_ANDROID_PACKAGE_NAME); 59 | context.startActivity(appStoreIntent); 60 | } catch (ActivityNotFoundException exception) { 61 | context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(googlePlayLink))); 62 | } 63 | } 64 | 65 | public static boolean checkUpdateAvailable(Context context) { 66 | RequestQueue requestQueue = Volley.newRequestQueue(context); 67 | RequestFuture future = RequestFuture.newFuture(); 68 | 69 | requestQueue.add( 70 | new JsonObjectRequest( 71 | Request.Method.GET, 72 | context.getString(R.string.github_api_link) + "/releases/latest", 73 | null, 74 | future, 75 | future 76 | ) 77 | ); 78 | 79 | try { 80 | JSONObject response = future.get(); 81 | Version actualVersion = new Version(BuildConfig.VERSION_NAME); 82 | Version fetchedVersion = new Version(response.getString("tag_name").substring(1)); 83 | if (actualVersion.compareTo(fetchedVersion) < 0) 84 | return true; 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /** 93 | * This method generates strings used for IN queries. 94 | * It creates string containing "?" characters repeated {@code size} times and separated by ",". 95 | * 96 | * @param size size of the items. 97 | * @return IN query string of the form ?,?,?,?. 98 | */ 99 | public static String createInQueryString(int size) { 100 | StringBuilder stringBuilder = new StringBuilder(); 101 | String separator = ""; 102 | for (int i = 0; i < size; i++) { 103 | stringBuilder.append(separator); 104 | stringBuilder.append("?"); 105 | separator = ","; 106 | } 107 | return stringBuilder.toString(); 108 | } 109 | 110 | /** 111 | * This method returns, given an {@code androidPackageName}, the label of the corresponding 112 | * application, or a localized string "Unknown" if the application is not installed. 113 | * 114 | * @param context context. 115 | * @param androidPackageName the Android package name of the application to get the label for. 116 | * @return the application label if the application exists; The localized string 117 | * {@link R.string#unknown} otherwise. 118 | */ 119 | public static String getApplicationLabelOrUnknown(Context context, String androidPackageName) { 120 | String applicationLabel = context.getString(R.string.unknown); 121 | 122 | try { 123 | PackageManager packageManager = context.getPackageManager(); 124 | ApplicationInfo applicationInfo = packageManager.getApplicationInfo(androidPackageName, 0); 125 | if (applicationInfo != null) 126 | applicationLabel = (String) (packageManager.getApplicationLabel(applicationInfo)); 127 | } catch (PackageManager.NameNotFoundException ignored) { 128 | } 129 | 130 | return applicationLabel; 131 | } 132 | 133 | // Static variables for showSelectPackageDialog() 134 | private static CharSequence lastPackageSearched = null; 135 | private static Boolean lastPackageSearchedRemember = true; 136 | 137 | /** 138 | * Show the "Select Package" dialog, a custom view to select package names contained in the 139 | * Phenotype DB with search and fastscroll features. 140 | * 141 | * @param context context. 142 | * @param coreRootServiceIpc a {@code ICoreRootService} instance. 143 | * @param onItemClickListener an implementation of the {@link OnItemClickListener} interface, 144 | * to perform actions after the user has selected a package. 145 | * The received item is a string containing the selected Phenotype 146 | * (not Android) package name. 147 | */ 148 | public static void showSelectPackageDialog(Context context, ICoreRootService coreRootServiceIpc, OnItemClickListener onItemClickListener, DialogInterface.OnDismissListener onDismissListener) { 149 | // Dialog builder 150 | MaterialAlertDialogBuilder selectPackageDialogBuilder = new MaterialAlertDialogBuilder(context); 151 | 152 | // Inflate dialog layout 153 | DialogSelectPackageBinding dialogSelectPackageBinding = DialogSelectPackageBinding.inflate(LayoutInflater.from(context)); 154 | selectPackageDialogBuilder.setView(dialogSelectPackageBinding.getRoot()); 155 | 156 | // Create dialog 157 | AlertDialog selectPackageDialog = selectPackageDialogBuilder.create(); 158 | 159 | // Set dialog custom height and width 160 | selectPackageDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 161 | selectPackageDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 162 | 163 | // Set dialog onDismissListener 164 | selectPackageDialog.setOnDismissListener(onDismissListener); 165 | 166 | // Dialog components 167 | SearchView selectPackageSearchView = dialogSelectPackageBinding.searchview; 168 | FastScrollRecyclerView selectPackageRecyclerView = dialogSelectPackageBinding.recyclerview; 169 | CheckBox SelectPackageRememberCheckbox = dialogSelectPackageBinding.remembercheckbox; 170 | 171 | // Initialize the dialog adapter 172 | SelectPackageRecyclerViewAdapter selectPackageRecyclerViewAdapter = new SelectPackageRecyclerViewAdapter(context, coreRootServiceIpc, item -> { 173 | // Pass the received item to the caller onItemClickListener 174 | onItemClickListener.onItemClick(item); 175 | 176 | // Dismiss dialog 177 | selectPackageDialog.dismiss(); 178 | }); 179 | 180 | // Disable fast scroll if the selectPackageRecyclerView is empty or changes to empty 181 | selectPackageRecyclerView.setFastScrollEnabled(selectPackageRecyclerViewAdapter.getItemCount() != 0); 182 | selectPackageRecyclerViewAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 183 | @Override 184 | public void onChanged() { 185 | super.onChanged(); 186 | selectPackageRecyclerView.setFastScrollEnabled(selectPackageRecyclerViewAdapter.getItemCount() != 0); 187 | } 188 | }); 189 | 190 | // Set the dialog selectPackageRecyclerView LayoutManager and Adapter 191 | selectPackageRecyclerView.setLayoutManager(new LinearLayoutManager(context)); 192 | selectPackageRecyclerView.setAdapter(selectPackageRecyclerViewAdapter); 193 | 194 | // Add list dividers to the selectPackageRecyclerView 195 | selectPackageRecyclerView.addItemDecoration(new DividerItemDecoration(selectPackageRecyclerView.getContext(), DividerItemDecoration.VERTICAL)); 196 | 197 | // Dialog filter 198 | selectPackageSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 199 | @Override 200 | public boolean onQueryTextSubmit(String query) { 201 | return false; 202 | } 203 | 204 | @Override 205 | public boolean onQueryTextChange(String newText) { 206 | lastPackageSearched = newText; 207 | selectPackageRecyclerViewAdapter.getFilter().filter(newText); 208 | return false; 209 | } 210 | }); 211 | 212 | // Remember last package searched 213 | SelectPackageRememberCheckbox.setChecked(lastPackageSearchedRemember); 214 | SelectPackageRememberCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { 215 | lastPackageSearchedRemember = isChecked; 216 | lastPackageSearched = selectPackageSearchView.getQuery(); 217 | }); 218 | if (lastPackageSearched != null && lastPackageSearchedRemember) 219 | selectPackageSearchView.setQuery(lastPackageSearched, true); 220 | 221 | // Show dialog 222 | selectPackageDialog.show(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /app/src/main/proto/call_screen_i18n.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.jacopomii.gappsmod.protos; 4 | 5 | option java_multiple_files = true; 6 | option java_package = "com.jacopomii.gappsmod.protos"; 7 | option java_outer_classname = "CallScreenI18nProtos"; 8 | 9 | /* 10 | Reversed from Dialer. Hex 0a140a026974120e0a0c0a0569742d495412030a0102 corresponds to the following JSON. 11 | { 12 | "countryConfigs":[{ 13 | "country": "it", 14 | "languageConfig": { 15 | "languages":[{ 16 | "languageCode":"it-IT", 17 | "a6":{ 18 | "a7":2 19 | } 20 | }] 21 | } 22 | }] 23 | } 24 | */ 25 | 26 | message Call_screen_i18n_config { 27 | message A6 { 28 | bytes a7 = 1; 29 | } 30 | 31 | message Language { 32 | string languageCode = 1; 33 | A6 a6 = 2; 34 | } 35 | 36 | message LanguageConfig { 37 | repeated Language languages = 1; 38 | } 39 | 40 | message CountryConfig { 41 | string country = 1; 42 | LanguageConfig languageConfig = 2; 43 | } 44 | 45 | repeated CountryConfig countryConfigs = 1; 46 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_up_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_beta_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fail_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_install_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_search_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav_drawer_boolean_mods_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav_drawer_delete_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav_drawer_information_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav_drawer_suggested_mods_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_success_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/activities/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 22 | 23 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/activities/layout/activity_splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 26 | 27 | 34 | 35 | 36 | 40 | 41 | 47 | 48 | 54 | 55 | 61 | 62 | 69 | 70 | 71 | 77 | 78 | 84 | 85 | 91 | 92 | 99 | 100 | 101 | 107 | 108 | 114 | 115 | 121 | 122 | 129 | 130 | 131 | 137 | 138 | 144 | 145 | 151 | 152 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/dialogs/layout/dialog_select_package.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 23 | 24 | 33 | 34 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/fragments/layout/fragment_boolean_mods.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 31 | 32 | 37 | 38 | 43 | 44 | 51 | 52 | 60 | 61 | 62 | 63 | 70 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 92 | 93 | 98 | 99 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 118 | 119 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/fragments/layout/fragment_information.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 23 | 24 | 33 | 34 | 41 | 42 | 51 | 52 | 59 | 60 | 68 | 69 | 77 | 78 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/res/layouts/fragments/layout/fragment_revert_mods.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 24 | 25 | 30 | 31 | 36 | 37 | 46 | 47 | 48 | 49 |