Repository: jacopotediosi/GoogleDialerMod Branch: main Commit: 6c63c56da89a Files: 82 Total size: 251.0 KB Directory structure: gitextract_6qvd41v1/ ├── .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 │ ├── navigation/ │ │ └── mobile_navigation.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── 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 ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: jacopotediosi custom: https://paypal.me/jacopotediosi ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug Report about: Report something that isn't working title: '' labels: bug assignees: '' --- ## Overview [NOTE]: # ( Give a BRIEF summary about your problem ) ## Steps to Reproduce [NOTE]: # ( Provide a simple set of steps to reproduce this bug. ) 1. 2. 3. ## Expected Behavior [NOTE]: # ( Tell us what you expected to happen ) ## Actual Behavior [NOTE]: # ( Tell us what actually happens ) ## Screenshots [NOTE]: # ( If applicable, add screenshots to help explain your problem. ) ## System Information - Device and model: - ROM and Android version: - Is the Google app you are trying to tweak (e.g., Phone by Google) installed as system app: yes/no - Installed Magisk / other SU Manager version: [NOTE]: # ( Paste below the output of the `adb shell "dumpsys package com.jacopomii.gappsmod | grep version"` command ) - Installed GAppsMod version: [NOTE]: # ( Paste below the output of the `adb shell "dumpsys package REPLACE_WITH_PACKAGENAME | grep version"` command ) - The version of the Google app you are trying to tweak (e.g., Phone by Google): [NOTE]: # ( Paste below the output of the `adb shell "getprop | grep locale"` command ) - Your device language (locale): [NOTE]: # ( Paste below the output of the `adb shell "getprop | grep iso-country"` command ) - Your location (country of the SIM and country where you are): ## Logcat [NOTE]: # ( 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. Open another terminal and use the `adb logcat > logs.txt` command to start capturing logs. Perform the necessary steps to replicate the bug, then press CTRL+C to stop capturing logs. Attach below the resulting logs.txt file. ) ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature Request about: Suggest an idea for a new feature you want title: '' labels: enhancement assignees: '' --- ## Summary [NOTE]: # ( Provide a brief overview of what the new feature is all about ) ## Solution [NOTE]: # ( A clear and concise description of what you want to happen ) ## Examples [NOTE]: # ( Show us a picture or mock-up of your proposal ) ## Alternatives [NOTE]: # ( A clear and concise description of any alternative solutions or features you've considered ) ## Context [NOTE]: # ( Why does this feature matter to you? What unique circumstances do you have? ) ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gradle" directory: "/" schedule: interval: "monthly" ================================================ FILE: .github/workflows/push.yml ================================================ name: Assemble on push on: push: branches: [ main ] paths-ignore: - '.github/**' - '**.md' workflow_dispatch: jobs: build: name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ ubuntu-latest ] steps: - name: Checkout uses: actions/checkout@v3 with: submodules: 'recursive' fetch-depth: 0 - name: Gradle wrapper validation uses: gradle/wrapper-validation-action@v1 - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' - name: Write keystore parameters run: | echo keystore.password='${{ secrets.KEYSTORE_PASSWORD }}' >> local.properties echo keystore.alias='${{ secrets.KEYSTORE_ALIAS }}' >> local.properties echo keystore.alias_password='${{ secrets.KEYSTORE_ALIAS_PASSWORD }}' >> local.properties echo keystore.path=`pwd`/keystore.jks >> local.properties echo "${{ secrets.KEYSTORE_KEY }}" | base64 --decode > keystore.jks - name: Gradle Dependency Submission uses: mikepenz/gradle-dependency-submission@v0.8.6 with: gradle-build-module: ":app" - name: Assemble uses: gradle/gradle-build-action@v2 with: arguments: assemble - name: Get short commit hash run: | echo "LATEST_COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Upload debug if: success() uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-${{ env.LATEST_COMMIT_HASH }}-debug.apk path: "app/build/outputs/apk/debug/app-debug.apk" - name: Upload release if: success() uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-${{ env.LATEST_COMMIT_HASH }}-release.apk path: "app/build/outputs/apk/release/app-release.apk" ================================================ FILE: .gitignore ================================================ *.iml .gradle .idea .DS_Store /build local.properties ================================================ FILE: README.md ================================================ # Deprecation notice ## !!! This project is no longer maintained !!! I had started researching how Phenotype DB works as a hobby, and this application was only meant to be a proof of concept. Making things work required a huge amount of reverse engineering of Google applications, and today I no longer have time to dedicate to it. 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. 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. # GAppsMod (ex GoogleDialerMod) The ultimate All-In-One Utility to tweak Google applications. ## Downloads: - Please visit the [GAppsMod Release Page](https://github.com/jacopotediosi/GAppsMod/releases) ## How do I use it? - 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 - 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 - There is no need to keep GAppsMod installed after applying the desired mods, because they (should) survive Google applications updates / reinstalls over time ## How does it work? 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. Some of those flags concern applications core functionalities, while others pertain to hidden or upcoming features that have not yet been released. 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. ## Features: - Supports all arm / arm64 / x86 / x86_64 devices and all Android versions from 5.0 (Lollipop) - Enable / disable hidden features for all users at once when Android "multiple users" mode is in use - Allows users to list and change all Phenotype DB boolean flags for all installed Google applications - A convenient home screen brings together the suggested mods for the most used Google applications ## Currently suggested mods - For the **Phone** application ([link](https://play.google.com/store/apps/details?id=com.google.android.dialer)): - Force **enable call recording** feature, even on unsupported devices or in unsupported countries ([ref](https://support.google.com/phoneapp/answer/9803950)) - Enable also **automatic call recording** ("always record") feature based on caller (otherwise only available in India) - Silence the annoying "registration has started / ended" **call recording announcements** (only on Phone version <= 94.x) - 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)) - Allows users to choose the language for call screening - For the **Messages** application ([link](https://play.google.com/store/apps/details?id=com.google.android.apps.messaging)): - Force **enable debug menu** (it can also be enabled without mods by entering `*xyzzy*` in the application's search field) - Force **enable message organization** ("supersort") - Force **enable marking conversations as unread** - Force **enable verified SMS** settings menu ([ref](https://support.google.com/messages/answer/9326240)) - Force **enable always sending images by Google Photos links in SMS** ([ref](https://9to5google.com/2022/02/19/messages-google-photos/)) - Force **enable nudges and birthday reminders** ([ref](https://support.google.com/messages/answer/11555591)) - Force **enable Bard AI draft suggestions** ("magic compose") ([ref](https://9to5google.com/2023/05/05/google-messages-magic-compose-ai/)) - 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** And much more coming soon :) ## Demo ![Demo GIF](https://github.com/jacopotediosi/GAppsMod/assets/20026524/5b13c935-4b12-46ac-b67d-0182004c8ac0) ## Troubleshooting: - 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. - Before to report an issue try to delete Google apps data, to reboot your phone and to try again what didn't work ## Donations 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. ## Credits: - 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 - [Libsu](https://github.com/topjohnwu/libsu) by [topjohnwu](https://github.com/topjohnwu) ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id "com.android.application" id "com.google.protobuf" id "com.likethesalad.stem" } def keyfile def keystorePSW def keystoreAlias def keystoreAliasPSW Properties properties = new Properties() properties.load(project.rootProject.file("local.properties").newDataInputStream()) def keystoreFilepath = properties.getProperty("keystore.path") if (keystoreFilepath) { keyfile = file(keystoreFilepath) keystorePSW = properties.getProperty("keystore.password") keystoreAlias = properties.getProperty("keystore.alias") keystoreAliasPSW = properties.getProperty("keystore.alias_password") } else { // Remember to config your keystore settings in local.properties or in the below lines keyfile = file("C:/keystore.jks") keystorePSW = "CHANGEME" keystoreAlias = "CHANGEME" keystoreAliasPSW = "CHANGEME" } android { namespace "com.jacopomii.gappsmod" compileSdk 33 defaultConfig { applicationId "com.jacopomii.gappsmod" minSdk 21 targetSdk 33 versionCode 400 versionName "4.00" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } signingConfigs { release { storeFile keyfile storePassword keystorePSW keyAlias keystoreAlias keyPassword keystoreAliasPSW } } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } } buildFeatures { viewBinding true aidl true buildConfig true } sourceSets { main { res.srcDirs = ["src/main/res", "src/main/res/layouts/activities", "src/main/res/layouts/dialogs", "src/main/res/layouts/fragments", "src/main/res/layouts/items"] } } } dependencies { // Libsu def libsuVersion = "5.0.5" implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}" // Official SQLite Java Bindings, downloaded from https://www.sqlite.org/download.html implementation files("libs/sqlite-android-3410200.aar") // Advanced toast implementation "com.github.GrenderG:Toasty:1.5.2" // HTTP implementation "com.android.volley:volley:1.2.1" // Protobuf implementation "com.google.protobuf:protobuf-javalite:3.23.1" // Apache Commons //noinspection GradleDependency implementation "commons-io:commons-io:2.12.0" implementation "org.apache.commons:commons-lang3:3.12.0" // Navigation drawer def navigationVersion = "2.5.3" implementation "androidx.navigation:navigation-fragment:${navigationVersion}" implementation "androidx.navigation:navigation-ui:${navigationVersion}" // FastScroller for RecyclerView implementation "io.github.l4digital:fastscroll:2.1.0" // Other UI Components implementation "com.google.android.material:material:1.9.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.constraintlayout:constraintlayout:2.1.4" } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.23.1" } generateProtoTasks { all().each { task -> task.builtins { java { option "lite" } } } } } ================================================ FILE: app/lint.xml ================================================ ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile # Keep protobuf autogenerated classes -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } # Keep SQLite Java Bindings classes -keep class org.sqlite.** { *; } -keep class org.sqlite.database.** { *; } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/aidl/com/jacopomii/gappsmod/ICoreRootService.aidl ================================================ package com.jacopomii.gappsmod; interface ICoreRootService { IBinder getFileSystemService(); /** * Query the GMS and Vending Phenotype DBs to get a list of all package names that have at least * one Flag set. * * @return a {@code HashMap} in the format "Phenotype package name" => "Android package name". */ Map phenotypeDBGetAllPackageNames(); /** * Query the GMS and Vending Phenotype DBs to get a list of all package names that have at least * one Flag overridden. * * @return a {@code HashMap} in the format "Phenotype package name" => "Android package name". */ Map phenotypeDBGetAllOverriddenPackageNames(); /** * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}) to get the * Android package name corresponding to a given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype package name for which the corresponding Android * package name is to be returned. * @return the Android package name corresponding to the specified {@code phenotypePackageName}. * An empty string if the phenotypePackageName couldn't be found. */ String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(in String phenotypePackageName); /** * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}), selecting all * the boolean flags for a given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name whose flags are to be * returned. * @return a {@code HashMap} that uses the ({@code String}) flag name as the key.
* For performance reasons, the value of this HashMap is a {@code List} structured as follows:
* - Position 0 contains the {@code Boolean} value of the flag, giving priority to the value * overridden in the FlagOverrides table, if present, over the one contained in the Flags table.
* - Position 1 contains the {@code Boolean} value "changed", which is {@code true} if and only * if the returned flag is overwritten in the FlagOverrides table and has a different value * than the one contained in the Flags table; {@code false} otherwise. */ Map phenotypeDBGetBooleanFlagsOrOverridden(in String phenotypePackageName); /** * Query the GMS/Vending Phenotype DB (based on the {@code phenotypePackageName}) to find out if * all given {@code flags} are overridden for a given {@code phenotypePackageName}. * Please note that the fact that a flag is overridden only implies that it is present in the * FlagOverrides table, and not that its value is different from what is indicated in the Flags * table. * * @param phenotypePackageName the Phenotype (not Android) package name for which to check. * @param flags the flags to check. * @return {@code true} if all given {@code flags} are overridden for the given * {@code phenotypePackageName}; {@code false} otherwise. */ boolean phenotypeDBAreAllFlagsOverridden(in String phenotypePackageName, in List flags); /** * Remove all flag overrides from the GMS and Vending Phenotype DBs by truncating the * FlagOverrides table. * It also clears from the filesystem the Phenotype cache of all applications for which at least * one flag was overridden. */ void phenotypeDBDeleteAllFlagOverrides(); /** * Delete all flag overrides from the GMS/Vending Phenotype DB (based on the * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. * It also clears from the filesystem the Phenotype cache of the application corresponding to * the given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to delete the * flag overrides. */ void phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(in String phenotypePackageName); /** * Delete a given list of flag overrides from the GMS/Vending Phenotype DB (based on the * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. * It also clears from the filesystem the Phenotype cache of the application corresponding to * the given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to delete the * flag overrides. * @param flags the list of flags to delete. */ void phenotypeDBDeleteFlagOverrides(in String phenotypePackageName, in List flags); /** * Override the value of a boolean flag in the GMS/Vending Phenotype DB (based on the * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. * It also clears from the filesystem the Phenotype cache of the application corresponding to * the given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to override * the flag. * @param flag the name of the flag to override. * @param value the value to override the flag with. */ void phenotypeDBOverrideBooleanFlag(in String phenotypePackageName, in String flag, in boolean value); /** * Override the value of an extension flag in the GMS/Vending Phenotype DB (based on the * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. * It also clears from the filesystem the Phenotype cache of the application corresponding to * the given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to override * the flag. * @param flag the name of the flag to override. * @param value the value to override the flag with. */ void phenotypeDBOverrideExtensionFlag(in String phenotypePackageName, in String flag, in byte[] value); /** * Override the value of a string flag in the GMS/Vending Phenotype DB (based on the * {@code phenotypePackageName}) for a given {@code phenotypePackageName}. * It also clears from the filesystem the Phenotype cache of the application corresponding to * the given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to override * the flag. * @param flag the name of the flag to override. * @param value the value to override the flag with. */ void phenotypeDBOverrideStringFlag(in String phenotypePackageName, in String flag, in String value); } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/application/GAppsModApplication.java ================================================ package com.jacopomii.gappsmod.application; import android.app.Application; import com.google.android.material.color.DynamicColors; public class GAppsModApplication extends Application { @Override public void onCreate() { super.onCreate(); DynamicColors.applyToActivitiesIfAvailable(this); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/data/BooleanFlag.java ================================================ package com.jacopomii.gappsmod.data; public class BooleanFlag { private final String mFlagName; private boolean mFlagValue; private boolean mFlagOverriddenAndChanged; public BooleanFlag(String flagName, boolean flagValue, boolean flagOverriddenAndChanged) { mFlagName = flagName; mFlagValue = flagValue; mFlagOverriddenAndChanged = flagOverriddenAndChanged; } public String getFlagName() { return mFlagName; } public boolean getFlagValue() { return mFlagValue; } public void setFlagValue(boolean flagValue) { mFlagValue = flagValue; } public void setFlagOverriddenAndChanged(boolean flagOverriddenAndChanged) { mFlagOverriddenAndChanged = flagOverriddenAndChanged; } public boolean getFlagOverriddenAndChanged() { return mFlagOverriddenAndChanged; } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/data/Constants.java ================================================ package com.jacopomii.gappsmod.data; import android.annotation.SuppressLint; @SuppressLint("SdCardPath") public interface Constants { // Android package names String GMS_ANDROID_PACKAGE_NAME = "com.google.android.gms"; String VENDING_ANDROID_PACKAGE_NAME = "com.android.vending"; String DIALER_ANDROID_PACKAGE_NAME = "com.google.android.dialer"; String MESSAGES_ANDROID_PACKAGE_NAME = "com.google.android.apps.messaging"; // Phenotype package names String DIALER_PHENOTYPE_PACKAGE_NAME = "com.google.android.dialer"; String MESSAGES_PHENOTYPE_PACKAGE_NAME = "com.google.android.apps.messaging#com.google.android.apps.messaging"; // Google Play links String GOOGLE_PLAY_DETAILS_LINK = "https://play.google.com/store/apps/details?id="; String GOOGLE_PLAY_BETA_LINK = "https://play.google.com/apps/testing/"; // Data / data folders String DATA_DATA_PREFIX = "/data/data/"; String DIALER_CALLRECORDINGPROMPT = DATA_DATA_PREFIX + DIALER_ANDROID_PACKAGE_NAME + "/files/callrecordingprompt"; String GMS_PHENOTYPE_DB = DATA_DATA_PREFIX + GMS_ANDROID_PACKAGE_NAME + "/databases/phenotype.db"; String VENDING_PHENOTYPE_DB = DATA_DATA_PREFIX + VENDING_ANDROID_PACKAGE_NAME + "/databases/phenotype.db"; } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/data/PhenotypeDBPackageName.java ================================================ package com.jacopomii.gappsmod.data; public class PhenotypeDBPackageName { private final String mPhenotypePackageName; private final String mAndroidPackageName; public PhenotypeDBPackageName(String phenotypePackageName, String androidPackageName) { mPhenotypePackageName = phenotypePackageName; mAndroidPackageName = androidPackageName; } public String getPhenotypePackageName() { return mPhenotypePackageName; } public String getAndroidPackageName() { return mAndroidPackageName; } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/data/Version.java ================================================ package com.jacopomii.gappsmod.data; public class Version implements Comparable { private final String mVersion; public Version(String version) { if (version == null) throw new IllegalArgumentException("Version can not be null"); if (!version.matches("\\d+(\\.\\d+)*")) throw new IllegalArgumentException("Invalid version format"); mVersion = version; } public final String getVersion() { return mVersion; } @Override public int compareTo(Version that) { if(that == null) return 1; String[] thisParts = this.getVersion().split("\\."); String[] thatParts = that.getVersion().split("\\."); int length = Math.max(thisParts.length, thatParts.length); for(int i = 0; i < length; i++) { int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0; int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0; if(thisPart < thatPart) return -1; if(thisPart > thatPart) return 1; } return 0; } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/service/CoreRootService.java ================================================ package com.jacopomii.gappsmod.service; import static com.jacopomii.gappsmod.data.Constants.DATA_DATA_PREFIX; import static com.jacopomii.gappsmod.data.Constants.GMS_PHENOTYPE_DB; import static com.jacopomii.gappsmod.data.Constants.VENDING_ANDROID_PACKAGE_NAME; import static com.jacopomii.gappsmod.data.Constants.VENDING_PHENOTYPE_DB; import static com.jacopomii.gappsmod.util.Utils.createInQueryString; import static org.sqlite.database.sqlite.SQLiteDatabase.OPEN_READWRITE; import static org.sqlite.database.sqlite.SQLiteDatabase.openDatabase; import android.content.ContentValues; import android.content.Intent; import android.database.Cursor; import android.os.IBinder; import android.os.Process; import androidx.annotation.NonNull; import com.jacopomii.gappsmod.ICoreRootService; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.ipc.RootService; import com.topjohnwu.superuser.nio.ExtendedFile; import com.topjohnwu.superuser.nio.FileSystemManager; import org.apache.commons.io.FileUtils; import org.sqlite.database.sqlite.SQLiteDatabase; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @SuppressWarnings("SameParameterValue") public class CoreRootService extends RootService { static { // Only load the library when this class is loaded in a root process. // The classloader will load this class (and call this static block) in the non-root process because we accessed it when constructing the Intent to send. // Add this check so we don't unnecessarily load native code that'll never be used. if (Process.myUid() == 0) System.loadLibrary("sqliteX"); } private SQLiteDatabase GMSPhenotypeDB = null; private SQLiteDatabase VendingPhenotypeDB = null; @Override public IBinder onBind(@NonNull Intent intent) { try { GMSPhenotypeDB = openDatabase(GMS_PHENOTYPE_DB, null, OPEN_READWRITE); } catch (Exception e) { e.printStackTrace(); } try { VendingPhenotypeDB = openDatabase(VENDING_PHENOTYPE_DB, null, OPEN_READWRITE); } catch (Exception e) { e.printStackTrace(); } return new CoreRootServiceIPC(); } @Override public boolean onUnbind(@NonNull Intent intent) { if (GMSPhenotypeDB != null && GMSPhenotypeDB.isOpen()) GMSPhenotypeDB.close(); if (VendingPhenotypeDB != null && VendingPhenotypeDB.isOpen()) VendingPhenotypeDB.close(); super.onUnbind(intent); return false; } @Override public void onDestroy() { if (GMSPhenotypeDB != null && GMSPhenotypeDB.isOpen()) GMSPhenotypeDB.close(); if (VendingPhenotypeDB != null && VendingPhenotypeDB.isOpen()) VendingPhenotypeDB.close(); super.onDestroy(); } private class CoreRootServiceIPC extends ICoreRootService.Stub { @Override public IBinder getFileSystemService() { return FileSystemManager.getService(); } @Override public Map phenotypeDBGetAllPackageNames() { return CoreRootService.this.phenotypeDBGetAllPackageNames(); } @Override public Map phenotypeDBGetAllOverriddenPackageNames() { return CoreRootService.this.phenotypeDBGetAllOverriddenPackageNames(); } @Override public String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(String phenotypePackageName) { return CoreRootService.this.phenotypeDBGetAndroidPackageNameByPhenotypePackageName(phenotypePackageName); } @Override public Map> phenotypeDBGetBooleanFlagsOrOverridden(String phenotypePackageName) { return CoreRootService.this.phenotypeDBGetBooleanFlagsOrOverridden(phenotypePackageName); } @Override public boolean phenotypeDBAreAllFlagsOverridden(String phenotypePackageName, List flags) { return CoreRootService.this.phenotypeDBAreAllFlagsOverridden(phenotypePackageName, flags); } @Override public void phenotypeDBDeleteAllFlagOverrides() { CoreRootService.this.phenotypeDBDeleteAllFlagOverrides(true); } @Override public void phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(String phenotypePackageName) { CoreRootService.this.phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(phenotypePackageName, true); } @Override public void phenotypeDBDeleteFlagOverrides(String phenotypePackageName, List flags) { CoreRootService.this.phenotypeDBDeleteFlagOverrides(phenotypePackageName, flags, true); } @Override public void phenotypeDBOverrideBooleanFlag(String phenotypePackageName, String flag, boolean value) { CoreRootService.this.phenotypeDBOverrideBooleanFlag(phenotypePackageName, flag, value, true); } @Override public void phenotypeDBOverrideExtensionFlag(String phenotypePackageName, String flag, byte[] value) { CoreRootService.this.phenotypeDBOverrideExtensionFlag(phenotypePackageName, flag, value, true); } @Override public void phenotypeDBOverrideStringFlag(String phenotypePackageName, String flag, String value) { CoreRootService.this.phenotypeDBOverrideStringFlag(phenotypePackageName, flag, value, true); } } private Map phenotypeDBGetAllPackageNames() { HashMap map = new HashMap<>(); String sql = "SELECT Flags.packageName as phenotypePackageName, Packages.androidPackageName " + "FROM Flags, Packages " + "WHERE phenotypePackageName=Packages.packageName " + "GROUP BY phenotypePackageName " + "ORDER BY phenotypePackageName ASC"; try { try (Cursor cursor = GMSPhenotypeDB.rawQuery(sql, null)) { while (cursor.moveToNext()) map.put(cursor.getString(0), cursor.getString(1)); } } catch (Exception e) { e.printStackTrace(); } try { try (Cursor cursor = VendingPhenotypeDB.rawQuery(sql, null)) { while (cursor.moveToNext()) map.put(cursor.getString(0), cursor.getString(1)); } } catch (Exception e) { e.printStackTrace(); } return map; } private Map phenotypeDBGetAllOverriddenPackageNames() { HashMap map = new HashMap<>(); String sql = "SELECT FlagOverrides.packageName as phenotypePackageName, Packages.androidPackageName " + "FROM FlagOverrides, Packages " + "WHERE phenotypePackageName=Packages.packageName " + "GROUP BY phenotypePackageName " + "ORDER BY phenotypePackageName ASC"; try { try (Cursor cursor = GMSPhenotypeDB.rawQuery(sql, null)) { while (cursor.moveToNext()) map.put(cursor.getString(0), cursor.getString(1)); } } catch (Exception e) { e.printStackTrace(); } try { try (Cursor cursor = VendingPhenotypeDB.rawQuery(sql, null)) { while (cursor.moveToNext()) map.put(cursor.getString(0), cursor.getString(1)); } } catch (Exception e) { e.printStackTrace(); } return map; } private String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(String phenotypePackageName) { SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT androidPackageName FROM Packages WHERE packageName=? LIMIT 1"; String[] selectionArgs = {phenotypePackageName}; try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs)) { if (cursor.moveToFirst()) return cursor.getString(0); } } catch (Exception e) { e.printStackTrace(); } return ""; } private Map> phenotypeDBGetBooleanFlagsOrOverridden(String phenotypePackageName) { Map> map = new HashMap<>(); SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT DISTINCT f.name AS name, COALESCE(fo.boolVal, f.boolVal) AS boolVal, CASE WHEN fo.boolVal != f.boolVal THEN 1 ELSE 0 END AS changed " + "FROM Flags f " + "LEFT JOIN FlagOverrides fo ON f.packageName = fo.packageName AND f.name = fo.name " + "WHERE f.packageName = ? AND f.user = '' AND (f.boolVal IS NOT NULL OR fo.boolVal IS NOT NULL)" + "UNION ALL " + "SELECT DISTINCT fo.name AS name, fo.boolVal, 1 AS changed " + "FROM FlagOverrides fo " + "LEFT JOIN Flags f ON f.packageName = fo.packageName AND f.name = fo.name " + "WHERE fo.packageName = ? AND f.name IS NULL AND fo.user = '' AND fo.boolVal IS NOT NULL"; String[] selectionArgs = {phenotypePackageName, phenotypePackageName}; try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs)) { while (cursor.moveToNext()) { String name = cursor.getString(0); Boolean boolVal = cursor.getInt(1) != 0; Boolean changed = cursor.getInt(2) != 0; map.put(name, Arrays.asList(boolVal, changed)); } } } catch (Exception e) { e.printStackTrace(); } return map; } private boolean phenotypeDBAreAllFlagsOverridden(String phenotypePackageName, List flags) { boolean areAllFlagsOverridden = false; SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT DISTINCT name FROM FlagOverrides WHERE packageName=? AND name IN (" + createInQueryString(flags.size()) + ")"; List selectionArgs = new ArrayList<>(); selectionArgs.add(phenotypePackageName); selectionArgs.addAll(flags); try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs.toArray(new String[0]))) { areAllFlagsOverridden = cursor.getCount() == flags.size(); } } catch (Exception e) { e.printStackTrace(); } return areAllFlagsOverridden; } private void phenotypeDBDeleteAllFlagOverrides(boolean deletePackagePhenotypeCache) { Set overriddenPhenotypePackageNames = phenotypeDBGetAllOverriddenPackageNames().keySet(); try { GMSPhenotypeDB.delete("FlagOverrides", null, null); } catch (Exception e) { e.printStackTrace(); } try { VendingPhenotypeDB.delete("FlagOverrides", null, null); } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) { for (String phenotypePackageName : overriddenPhenotypePackageNames) { killPackageAndDeletePhenotypeCache(phenotypePackageName); } } } private void phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(String phenotypePackageName, boolean deletePackagePhenotypeCache) { SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); try { phenotypeDB.delete("FlagOverrides", "packageName=?", new String[]{phenotypePackageName}); } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) killPackageAndDeletePhenotypeCache(phenotypePackageName); } private void phenotypeDBDeleteFlagOverrides(String phenotypePackageName, List flags, boolean deletePackagePhenotypeCache) { SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String whereClause = "packageName=? AND name IN (" + createInQueryString(flags.size()) + ")"; List whereArgs = new ArrayList<>(); whereArgs.add(phenotypePackageName); whereArgs.addAll(flags); try { phenotypeDB.delete("FlagOverrides", whereClause, whereArgs.toArray(new String[0])); } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) killPackageAndDeletePhenotypeCache(phenotypePackageName); } private void phenotypeDBOverrideBooleanFlag(String phenotypePackageName, String flag, boolean value, boolean deletePackagePhenotypeCache) { phenotypeDBDeleteFlagOverrides(phenotypePackageName, Collections.singletonList(flag), false); SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT DISTINCT user FROM Flags WHERE packageName = ?"; String[] selectionArgs = {phenotypePackageName}; try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs)) { while (cursor.moveToNext()) { ContentValues contentValues = new ContentValues(); contentValues.put("packageName", phenotypePackageName); contentValues.put("flagType", 0); contentValues.put("name", flag); contentValues.put("user", cursor.getString(0)); contentValues.put("boolVal", (value ? "1" : "0")); contentValues.put("committed", 0); phenotypeDB.insertWithOnConflict("FlagOverrides", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); } } } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) killPackageAndDeletePhenotypeCache(phenotypePackageName); } private void phenotypeDBOverrideExtensionFlag(String phenotypePackageName, String flag, byte[] value, boolean deletePackagePhenotypeCache) { phenotypeDBDeleteFlagOverrides(phenotypePackageName, Collections.singletonList(flag), false); SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT DISTINCT user FROM Flags WHERE packageName = ?"; String[] selectionArgs = {phenotypePackageName}; try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs)) { while (cursor.moveToNext()) { ContentValues contentValues = new ContentValues(); contentValues.put("packageName", phenotypePackageName); contentValues.put("flagType", 0); contentValues.put("name", flag); contentValues.put("user", cursor.getString(0)); contentValues.put("extensionVal", value); contentValues.put("committed", 0); phenotypeDB.insertWithOnConflict("FlagOverrides", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); } } } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) killPackageAndDeletePhenotypeCache(phenotypePackageName); } private void phenotypeDBOverrideStringFlag(String phenotypePackageName, String flag, String value, boolean deletePackagePhenotypeCache) { phenotypeDBDeleteFlagOverrides(phenotypePackageName, Collections.singletonList(flag), false); SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName); String sql = "SELECT DISTINCT user FROM Flags WHERE packageName = ?"; String[] selectionArgs = {phenotypePackageName}; try { try (Cursor cursor = phenotypeDB.rawQuery(sql, selectionArgs)) { while (cursor.moveToNext()) { ContentValues contentValues = new ContentValues(); contentValues.put("packageName", phenotypePackageName); contentValues.put("flagType", 0); contentValues.put("name", flag); contentValues.put("user", cursor.getString(0)); contentValues.put("stringVal", value); contentValues.put("committed", 0); phenotypeDB.insertWithOnConflict("FlagOverrides", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); } } } catch (Exception e) { e.printStackTrace(); } if (deletePackagePhenotypeCache) killPackageAndDeletePhenotypeCache(phenotypePackageName); } /** * Get the correct database (GMS or Vending) based on the {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name for which to get the * Phenotype DB. * @return a {@code SQLiteDatabase} reference to the GMS or Vending Phenotype DB. * The returned object may be {@code null} if there was an error opening the database. */ private SQLiteDatabase getPhenotypeDBByPhenotypePackageName(String phenotypePackageName) { if (phenotypePackageName.equals("com.google.android.finsky.regular") || phenotypePackageName.equals("com.google.android.finsky.stable")) { return VendingPhenotypeDB; } else { return GMSPhenotypeDB; } } /** * Kill and delete the Phenotype cache files of the Android application corresponding to the * given {@code phenotypePackageName}. * * @param phenotypePackageName the Phenotype (not Android) package name to kill and to delete * the Phenotype cache files for. */ private void killPackageAndDeletePhenotypeCache(String phenotypePackageName) { String androidPackageName = phenotypeDBGetAndroidPackageNameByPhenotypePackageName(phenotypePackageName); // Kill the android application corresponding to the phenotypePackageName Shell.cmd("am kill all " + androidPackageName).exec(); // Delete application phenotype Cache ExtendedFile phenotypeCache = FileSystemManager.getLocal().getFile(DATA_DATA_PREFIX + androidPackageName + "/files/phenotype"); if (phenotypeCache.exists()) { try { FileUtils.deleteDirectory(phenotypeCache); } catch (IOException | IllegalArgumentException e) { e.printStackTrace(); } } // If the application is Vending, additional cache files need to be deleted if (androidPackageName.equals(VENDING_ANDROID_PACKAGE_NAME)) { ExtendedFile[] VendingFiles = FileSystemManager.getLocal().getFile(DATA_DATA_PREFIX + androidPackageName + "/files").listFiles(); if (VendingFiles != null) { for (ExtendedFile file : VendingFiles) { if (file.getName().startsWith("experiment-flags")) { //noinspection ResultOfMethodCallIgnored file.delete(); } } } } } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/activity/MainActivity.java ================================================ package com.jacopomii.gappsmod.ui.activity; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.navigation.NavController; import androidx.navigation.Navigation; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.ActivityMainBinding; import com.jacopomii.gappsmod.service.CoreRootService; import com.topjohnwu.superuser.ipc.RootService; import com.topjohnwu.superuser.nio.FileSystemManager; public class MainActivity extends AppCompatActivity { private AppBarConfiguration mAppBarConfiguration; private ActivityMainBinding mBinding; private boolean mCoreRootServiceBound = false; private ServiceConnection mCoreRootServiceConnection; private ICoreRootService mCoreRootServiceIpc; private FileSystemManager mCoreRootServiceFSManager; @Override protected void onCreate(Bundle savedInstanceState) { // The savedInstanceState must not be used, otherwise the views (and the fragments contained // by this activity) are restored before the RootService is started, causing NPE. super.onCreate(null); // Enable edge-to-edge: allows drawing under system bars, preventing Android from // automatically applying the fitSystemWindows property to the root view. WindowCompat.setDecorFitsSystemWindows(getWindow(), false); // Start CoreRootService connection Intent intent = new Intent(this, CoreRootService.class); mCoreRootServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { try { // Set references to the remote coreRootService mCoreRootServiceBound = true; mCoreRootServiceIpc = ICoreRootService.Stub.asInterface(service); mCoreRootServiceFSManager = FileSystemManager.getRemote(mCoreRootServiceIpc.getFileSystemService()); // Inflate the activity layout and set the content view mBinding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(mBinding.getRoot()); // Set the toolbar setSupportActionBar(mBinding.toolbar); // Set the drawer DrawerLayout drawer = mBinding.drawerLayout; mAppBarConfiguration = new AppBarConfiguration.Builder( R.id.nav_suggested_mods, R.id.nav_boolean_mods, R.id.nav_revert_mods, R.id.nav_information ).setOpenableLayout(drawer).build(); // Pass through the window insets to the navHostFragment child views, except the top system bar ViewCompat.setOnApplyWindowInsetsListener(mBinding.navHostFragment, (view, insets) -> { WindowInsetsCompat insetsCompat = new WindowInsetsCompat.Builder(insets) .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of( insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).left, 0, insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).right, insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime()).bottom)) .build(); return ViewCompat.onApplyWindowInsets(view, insetsCompat); }); // Set the navigation controller NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(mBinding.navHostFragment.getId()); if (navHostFragment != null) { NavController navController = navHostFragment.getNavController(); NavigationUI.setupActionBarWithNavController(MainActivity.this, navController, mAppBarConfiguration); NavigationUI.setupWithNavController(mBinding.navView, navController); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { mCoreRootServiceBound = false; mCoreRootServiceIpc = null; mCoreRootServiceFSManager = null; } }; RootService.bind(intent, mCoreRootServiceConnection); } public FileSystemManager getCoreRootServiceFSManager() { return mCoreRootServiceFSManager; } public ICoreRootService getCoreRootServiceIpc() { return mCoreRootServiceIpc; } @Override public boolean onSupportNavigateUp() { NavController navController = Navigation.findNavController(this, mBinding.navHostFragment.getId()); return NavigationUI.navigateUp(navController, mAppBarConfiguration) || super.onSupportNavigateUp(); } @Override public void onBackPressed() { if (mBinding.drawerLayout.isOpen()) mBinding.drawerLayout.close(); else finishAffinity(); } @Override protected void onDestroy() { if (mCoreRootServiceBound) RootService.unbind(mCoreRootServiceConnection); super.onDestroy(); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/activity/SplashScreenActivity.java ================================================ package com.jacopomii.gappsmod.ui.activity; import static com.jacopomii.gappsmod.data.Constants.GMS_ANDROID_PACKAGE_NAME; import static com.jacopomii.gappsmod.data.Constants.GOOGLE_PLAY_DETAILS_LINK; import static com.jacopomii.gappsmod.data.Constants.GMS_PHENOTYPE_DB; import static com.jacopomii.gappsmod.util.Utils.checkUpdateAvailable; import static com.jacopomii.gappsmod.util.Utils.openGooglePlay; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.view.View; import android.widget.ImageView; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.CircularProgressIndicator; import com.jacopomii.gappsmod.BuildConfig; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.ActivitySplashScreenBinding; import com.jacopomii.gappsmod.service.CoreRootService; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.ipc.RootService; import com.topjohnwu.superuser.nio.FileSystemManager; import java.util.concurrent.CountDownLatch; @SuppressLint("CustomSplashScreen") public class SplashScreenActivity extends AppCompatActivity { static { // Set Libsu settings before creating the main shell Shell.enableVerboseLogging = BuildConfig.DEBUG; Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); } private ActivitySplashScreenBinding mBinding; private final CountDownLatch mRootCheckPassed = new CountDownLatch(1); private final CountDownLatch mCoreRootServiceConnected = new CountDownLatch(1); private final CountDownLatch mGMSPhenotypeCheckPassed = new CountDownLatch(1); private final CountDownLatch mUpdateCheckFinished = new CountDownLatch(1); private boolean mCoreRootServiceBound = false; private ServiceConnection mCoreRootServiceConnection; private FileSystemManager mCoreRootServiceFSManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Inflate the activity layout and set the content view mBinding = ActivitySplashScreenBinding.inflate(getLayoutInflater()); setContentView(R.layout.activity_splash_screen); // Start CoreRootService connection Intent intent = new Intent(this, CoreRootService.class); mCoreRootServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // Set references to the remote coreRootService mCoreRootServiceBound = true; ICoreRootService ipc = ICoreRootService.Stub.asInterface(service); try { mCoreRootServiceFSManager = FileSystemManager.getRemote(ipc.getFileSystemService()); mCoreRootServiceConnected.countDown(); } catch (RemoteException e) { e.printStackTrace(); } // Update the UI setCheckUIDone(mBinding.circularRootService.getId(), mBinding.doneRootService.getId(), mCoreRootServiceConnected.getCount() == 0); } @Override public void onServiceDisconnected(ComponentName name) { mCoreRootServiceBound = false; mCoreRootServiceFSManager = null; } }; RootService.bind(intent, mCoreRootServiceConnection); // Root permission check new Thread() { @Override public void run() { // Check root if (checkRoot()) { mRootCheckPassed.countDown(); } else { runOnUiThread(() -> new MaterialAlertDialogBuilder(SplashScreenActivity.this) .setCancelable(false) .setMessage(R.string.root_access_denied) .setPositiveButton(R.string.exit, (dialog, i) -> System.exit(0)) .show()); } // Update the UI setCheckUIDone(mBinding.circularRoot.getId(), mBinding.doneRoot.getId(), mRootCheckPassed.getCount() == 0); } }.start(); // GMS Phenotype DB check new Thread() { @Override public void run() { try { // Wait for root check to pass mRootCheckPassed.await(); // Wait for coreRootService to connect mCoreRootServiceConnected.await(); // Check the GMS Phenotype DB if (checkGMSPhenotypeDB()) { mGMSPhenotypeCheckPassed.countDown(); } else { runOnUiThread(() -> new MaterialAlertDialogBuilder(SplashScreenActivity.this) .setCancelable(false) .setMessage(getString(R.string.phenotype_db_does_not_exist_gms)) .setPositiveButton(R.string.install, (dialogInterface, i) -> openGooglePlay(SplashScreenActivity.this, GOOGLE_PLAY_DETAILS_LINK + GMS_ANDROID_PACKAGE_NAME)) .setNegativeButton(R.string.exit, (dialog, which) -> System.exit(0)) .setNeutralButton(R.string.continue_anyway, (dialogInterface, i) -> mGMSPhenotypeCheckPassed.countDown()) .show()); } // Update the UI setCheckUIDone(mBinding.circularPhenotypeGms.getId(), mBinding.donePhenotypeGms.getId(), mGMSPhenotypeCheckPassed.getCount() == 0); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); // Update available check new Thread() { @Override public void run() { // Check if updates are available if (!checkUpdateAvailable(SplashScreenActivity.this)) { mUpdateCheckFinished.countDown(); } else { runOnUiThread(() -> new MaterialAlertDialogBuilder(SplashScreenActivity.this) .setCancelable(false) .setMessage(R.string.new_version_alert) .setPositiveButton( R.string.github, (dialogInterface, i) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github_link) + "/releases"))) ) .setNegativeButton(R.string.continue_anyway, (dialogInterface, i) -> mUpdateCheckFinished.countDown()) .show()); } // Update the UI setCheckUIDone(mBinding.circularUpdates.getId(), mBinding.doneUpdates.getId(), mUpdateCheckFinished.getCount() == 0); } }.start(); // End splash screen and go to the main activity new Thread() { @Override public void run() { try { // Wait for all checks to pass and for all operations to finish mRootCheckPassed.await(); mCoreRootServiceConnected.await(); mGMSPhenotypeCheckPassed.await(); mUpdateCheckFinished.await(); // This is just for aesthetics: I don't want the splashscreen to be too fast Thread.sleep(1000); // Start the main activity Intent intent = new Intent(SplashScreenActivity.this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); } private boolean checkRoot() { return Shell.getShell().isRoot(); } private boolean checkGMSPhenotypeDB() { return mCoreRootServiceFSManager.getFile(GMS_PHENOTYPE_DB).exists(); } private void setCheckUIDone(int circularID, int doneImageID, boolean success) { CircularProgressIndicator circular = findViewById(circularID); ImageView doneImage = findViewById(doneImageID); runOnUiThread(() -> { circular.setVisibility(View.GONE); doneImage.setImageResource(success ? R.drawable.ic_success_24 : R.drawable.ic_fail_24); doneImage.setVisibility(View.VISIBLE); }); } @Override protected void onDestroy() { if (mCoreRootServiceBound) RootService.unbind(mCoreRootServiceConnection); super.onDestroy(); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/adapter/BooleanModsRecyclerViewAdapter.java ================================================ package com.jacopomii.gappsmod.ui.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.os.RemoteException; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.card.MaterialCardView; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.data.BooleanFlag; import com.jacopomii.gappsmod.databinding.SwitchCardBinding; import com.jacopomii.gappsmod.ui.view.ProgrammaticMaterialSwitchView; import com.l4digital.fastscroll.FastScroller; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; @SuppressWarnings("unchecked") public class BooleanModsRecyclerViewAdapter extends RecyclerView.Adapter implements Filterable, FastScroller.SectionIndexer { private final Context mContext; private List mFlagsList = new ArrayList<>(); private List mFlagsListFiltered = new ArrayList<>(); private String mPhenotypePackageName = null; private CharSequence mLastFilterPerformed = null; private final ICoreRootService mCoreRootServiceIpc; public BooleanModsRecyclerViewAdapter(Context context, ICoreRootService coreRootServiceIpc) { mContext = context; mCoreRootServiceIpc = coreRootServiceIpc; } @SuppressLint("NotifyDataSetChanged") public void selectPhenotypePackageName(String phenotypePackageName) { mPhenotypePackageName = phenotypePackageName; try { mFlagsList = new ArrayList<>(); TreeMap> map = new TreeMap>(mCoreRootServiceIpc.phenotypeDBGetBooleanFlagsOrOverridden(phenotypePackageName)); for (Map.Entry> flag : map.entrySet()) { String flagName = flag.getKey(); List flagData = flag.getValue(); Boolean flagValue = (Boolean) flagData.get(0); Boolean flagOverriddenAndChanged = (Boolean) flagData.get(1); mFlagsList.add(new BooleanFlag(flagName, flagValue, flagOverriddenAndChanged)); } if (mLastFilterPerformed != null) { getFilter().filter(mLastFilterPerformed); } else { mFlagsListFiltered = mFlagsList; notifyDataSetChanged(); } } catch (RemoteException e) { e.printStackTrace(); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // Initialize binding and viewHolder SwitchCardBinding binding = SwitchCardBinding.inflate(LayoutInflater.from(mContext), parent, false); ViewHolder viewHolder = new ViewHolder(binding); // Set setOnCheckedChangeListener on list items viewHolder.mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { int checkedPosition = viewHolder.getAdapterPosition(); BooleanFlag checkedBooleanFlag = mFlagsListFiltered.get(checkedPosition); String checkedBooleanFlagName = checkedBooleanFlag.getFlagName(); checkedBooleanFlag.setFlagValue(isChecked); try { mCoreRootServiceIpc.phenotypeDBOverrideBooleanFlag(mPhenotypePackageName, checkedBooleanFlagName, isChecked); } catch (RemoteException e) { e.printStackTrace(); } checkedBooleanFlag.setFlagOverriddenAndChanged(!checkedBooleanFlag.getFlagOverriddenAndChanged()); notifyItemChanged(checkedPosition); }); // Return viewHolder return viewHolder; } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { // Get the boolean flag BooleanFlag booleanFlag = mFlagsListFiltered.get(position); // Update switch text holder.mTextView.setText(booleanFlag.getFlagName()); // Update the switch checked status without triggering any existing listener holder.mSwitch.setCheckedProgrammatically(booleanFlag.getFlagValue()); // Change background color for cards containing overridden and changed flags TypedArray typedArray = mContext.getTheme().obtainStyledAttributes(R.styleable.ViewStyle); int colorSurface = typedArray.getColor(R.styleable.ViewStyle_colorSurface, Color.WHITE); int colorSurfaceVariant = typedArray.getColor(R.styleable.ViewStyle_colorSecondaryContainer, Color.LTGRAY); typedArray.recycle(); int cardBackgroundColor = booleanFlag.getFlagOverriddenAndChanged() ? colorSurfaceVariant : colorSurface; ((MaterialCardView) holder.itemView).setCardBackgroundColor(cardBackgroundColor); } @Override public int getItemCount() { return mFlagsListFiltered.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { mLastFilterPerformed = charSequence; try { JSONObject filterConfig = new JSONObject(charSequence.toString()); String key = filterConfig.getString("key"); boolean enabled = filterConfig.getBoolean("enabled"); boolean disabled = filterConfig.getBoolean("disabled"); boolean changed = filterConfig.getBoolean("changed"); boolean unchanged = filterConfig.getBoolean("unchanged"); List flagsListFiltered = new ArrayList<>(); for (BooleanFlag booleanFlag : mFlagsList) { if (booleanFlag.getFlagName().toLowerCase().contains(key.toLowerCase())) { boolean flagValue = booleanFlag.getFlagValue(); boolean flagChanged = booleanFlag.getFlagOverriddenAndChanged(); if (((enabled && flagValue) || (disabled && !flagValue)) && ((changed && flagChanged) || (unchanged && !flagChanged))) flagsListFiltered.add(booleanFlag); } } mFlagsListFiltered = flagsListFiltered; } catch (JSONException e) { e.printStackTrace(); } FilterResults filterResults = new FilterResults(); filterResults.values = mFlagsListFiltered; filterResults.count = mFlagsListFiltered.size(); return filterResults; } @SuppressLint("NotifyDataSetChanged") @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { mFlagsListFiltered = (List) filterResults.values; notifyDataSetChanged(); } }; } @Override public CharSequence getSectionText(int position) { return mFlagsListFiltered.get(position).getFlagName().substring(0, 1); } public static class ViewHolder extends RecyclerView.ViewHolder { private final TextView mTextView; private final ProgrammaticMaterialSwitchView mSwitch; public ViewHolder(SwitchCardBinding binding) { super(binding.getRoot()); mTextView = binding.switchCardTextview; mSwitch = binding.switchCardSwitch; } } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/adapter/SelectPackageRecyclerViewAdapter.java ================================================ package com.jacopomii.gappsmod.ui.adapter; import static com.jacopomii.gappsmod.util.Utils.getApplicationLabelOrUnknown; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.recyclerview.widget.RecyclerView; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.data.PhenotypeDBPackageName; import com.jacopomii.gappsmod.databinding.PackageRowBinding; import com.jacopomii.gappsmod.util.OnItemClickListener; import com.l4digital.fastscroll.FastScroller; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; @SuppressWarnings("unchecked") public class SelectPackageRecyclerViewAdapter extends RecyclerView.Adapter implements Filterable, FastScroller.SectionIndexer { private final Context mContext; private List mPackageList = new ArrayList<>(); private List mPackageListFiltered = new ArrayList<>(); private final ICoreRootService mCoreRootServiceIpc; private final OnItemClickListener mOnItemClickListener; @SuppressLint("NotifyDataSetChanged") public SelectPackageRecyclerViewAdapter(Context context, ICoreRootService coreRootServiceIpc, OnItemClickListener onItemClickListener) { mContext = context; mCoreRootServiceIpc = coreRootServiceIpc; mOnItemClickListener = onItemClickListener; try { mPackageList = new ArrayList<>(); TreeMap map = new TreeMap(mCoreRootServiceIpc.phenotypeDBGetAllPackageNames()); for (Map.Entry packageName : map.entrySet()) mPackageList.add(new PhenotypeDBPackageName(packageName.getKey(), packageName.getValue())); mPackageListFiltered = mPackageList; notifyDataSetChanged(); } catch (RemoteException e) { e.printStackTrace(); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // Initialize binding and viewHolder PackageRowBinding binding = PackageRowBinding.inflate(LayoutInflater.from(mContext), parent, false); ViewHolder viewHolder = new ViewHolder(binding); // Set onClickListener on list rows viewHolder.mRow.setOnClickListener(v -> { int position = viewHolder.getAdapterPosition(); mOnItemClickListener.onItemClick(mPackageListFiltered.get(position).getPhenotypePackageName()); }); // Return viewHolder return viewHolder; } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String phenotypePackageName = mPackageListFiltered.get(position).getPhenotypePackageName(); String androidPackageName = mPackageListFiltered.get(position).getAndroidPackageName(); PackageManager packageManager = mContext.getPackageManager(); Drawable packageIcon; try { packageIcon = packageManager.getApplicationIcon(androidPackageName); } catch (PackageManager.NameNotFoundException e) { packageIcon = AppCompatResources.getDrawable(mContext, R.drawable.ic_error_24); if (packageIcon != null) { TypedArray typedArray = mContext.getTheme().obtainStyledAttributes(R.styleable.ViewStyle); int colorError = typedArray.getColor(R.styleable.ViewStyle_colorError, Color.RED); typedArray.recycle(); packageIcon.mutate().setTint(colorError); } } holder.mPackageIcon.setImageDrawable(packageIcon); String appName = getApplicationLabelOrUnknown(mContext, androidPackageName); holder.mAppName.setText(appName); holder.mPhenotypePackageName.setText(phenotypePackageName); } @Override public int getItemCount() { return mPackageListFiltered.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String keyLowercase = charSequence.toString().toLowerCase(); List packageListFiltered = new ArrayList<>(); for (PhenotypeDBPackageName phenotypeDBPackageName : mPackageList) { String phenotypePackageNameLowercase = phenotypeDBPackageName.getPhenotypePackageName().toLowerCase(); String appNameLowercase = getApplicationLabelOrUnknown(mContext, phenotypeDBPackageName.getAndroidPackageName()).toLowerCase(); if (phenotypePackageNameLowercase.contains(keyLowercase) || appNameLowercase.contains(keyLowercase)) packageListFiltered.add(phenotypeDBPackageName); } mPackageListFiltered = packageListFiltered; FilterResults filterResults = new FilterResults(); filterResults.values = mPackageListFiltered; filterResults.count = mPackageListFiltered.size(); return filterResults; } @SuppressLint("NotifyDataSetChanged") @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { mPackageListFiltered = (List) filterResults.values; notifyDataSetChanged(); } }; } @Override public CharSequence getSectionText(int position) { // Get raw Phenotype package name String phenotypePackageName = mPackageListFiltered.get(position).getPhenotypePackageName(); // Initialize indexEnd int indexEnd = 0; // Try to split package name by dot character: look for the first 4 parts, if there aren't try 3, 2 and so on for (int i = 4; i >= 0; i--) { int indexEndTmp = StringUtils.ordinalIndexOf(phenotypePackageName, ".", i); if (indexEndTmp != -1) { indexEnd = indexEndTmp; break; } } // Increment indexEnd by 2 to show two more characters beyond the found parts if (indexEnd + 2 <= phenotypePackageName.length()) indexEnd += 2; else indexEnd = phenotypePackageName.length(); // Return the phenotypePackageName parsed substring return phenotypePackageName.substring(0, indexEnd); } public static class ViewHolder extends RecyclerView.ViewHolder { private final LinearLayout mRow; private final ImageView mPackageIcon; private final TextView mAppName; private final TextView mPhenotypePackageName; public ViewHolder(PackageRowBinding binding) { super(binding.getRoot()); mRow = binding.row; mPackageIcon = binding.packageIcon; mAppName = binding.appName; mPhenotypePackageName = binding.phenotypePackageName; } } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/BooleanModsFragment.java ================================================ package com.jacopomii.gappsmod.ui.fragment; import static com.jacopomii.gappsmod.util.Utils.showSelectPackageDialog; import android.app.Activity; import android.graphics.Rect; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.FragmentBooleanModsBinding; import com.jacopomii.gappsmod.ui.activity.MainActivity; import com.jacopomii.gappsmod.ui.adapter.BooleanModsRecyclerViewAdapter; import com.jacopomii.gappsmod.ui.view.FilterableSearchView; import com.l4digital.fastscroll.FastScrollRecyclerView; import org.json.JSONException; import org.json.JSONObject; import java.util.concurrent.atomic.AtomicBoolean; public class BooleanModsFragment extends Fragment { private FragmentBooleanModsBinding mBinding; private BooleanModsRecyclerViewAdapter mFlagsRecyclerViewAdapter; private ICoreRootService mCoreRootServiceIpc; private String flagsFilterKey = ""; private boolean flagsFilterEnabled = true; private boolean flagsFilterDisabled = true; private boolean flagsFilterChanged = true; private boolean flagsFilterUnchanged = true; public BooleanModsFragment() { } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Activity activity = getActivity(); if (activity instanceof MainActivity) mCoreRootServiceIpc = ((MainActivity) activity).getCoreRootServiceIpc(); else throw new RuntimeException("SuggestedModsFragment can be attached only to the MainActivity"); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // View bindings mBinding = FragmentBooleanModsBinding.inflate(getLayoutInflater()); // Setup menu setupMenu(); // Select package TextView selectPackage = mBinding.selectPackage; // Initialize the selectPackageDialogOpened AtomicBoolean selectPackageDialogOpened = new AtomicBoolean(false); // Select package onClick selectPackage.setOnClickListener(v -> { // Clear focus from other views View currentFocus = requireActivity().getCurrentFocus(); if (currentFocus != null) currentFocus.clearFocus(); // If the select package dialog isn't already opened if (!selectPackageDialogOpened.get()) { // Set the selectPackageDialogOpened to true selectPackageDialogOpened.set(true); // Show the select package dialog showSelectPackageDialog(getContext(), mCoreRootServiceIpc, item -> { // The item received by the listener here is the Phenotype package name chosen by the user // Update the select package textview selectPackage.setText((String) item); // Update the selectPackageRecyclerView adapter mFlagsRecyclerViewAdapter.selectPhenotypePackageName((String) item); // Set the selectPackageDialogOpened to false selectPackageDialogOpened.set(false); }, dialog -> { // Set the selectPackageDialogOpened to false dismissing the dialog selectPackageDialogOpened.set(false); }); } }); // Flags recyclerview FastScrollRecyclerView flagsRecyclerView = mBinding.recyclerview; // Initialize the flagsRecyclerView adapter mFlagsRecyclerViewAdapter = new BooleanModsRecyclerViewAdapter(getContext(), mCoreRootServiceIpc); // Disable fast scroll if the flagsRecyclerView is empty or changes to empty flagsRecyclerView.setFastScrollEnabled(mFlagsRecyclerViewAdapter.getItemCount() != 0); mFlagsRecyclerViewAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { super.onChanged(); flagsRecyclerView.setFastScrollEnabled(mFlagsRecyclerViewAdapter.getItemCount() != 0); } }); // Set flagsRecyclerView items padding flagsRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int padding = (int) getResources().getDimension(R.dimen.margin_generic); int itemPosition = parent.getChildAdapterPosition(view); if (itemPosition == 0) outRect.top = padding; else if (itemPosition == mFlagsRecyclerViewAdapter.getItemCount() - 1) outRect.bottom = padding; outRect.left = padding; outRect.right = padding; } }); // Set the flagsRecyclerView LayoutManager and Adapter flagsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); flagsRecyclerView.setAdapter(mFlagsRecyclerViewAdapter); // Return the fragment view return mBinding.getRoot(); } private void setupMenu() { requireActivity().addMenuProvider(new MenuProvider() { @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { // Inflate the menu layout menuInflater.inflate(R.menu.search_menu, menu); // Initialize the menuSearchIcon MenuItem menuSearchIcon = menu.findItem(R.id.menu_search_icon); // Initialize filterableSearchView and the additional filter container FilterableSearchView filterableSearchView = (FilterableSearchView) menuSearchIcon.getActionView(); filterableSearchView.setQueryHint(getString(R.string.search_by_flag)); filterableSearchView.setFilterContainer(mBinding.filterContainer, false); // Initialize the filterEnabledStatusSpinner String[] filterEnabledStatusSpinnerChoices = new String[]{getString(R.string.enabled_and_disabled), getString(R.string.enabled_only), getString(R.string.disabled_only)}; mBinding.filterEnabledStatusSpinner.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterEnabledStatusSpinnerChoices)); mBinding.filterEnabledStatusSpinner.setOnItemClickListener((parent, view, position, id) -> { flagsFilterEnabled = position == 0 || position == 1; flagsFilterDisabled = position == 0 || position == 2; applyFlagsFilters(); }); // Initialize the filterChangedStatusSpinner String[] filterChangedStatusSpinnerChoices = new String[]{getString(R.string.changed_and_unchanged), getString(R.string.changed_only), getString(R.string.unchanged_only)}; mBinding.filterChangedStatusSpinner.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filterChangedStatusSpinnerChoices)); mBinding.filterChangedStatusSpinner.setOnItemClickListener((parent, view, position, id) -> { flagsFilterChanged = position == 0 || position == 1; flagsFilterUnchanged = position == 0 || position == 2; applyFlagsFilters(); }); // Set flags filters to default values resetFlagsFilters(); // Handle menuSearchIcon expand / collapse actions menuSearchIcon.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { // When the search view is collapsed, flag filters need to be reset and applied resetFlagsFilters(); applyFlagsFilters(); return true; } }); // Handle filterableSearchView search query changes filterableSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { flagsFilterKey = newText; applyFlagsFilters(); return false; } }); } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { return false; } }, getViewLifecycleOwner(), Lifecycle.State.RESUMED); } private void resetFlagsFilters() { flagsFilterKey = ""; flagsFilterEnabled = true; flagsFilterDisabled = true; flagsFilterChanged = true; flagsFilterUnchanged = true; mBinding.filterEnabledStatusSpinner.setText(mBinding.filterEnabledStatusSpinner.getAdapter().getItem(0).toString(), false); mBinding.filterChangedStatusSpinner.setText(mBinding.filterChangedStatusSpinner.getAdapter().getItem(0).toString(), false); } private void applyFlagsFilters() { try { JSONObject filterConfig = new JSONObject(); filterConfig.put("key", flagsFilterKey); filterConfig.put("enabled", flagsFilterEnabled); filterConfig.put("disabled", flagsFilterDisabled); filterConfig.put("changed", flagsFilterChanged); filterConfig.put("unchanged", flagsFilterUnchanged); mFlagsRecyclerViewAdapter.getFilter().filter(filterConfig.toString()); } catch (JSONException e) { e.printStackTrace(); } } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/InformationFragment.java ================================================ package com.jacopomii.gappsmod.ui.fragment; import android.os.Bundle; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.jacopomii.gappsmod.databinding.FragmentInformationBinding; public class InformationFragment extends Fragment { FragmentInformationBinding mBinding; public InformationFragment() {} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mBinding = FragmentInformationBinding.inflate(getLayoutInflater()); // Links aren't clickable workaround mBinding.madeWithLoveByJacopoTediosi.setMovementMethod(LinkMovementMethod.getInstance()); return mBinding.getRoot(); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/RevertModsFragment.java ================================================ package com.jacopomii.gappsmod.ui.fragment; import static com.jacopomii.gappsmod.data.Constants.DIALER_CALLRECORDINGPROMPT; import static com.jacopomii.gappsmod.data.Constants.DIALER_PHENOTYPE_PACKAGE_NAME; import static com.jacopomii.gappsmod.util.Utils.showSelectPackageDialog; import android.app.Activity; import android.os.Bundle; import android.os.RemoteException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.FragmentRevertModsBinding; import com.jacopomii.gappsmod.ui.activity.MainActivity; import com.topjohnwu.superuser.nio.ExtendedFile; import com.topjohnwu.superuser.nio.FileSystemManager; import java.util.concurrent.atomic.AtomicBoolean; import es.dmoral.toasty.Toasty; public class RevertModsFragment extends Fragment { FragmentRevertModsBinding mBinding; private ICoreRootService mCoreRootServiceIpc; private FileSystemManager mCoreRootServiceFSManager; public RevertModsFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Activity activity = getActivity(); if (activity instanceof MainActivity) { mCoreRootServiceIpc = ((MainActivity) activity).getCoreRootServiceIpc(); mCoreRootServiceFSManager = ((MainActivity) activity).getCoreRootServiceFSManager(); } else { throw new RuntimeException("RevertModsFragment can be attached only to the MainActivity"); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // View bindings mBinding = FragmentRevertModsBinding.inflate(getLayoutInflater()); // Select package for revert mods for specific package TextView selectPackage = mBinding.selectPackage; // Initialize the selectPackageDialogOpened AtomicBoolean selectPackageDialogOpened = new AtomicBoolean(false); // Select package onClick selectPackage.setOnClickListener(v -> { // If the select package dialog isn't already opened if (!selectPackageDialogOpened.get()) { // Set the selectPackageDialogOpened to true selectPackageDialogOpened.set(true); // Show the select package dialog showSelectPackageDialog( getContext(), mCoreRootServiceIpc, item -> { // The item received by the listener here is the Phenotype package name chosen by the user // Update the select package textview selectPackage.setText((String) item); // Enable the revertModsSelectedPackageButton mBinding.revertModsSelectedPackageButton.setEnabled(true); }, dialog -> { // Set the selectPackageDialogOpened to false dismissing the dialog selectPackageDialogOpened.set(false); }); } }); // Revert mods for the selected package button mBinding.revertModsSelectedPackageButton.setOnClickListener(v -> { String selectedPhenotypePackageName = selectPackage.getText().toString(); new MaterialAlertDialogBuilder(requireContext()) .setMessage(String.format(getResources().getString(R.string.revert_mods_for_the_selected_package_confirm), selectedPhenotypePackageName)) .setNegativeButton(getString(R.string.no), (dialog, which) -> { }) .setPositiveButton(getString(R.string.yes), (dialog, which) -> { try { // Delete all flag overrides for the selected package from Phenotype DB mCoreRootServiceIpc.phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(selectedPhenotypePackageName); // If the selected package was the Dialer if (selectedPhenotypePackageName.equals(DIALER_PHENOTYPE_PACKAGE_NAME)) { // Delete the com.google.android.dialer callrecordingprompt folder (if it exists) ExtendedFile callRecordingPromptFolder = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); if (callRecordingPromptFolder.exists()) { //noinspection ResultOfMethodCallIgnored callRecordingPromptFolder.delete(); } } // UI confirmation to the user Toasty.success(requireContext(), getString(R.string.done), Toast.LENGTH_LONG, true).show(); } catch (RemoteException e) { e.printStackTrace(); Toasty.error(requireContext(), getString(R.string.an_error_has_occurred), Toast.LENGTH_LONG, true).show(); } }).show(); } ); // Revert all mods button mBinding.revertAllModsButton.setOnClickListener(v -> new MaterialAlertDialogBuilder(requireContext()) .setMessage(R.string.revert_mods_for_all_packages_confirm) .setNegativeButton(getString(R.string.no), (dialog, which) -> { }) .setPositiveButton(getString(R.string.yes), (dialog, which) -> { try { // Delete all flag overrides from Phenotype DB mCoreRootServiceIpc.phenotypeDBDeleteAllFlagOverrides(); // Delete the com.google.android.dialer callrecordingprompt folder ExtendedFile callRecordingPromptFolder = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); if (callRecordingPromptFolder.exists()) { //noinspection ResultOfMethodCallIgnored callRecordingPromptFolder.delete(); } // UI confirmation to the user Toasty.success(requireContext(), getString(R.string.done), Toast.LENGTH_LONG, true).show(); } catch (RemoteException e) { e.printStackTrace(); Toasty.error(requireContext(), getString(R.string.an_error_has_occurred), Toast.LENGTH_LONG, true).show(); } }).show() ); return mBinding.getRoot(); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/SuggestedModsFragment.java ================================================ package com.jacopomii.gappsmod.ui.fragment; import static android.Manifest.permission.CAPTURE_AUDIO_OUTPUT; import static com.jacopomii.gappsmod.data.Constants.DIALER_ANDROID_PACKAGE_NAME; import static com.jacopomii.gappsmod.data.Constants.DIALER_CALLRECORDINGPROMPT; import static com.jacopomii.gappsmod.data.Constants.DIALER_PHENOTYPE_PACKAGE_NAME; import static com.jacopomii.gappsmod.data.Constants.GOOGLE_PLAY_BETA_LINK; import static com.jacopomii.gappsmod.data.Constants.GOOGLE_PLAY_DETAILS_LINK; import static com.jacopomii.gappsmod.data.Constants.MESSAGES_ANDROID_PACKAGE_NAME; import static com.jacopomii.gappsmod.data.Constants.MESSAGES_PHENOTYPE_PACKAGE_NAME; import static com.jacopomii.gappsmod.util.Utils.copyFile; import static com.jacopomii.gappsmod.util.Utils.openGooglePlay; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.RemoteException; import android.telephony.TelephonyManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.protobuf.ByteString; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.FragmentSuggestedModsBinding; import com.jacopomii.gappsmod.protos.Call_screen_i18n_config; import com.jacopomii.gappsmod.ui.activity.MainActivity; import com.jacopomii.gappsmod.ui.view.ProgrammaticMaterialSwitchView; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.nio.ExtendedFile; import com.topjohnwu.superuser.nio.FileSystemManager; import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @SuppressWarnings("FieldCanBeLocal") public class SuggestedModsFragment extends Fragment { private FragmentSuggestedModsBinding mBinding; private ICoreRootService mCoreRootServiceIpc; private FileSystemManager mCoreRootServiceFSManager; // The following boolean flags force-enable Call Recording features in Dialer app private final HashMap DIALER_ENABLE_CALL_RECORDING_FLAGS = new HashMap() {{ // Enable Call Recording feature put("G__enable_call_recording", true); put("enable_call_recording_system_feature", true); // Enable Call Recording also for Google Fi / Fides (e2e calls, etc) put("CallRecording__enable_call_recording_for_fi", true); // Bypass country-related restrictions for call recording feature put("G__force_within_call_recording_geofence_value", true); // Bypass country-related restrictions for automatic call recording ("always record") feature put("G__force_within_crosby_geofence_value", true); // Allow the usage of the above two "force geofence" flags put("G__use_call_recording_geofence_overrides", true); // Show call recording button put("enable_tidepods_call_recording", true); }}; // The following extensionVal flags concern the announcement audio played when a call recording starts or ends in Dialer app. // To silence announcements, we set them as empty stringVal flags. private final HashMap DIALER_SILENCE_CALL_RECORDING_ALERTS_FLAGS = new HashMap() {{ // The following flag contains a protobuf list of countries where the use of embedded audio is enforced. // If its value is an empty string, the Dialer will by default use TTS to generate audio call recording alerts. put("CallRecording__call_recording_countries_with_built_in_audio_file", ""); // The following flags are no longer used in recent versions of the Dialer and remain here for backwards compatibility. // They were used to contain a protobuf list of countries where the use of embedded or TTS audio was enforced. put("CallRecording__call_recording_force_enable_built_in_audio_file_countries", ""); put("CallRecording__call_recording_force_enable_tts_countries", ""); // The following flag contains a protobuf hashset with country-language matches, used by Dialer to generate call recording audio alerts via TTS // in the right language. If its value is an empty string, TTS will always fall back to en_US (hardcoded in the Dialer sources). put("CallRecording__call_recording_countries", ""); }}; private final String DIALER_CALLRECORDINGPROMPT_STARTING_VOICE_US = "starting_voice-en_US.wav"; private final String DIALER_CALLRECORDINGPROMPT_ENDING_VOICE_US = "ending_voice-en_US.wav"; // Dialer versionCode 10681248 (94.x) is the last version in which we can silence call recording alerts. In newer versions Google patched our hack. private final int DIALER_SILENCE_CALL_RECORDING_ALERTS_MAX_VERSION = 10681248; // The following boolean flags force-enable Call Screen / Revelio features in Dialer app private final HashMap DIALER_ENABLE_CALL_SCREEN_FLAGS = new HashMap() {{ // Enable Call Screen feature for both calls and video-calls put("G__speak_easy_enabled", true); put("enable_video_calling_screen", true); // Bypass Call Screen locale restrictions put("G__speak_easy_bypass_locale_check", true); // Enable translations for additional locales put("enable_call_screen_i18n_tidepods", true); // Enable the "listen in" button, which is located at the bottom right during screening put("G__speak_easy_enable_listen_in_button", true); // Enable the Call Screen Demo page in Dialer settings put("enable_call_screen_demo", true); // Enable the "See transcript" button in call history, which allows to read call screen transcripts and listen to recordings put("G__enable_speakeasy_details", true); // Enable Revelio,an advanced version of the Call Screen which allows to automatically filter calls put("G__enable_revelio", true); put("G__enable_revelio_on_bluetooth", true); put("G__enable_revelio_on_wired_headset", true); // Bypass Revelio locale restrictions put("G__bypass_revelio_roaming_check", true); // Enable translations for additional locales also for Revelio put("G__enable_tidepods_revelio", true); // Enable the Dialer settings option to save screened call audio (it does not depend on the Call Recording feature, but depends on Revelio) put("G__enable_call_screen_saving_audio", true); // Enable the saving of the transcript also for Revelio put("enable_revelio_transcript", true); }}; // The following extensionVal flag contains a protobuf (see call_screen_i18n.proto for its definition) // which matches the languages to be used for the Call Screen feature to the supported countries in Dialer app private final String DIALER_CALL_SCREEN_I18N_CONFIG_FLAG = "CallScreenI18n__call_screen_i18n_config"; // The following boolean flag force-enables debug menu in Messages app private final HashMap MESSAGES_ENABLE_DEBUG_MENU_FLAGS = new HashMap() {{ put("bugle_phenotype__debug_menu_default_available", true); }}; // The following boolean flag force-enables marking conversations as unread in Messages app private final HashMap MESSAGES_ENABLE_MARKING_CONVERSATIONS_UNREAD_FLAGS = new HashMap() {{ put("bugle_phenotype__enable_mark_as_unread", true); }}; // The following boolean flags force-enable Message Organization (Super Sort) features in Messages app private final HashMap MESSAGES_ENABLE_MESSAGE_ORGANIZATION_FLAGS = new HashMap() {{ // Enable super sort put("bugle_phenotype__conversation_labels_enabled", true); // Enable "all" category (this flag may be superfluous) put("bugle_phenotype__supersort_badge_all_filter", true); // Enable donation banner put("bugle_phenotype__supersort_enable_update_donation_banner", true); // Enable OTP auto deletion put("bugle_phenotype__enable_otp_auto_deletion", true); // Classify messages also in the foreground (don't use only workmanager) put("bugle_phenotype__supersort_use_only_work_manager", false); // I don't know what is it. In the Messages code I see that it's about "generating annotations" for "money, coupon, account number, percentage" put("bugle_phenotype__enable_supersort_annotators", true); // QPBC = Participant Based Quick Classification. I don't know how it works. put("bugle_phenotype__supersort_enable_qpbc", true); }}; // The following boolean flag force-enables verified SMS settings menu in Messages app private final HashMap MESSAGES_ENABLE_VERIFIED_SMS_FLAGS = new HashMap() {{ put("bugle_phenotype__enabled_verified_sms", true); }}; // The following boolean flag force-enables sending images via GPhotos links in Messages app private final HashMap MESSAGES_ENABLE_IMAGES_VIA_GPHOTOS_FLAGS = new HashMap() {{ put("bugle_phenotype__enable_google_photos_image_by_link", true); }}; // The following boolean flags force-enable nudges and birthday reminders in Messages app private final HashMap MESSAGES_ENABLE_NUDGES_FLAGS = new HashMap() {{ // Enable nudges and birthday reminders put("bugle_phenotype__enable_nudge", true); put("bugle_phenotype__enable_birthday_nudge", true); put("bugle_phenotype__enable_birthday_suggestions", true); // Enable banners put("bugle_phenotype__enable_nudge_banner", true); put("bugle_phenotype__enable_birthday_banner", true); put("bugle_phenotype__enable_save_birthday_banner", true); // Enable settings pages put("bugle_phenotype__enable_birthday_nudge_setting", true); put("bugle_phenotype__enable_birthday_banner_settings_button", true); // Leave the settings menu lines separate put("bugle_phenotype__combing_nudge_settings", false); }}; // The following boolean flags force-enable spotlights suggestions settings menu in Messages app private final HashMap MESSAGES_ENABLE_SPOTLIGHTS_FLAGS = new HashMap() {{ // Enable spotlights put("bugle_phenotype__enable_spotlights", true); // Enable spotlights settings page put("bugle_phenotype__enable_spotlight_settings_page", true); // Enable new settings page layout, otherwise it won't show up put("bugle_phenotype__enable_smarts_settings_page_v2", true); // Enable additional spotlights features put("bugle_phenotype__enable_spotlights_google_search", true); }}; // The following boolean flag force-enables smart compose (predictive writing) settings menu in Messages app private final HashMap MESSAGES_ENABLE_SMART_COMPOSE_FLAGS = new HashMap() {{ put("bugle_phenotype__enable_smart_compose", true); }}; // The following boolean flags force-enable magic compose (draft suggestions with Bard AI) in Messages app private final HashMap MESSAGES_ENABLE_MAGIC_COMPOSE_FLAGS = new HashMap() {{ // Enable magic compose view put("bugle_phenotype__enable_magic_compose_view", true); // Idk what is it, but it has to be true to effectively enable magic compose put("bugle_phenotype__enable_combined_magic_compose", true); // Enable all additional functionalities for magic compose (e.g., feedback and multiple writing styles) put("bugle_phenotype__enable_additional_functionalities_for_magic_compose", true); // Enable magic compose also in xms put("bugle_phenotype__magic_compose_enabled_in_xms", true); }}; // The following boolean flag force-enables smart actions (smart reply) in notifications in Messages app private final HashMap MESSAGES_ENABLE_SMART_ACTIONS_IN_NOTIFICATIONS_FLAGS = new HashMap() {{ put("bugle_phenotype__enable_smart_actions_in_notifications", true); }}; // The following boolean flag force-enables suggested stickers settings menu in Messages app private final HashMap MESSAGES_ENABLE_SUGGESTED_STICKERS_FLAGS = new HashMap() {{ put("bugle_phenotype__sticker_suggestions_setting_enabled", true); }}; public SuggestedModsFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Activity activity = getActivity(); if (activity instanceof MainActivity) { mCoreRootServiceIpc = ((MainActivity) activity).getCoreRootServiceIpc(); mCoreRootServiceFSManager = ((MainActivity) activity).getCoreRootServiceFSManager(); } else { throw new RuntimeException("SuggestedModsFragment can be attached only to the MainActivity"); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mBinding = FragmentSuggestedModsBinding.inflate(getLayoutInflater()); // GDialer try { // Beta and Install buttons actions mBinding.dialerAppHeader.getBetaButton().setOnClickListener(v -> openGooglePlay(requireContext(), GOOGLE_PLAY_BETA_LINK + DIALER_ANDROID_PACKAGE_NAME)); mBinding.dialerAppHeader.getInstallButton().setOnClickListener(v -> openGooglePlay(requireContext(), GOOGLE_PLAY_DETAILS_LINK + DIALER_ANDROID_PACKAGE_NAME)); // Check if application is installed requireContext().getPackageManager().getApplicationInfo(DIALER_ANDROID_PACKAGE_NAME, 0); // Check if application has CAPTURE_AUDIO_OUTPUT permission if (requireContext().getPackageManager().checkPermission(CAPTURE_AUDIO_OUTPUT, DIALER_ANDROID_PACKAGE_NAME) != PackageManager.PERMISSION_GRANTED) mBinding.dialerPermissionAlert.setVisibility(View.VISIBLE); // dialerForceEnableCallRecordingSwitch ProgrammaticMaterialSwitchView dialerForceEnableCallRecordingSwitch = mBinding.dialerForceEnableCallRecording.getSwitch(); boolean dialerForceEnableCallRecordingSwitchChecked = modCheckAreAllFlagsOverridden(DIALER_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(DIALER_ENABLE_CALL_RECORDING_FLAGS.keySet())); dialerForceEnableCallRecordingSwitch.setCheckedProgrammatically(dialerForceEnableCallRecordingSwitchChecked); dialerForceEnableCallRecordingSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, DIALER_PHENOTYPE_PACKAGE_NAME, DIALER_ENABLE_CALL_RECORDING_FLAGS)); dialerForceEnableCallRecordingSwitch.setEnabled(true); // dialerSilenceCallRecordingAlertsSwitch ProgrammaticMaterialSwitchView dialerSilenceCallRecordingAlertsSwitch = mBinding.dialerSilenceCallRecordingAlerts.getSwitch(); boolean dialerSilenceCallRecordingAlertsSwitchChecked = false; try { ExtendedFile startingVoiceFile = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT, DIALER_CALLRECORDINGPROMPT_STARTING_VOICE_US); ExtendedFile endingVoiceFile = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT, DIALER_CALLRECORDINGPROMPT_STARTING_VOICE_US); if (startingVoiceFile.exists() && endingVoiceFile.exists()) { InputStream silentVoiceInputStream; InputStream startingVoiceInputStream = startingVoiceFile.newInputStream(); silentVoiceInputStream = getResources().openRawResource(R.raw.silent_wav); boolean isStartingVoiceSilenced = IOUtils.contentEquals(silentVoiceInputStream, startingVoiceInputStream); startingVoiceInputStream.close(); silentVoiceInputStream.close(); InputStream endingVoiceInputStream = endingVoiceFile.newInputStream(); silentVoiceInputStream = getResources().openRawResource(R.raw.silent_wav); boolean isEndingVoiceSilenced = IOUtils.contentEquals(silentVoiceInputStream, endingVoiceInputStream); endingVoiceInputStream.close(); silentVoiceInputStream.close(); dialerSilenceCallRecordingAlertsSwitchChecked = modCheckAreAllFlagsOverridden(DIALER_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(DIALER_SILENCE_CALL_RECORDING_ALERTS_FLAGS.keySet())) && isStartingVoiceSilenced && isEndingVoiceSilenced; } } catch (IOException e) { e.printStackTrace(); } try { // If Dialer version > SILENCE_CALL_RECORDING_ALERTS_MAX_VERSION the dialerSilenceCallRecordingAlertsSwitch must remain disabled if (requireContext().getPackageManager().getPackageInfo(DIALER_ANDROID_PACKAGE_NAME, 0).versionCode > DIALER_SILENCE_CALL_RECORDING_ALERTS_MAX_VERSION) { // If the dialerSilenceCallRecordingAlertsSwitch was enabled in previous versions of GAppsMod, the silenceCallRecordingAlerts mod must be automatically disabled if (dialerSilenceCallRecordingAlertsSwitchChecked) { dialerSilenceCallRecordingAlerts(false); } // Otherwise, the dialerSilenceCallRecordingAlertsSwitch should be loaded as usual } else { dialerSilenceCallRecordingAlertsSwitch.setCheckedProgrammatically(dialerSilenceCallRecordingAlertsSwitchChecked); dialerSilenceCallRecordingAlertsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> dialerSilenceCallRecordingAlerts(isChecked)); dialerSilenceCallRecordingAlertsSwitch.setEnabled(true); } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } // dialerForceEnableCallScreenSwitch ProgrammaticMaterialSwitchView dialerForceEnableCallScreenSwitch = mBinding.dialerForceEnableCallScreen.getSwitch(); boolean dialerForceEnableCallScreenSwitchChecked = modCheckAreAllFlagsOverridden(DIALER_PHENOTYPE_PACKAGE_NAME, Collections.singletonList(DIALER_CALL_SCREEN_I18N_CONFIG_FLAG)); dialerForceEnableCallScreenSwitch.setCheckedProgrammatically(dialerForceEnableCallScreenSwitchChecked); dialerForceEnableCallScreenSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> dialerForceEnableCallScreen(isChecked)); dialerForceEnableCallScreenSwitch.setEnabled(true); } catch (PackageManager.NameNotFoundException e) { mBinding.dialerNotInstalledAlert.setVisibility(View.VISIBLE); } // GMessages try { // Beta and Install buttons actions mBinding.messagesAppHeader.getBetaButton().setOnClickListener(v -> openGooglePlay(requireContext(), GOOGLE_PLAY_BETA_LINK + MESSAGES_ANDROID_PACKAGE_NAME)); mBinding.messagesAppHeader.getInstallButton().setOnClickListener(v -> openGooglePlay(requireContext(), GOOGLE_PLAY_DETAILS_LINK + MESSAGES_ANDROID_PACKAGE_NAME)); // Check if application is installed requireContext().getPackageManager().getApplicationInfo(MESSAGES_ANDROID_PACKAGE_NAME, 0); // messagesForceEnableDebugMenuSwitch ProgrammaticMaterialSwitchView messagesForceEnableDebugMenuSwitch = mBinding.messagesForceEnableDebugMenu.getSwitch(); boolean messagesForceEnableDebugMenuSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_DEBUG_MENU_FLAGS.keySet())); messagesForceEnableDebugMenuSwitch.setCheckedProgrammatically(messagesForceEnableDebugMenuSwitchChecked); messagesForceEnableDebugMenuSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_DEBUG_MENU_FLAGS)); messagesForceEnableDebugMenuSwitch.setEnabled(true); // messagesForceEnableMarkingMessageThreadsUnreadSwitch ProgrammaticMaterialSwitchView messagesForceEnableMarkingMessageThreadsUnreadSwitch = mBinding.messagesForceEnableMarkingMessageThreadsUnread.getSwitch(); boolean messagesForceEnableMarkingMessageThreadsUnreadSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_MARKING_CONVERSATIONS_UNREAD_FLAGS.keySet())); messagesForceEnableMarkingMessageThreadsUnreadSwitch.setCheckedProgrammatically(messagesForceEnableMarkingMessageThreadsUnreadSwitchChecked); messagesForceEnableMarkingMessageThreadsUnreadSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_MARKING_CONVERSATIONS_UNREAD_FLAGS)); messagesForceEnableMarkingMessageThreadsUnreadSwitch.setEnabled(true); // messagesForceEnableMessageOrganizationSwitch ProgrammaticMaterialSwitchView messagesForceEnableMessageOrganizationSwitch = mBinding.messagesForceEnableMessageOrganization.getSwitch(); boolean messagesForceEnableMessageOrganizationSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_MESSAGE_ORGANIZATION_FLAGS.keySet())); messagesForceEnableMessageOrganizationSwitch.setCheckedProgrammatically(messagesForceEnableMessageOrganizationSwitchChecked); messagesForceEnableMessageOrganizationSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_MESSAGE_ORGANIZATION_FLAGS)); messagesForceEnableMessageOrganizationSwitch.setEnabled(true); // messagesForceEnableVerifiedSmsSwitch ProgrammaticMaterialSwitchView messagesForceEnableVerifiedSmsSwitch = mBinding.messagesForceEnableVerifiedSms.getSwitch(); boolean messagesForceEnableVerifiedSmsSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_VERIFIED_SMS_FLAGS.keySet())); messagesForceEnableVerifiedSmsSwitch.setCheckedProgrammatically(messagesForceEnableVerifiedSmsSwitchChecked); messagesForceEnableVerifiedSmsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_VERIFIED_SMS_FLAGS)); messagesForceEnableVerifiedSmsSwitch.setEnabled(true); // messagesForceEnableGphotosSwitch ProgrammaticMaterialSwitchView messagesForceEnableGphotosSwitch = mBinding.messagesForceEnableGphotos.getSwitch(); boolean messagesForceEnableGphotosSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_IMAGES_VIA_GPHOTOS_FLAGS.keySet())); messagesForceEnableGphotosSwitch.setCheckedProgrammatically(messagesForceEnableGphotosSwitchChecked); messagesForceEnableGphotosSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_IMAGES_VIA_GPHOTOS_FLAGS)); messagesForceEnableGphotosSwitch.setEnabled(true); // messagesForceEnableNudgesSwitch ProgrammaticMaterialSwitchView messagesForceEnableNudgesSwitch = mBinding.messagesForceEnableNudges.getSwitch(); boolean messagesForceEnableNudgesSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_NUDGES_FLAGS.keySet())); messagesForceEnableNudgesSwitch.setCheckedProgrammatically(messagesForceEnableNudgesSwitchChecked); messagesForceEnableNudgesSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_NUDGES_FLAGS)); messagesForceEnableNudgesSwitch.setEnabled(true); // messagesForceEnableSpotlightsSwitch ProgrammaticMaterialSwitchView messagesForceEnableSpotlightsSwitch = mBinding.messagesForceEnableSpotlights.getSwitch(); boolean messagesForceEnableSpotlightsSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_SPOTLIGHTS_FLAGS.keySet())); messagesForceEnableSpotlightsSwitch.setCheckedProgrammatically(messagesForceEnableSpotlightsSwitchChecked); messagesForceEnableSpotlightsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_SPOTLIGHTS_FLAGS)); messagesForceEnableSpotlightsSwitch.setEnabled(true); // messagesForceEnableSmartComposeSwitch ProgrammaticMaterialSwitchView messagesForceEnableSmartComposeSwitch = mBinding.messagesForceEnableSmartCompose.getSwitch(); boolean messagesForceEnableSmartComposeSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_SMART_COMPOSE_FLAGS.keySet())); messagesForceEnableSmartComposeSwitch.setCheckedProgrammatically(messagesForceEnableSmartComposeSwitchChecked); messagesForceEnableSmartComposeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_SMART_COMPOSE_FLAGS)); messagesForceEnableSmartComposeSwitch.setEnabled(true); // messagesForceEnableMagicComposeSwitch ProgrammaticMaterialSwitchView messagesForceEnableMagicComposeSwitch = mBinding.messagesForceEnableMagicCompose.getSwitch(); boolean messagesForceEnableMagicComposeSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_MAGIC_COMPOSE_FLAGS.keySet())); messagesForceEnableMagicComposeSwitch.setCheckedProgrammatically(messagesForceEnableMagicComposeSwitchChecked); messagesForceEnableMagicComposeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_MAGIC_COMPOSE_FLAGS)); messagesForceEnableMagicComposeSwitch.setEnabled(true); // messagesForceEnableSmartActionsInNotificationsSwitch ProgrammaticMaterialSwitchView messagesForceEnableSmartActionsInNotificationsSwitch = mBinding.messagesForceEnableSmartActionsInNotifications.getSwitch(); boolean messagesForceEnableSmartActionsInNotificationsSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_SMART_ACTIONS_IN_NOTIFICATIONS_FLAGS.keySet())); messagesForceEnableSmartActionsInNotificationsSwitch.setCheckedProgrammatically(messagesForceEnableSmartActionsInNotificationsSwitchChecked); messagesForceEnableSmartActionsInNotificationsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_SMART_ACTIONS_IN_NOTIFICATIONS_FLAGS)); messagesForceEnableSmartActionsInNotificationsSwitch.setEnabled(true); // messagesForceEnableSuggestedStickersSwitch ProgrammaticMaterialSwitchView messagesForceEnableSuggestedStickersSwitch = mBinding.messagesForceEnableSuggestedStickers.getSwitch(); boolean messagesForceEnableSuggestedStickersSwitchChecked = modCheckAreAllFlagsOverridden(MESSAGES_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(MESSAGES_ENABLE_SUGGESTED_STICKERS_FLAGS.keySet())); messagesForceEnableSuggestedStickersSwitch.setCheckedProgrammatically(messagesForceEnableSuggestedStickersSwitchChecked); messagesForceEnableSuggestedStickersSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> modSetBooleanFlags(isChecked, MESSAGES_PHENOTYPE_PACKAGE_NAME, MESSAGES_ENABLE_SUGGESTED_STICKERS_FLAGS)); messagesForceEnableSuggestedStickersSwitch.setEnabled(true); } catch (PackageManager.NameNotFoundException e) { mBinding.messagesNotInstalledAlert.setVisibility(View.VISIBLE); } return mBinding.getRoot(); } private boolean modCheckAreAllFlagsOverridden(String phenotypePackageName, List flags) { try { return mCoreRootServiceIpc.phenotypeDBAreAllFlagsOverridden(phenotypePackageName, flags); } catch (RemoteException e) { e.printStackTrace(); return false; } } private void modSetBooleanFlags(boolean enableMod, String phenotypePackageName, HashMap flags) { if (enableMod) { for (Map.Entry flag : flags.entrySet()) { try { mCoreRootServiceIpc.phenotypeDBOverrideBooleanFlag(phenotypePackageName, flag.getKey(), flag.getValue()); } catch (RemoteException e) { e.printStackTrace(); } } } else { modDeleteFlagOverrides(phenotypePackageName, new ArrayList<>(flags.keySet())); } } private void modSetStringFlags(boolean enableMod, String phenotypePackageName, HashMap flags) { if (enableMod) { for (Map.Entry flag : flags.entrySet()) { try { mCoreRootServiceIpc.phenotypeDBOverrideStringFlag(phenotypePackageName, flag.getKey(), flag.getValue()); } catch (RemoteException e) { e.printStackTrace(); } } } else { modDeleteFlagOverrides(phenotypePackageName, new ArrayList<>(flags.keySet())); } } private void modDeleteFlagOverrides(String phenotypePackageName, List flags) { try { mCoreRootServiceIpc.phenotypeDBDeleteFlagOverrides(phenotypePackageName, flags); } catch (RemoteException e) { e.printStackTrace(); } } private void dialerSilenceCallRecordingAlerts(boolean enableMod) { // Set flags (or delete overrides) modSetStringFlags(enableMod, DIALER_PHENOTYPE_PACKAGE_NAME, DIALER_SILENCE_CALL_RECORDING_ALERTS_FLAGS); // Apply additional mods if (enableMod) { try { // Create CALLRECORDINGPROMPT folder ExtendedFile callRecordingPromptDir = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); if (callRecordingPromptDir.mkdir() || (callRecordingPromptDir.exists() && callRecordingPromptDir.isDirectory())) { // Overwrite the two alert files with an empty audio ExtendedFile startingVoice = mCoreRootServiceFSManager.getFile(callRecordingPromptDir, DIALER_CALLRECORDINGPROMPT_STARTING_VOICE_US); ExtendedFile endingVoice = mCoreRootServiceFSManager.getFile(callRecordingPromptDir, DIALER_CALLRECORDINGPROMPT_ENDING_VOICE_US); copyFile(getResources().openRawResource(R.raw.silent_wav), startingVoice.newOutputStream()); copyFile(getResources().openRawResource(R.raw.silent_wav), endingVoice.newOutputStream()); // Set the right permissions to files and folders final int uid = requireActivity().getPackageManager().getApplicationInfo(DIALER_ANDROID_PACKAGE_NAME, 0).uid; Shell.cmd( String.format("chown -R %s:%s %s", uid, uid, DIALER_CALLRECORDINGPROMPT), String.format("chmod 755 %s", DIALER_CALLRECORDINGPROMPT), String.format("chmod 444 %s/*", DIALER_CALLRECORDINGPROMPT), String.format("restorecon -R %s", DIALER_CALLRECORDINGPROMPT) ).exec(); } } catch (PackageManager.NameNotFoundException | IOException e) { e.printStackTrace(); } } else { // Delete callrecordingprompt folder ExtendedFile callRecordingPromptFolder = mCoreRootServiceFSManager.getFile(DIALER_CALLRECORDINGPROMPT); if (callRecordingPromptFolder.exists()) { //noinspection ResultOfMethodCallIgnored callRecordingPromptFolder.delete(); } } } private void dialerForceEnableCallScreen(boolean enableMod) { if (enableMod) { // Ask the user what language the Call Screen feature should use String[] supportedLanguages = {"en", "en-AU", "en-GB", "en-IN", "ja-JP", "fr-FR", "hi-IN", "de-DE", "it-IT", "es-ES"}; final int[] chosenLanguageIndex = {0}; new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.choose_a_language_for_call_screen) .setSingleChoiceItems(supportedLanguages, chosenLanguageIndex[0], (dialog, which) -> chosenLanguageIndex[0] = which) .setPositiveButton(android.R.string.ok, (dialog, which) -> { // Update boolean flags modSetBooleanFlags(true, DIALER_PHENOTYPE_PACKAGE_NAME, DIALER_ENABLE_CALL_SCREEN_FLAGS); // Override the call screen i18n config extension flag with the user desired language TelephonyManager telephonyManager = (TelephonyManager) requireActivity().getSystemService(Context.TELEPHONY_SERVICE); String simCountryIso = telephonyManager.getSimCountryIso(); String chosenLanguage = supportedLanguages[chosenLanguageIndex[0]]; Call_screen_i18n_config call_screen_i18n_config = Call_screen_i18n_config.newBuilder() .addCountryConfigs( Call_screen_i18n_config.CountryConfig.newBuilder() .setCountry(simCountryIso) .setLanguageConfig( Call_screen_i18n_config.LanguageConfig.newBuilder() .addLanguages( Call_screen_i18n_config.Language.newBuilder() .setLanguageCode(chosenLanguage) .setA6( Call_screen_i18n_config.A6.newBuilder() .setA7(ByteString.copyFrom(new byte[]{2})) ) ) ) ).build(); try { mCoreRootServiceIpc.phenotypeDBOverrideExtensionFlag(DIALER_PHENOTYPE_PACKAGE_NAME, DIALER_CALL_SCREEN_I18N_CONFIG_FLAG, call_screen_i18n_config.toByteArray()); } catch (RemoteException e) { e.printStackTrace(); } }) .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) .setOnCancelListener(dialog -> mBinding.dialerForceEnableCallScreen.getSwitch().setCheckedProgrammatically(false)) .show(); } else { // Delete flag overrides modDeleteFlagOverrides(DIALER_PHENOTYPE_PACKAGE_NAME, new ArrayList<>(DIALER_ENABLE_CALL_SCREEN_FLAGS.keySet())); modDeleteFlagOverrides(DIALER_PHENOTYPE_PACKAGE_NAME, Collections.singletonList(DIALER_CALL_SCREEN_I18N_CONFIG_FLAG)); } } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/FilterableSearchView.java ================================================ package com.jacopomii.gappsmod.ui.view; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import androidx.appcompat.view.CollapsibleActionView; import androidx.appcompat.widget.SearchView; import androidx.core.content.res.ResourcesCompat; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.FilterableSearchviewBinding; /** * A View, containing a {@link SearchView}, to which an additional View can optionally be connected * as a container to contain components for filtering the search, via the * {@link #setFilterContainer} method. When a filterContainer is set, a button appears next to the * SearchView that allows the user to manually show / hide the filterContainer. */ // Here I use the deprecated CollapsibleActionView interface because otherwise the // onActionViewExpanded and onActionViewCollapsed methods are never called, idk why @SuppressWarnings("deprecation") public class FilterableSearchView extends LinearLayout implements CollapsibleActionView { FilterableSearchviewBinding mBinding; private final Context mContext; private View mFilterContainer; private boolean mIsFilterContainerVisible; private boolean mFilterContainerAutoExpand; public FilterableSearchView(Context context) { super(context); mContext = context; init(); } public FilterableSearchView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } private void init() { mBinding = FilterableSearchviewBinding.inflate(LayoutInflater.from(mContext), this, true); mBinding.collapseFilterContainerButton.setOnClickListener(v -> { if (mFilterContainer != null) setFilterContainerVisibility(!mIsFilterContainerVisible); }); } /** * Connects a filter container to the SearchView, which can be shown / hidden by the user * using a special button. * * @param filterContainer the filter container to attach. * @param filterContainerAutoExpand whether the filter container should open itself when the * SearchView is expanded. */ public void setFilterContainer(View filterContainer, boolean filterContainerAutoExpand) { mFilterContainer = filterContainer; mFilterContainerAutoExpand = filterContainerAutoExpand; mBinding.collapseFilterContainerButton.setVisibility(VISIBLE); } /** * Sets the hint text to display in the query text field of the SearchView. * * @param hint the hint text to display or {@code null}. */ public void setQueryHint(CharSequence hint) { mBinding.searchView.setQueryHint(hint); } /** * Sets a listener for user actions within the SearchView. * * @param listener the listener object that receives callbacks when the user performs actions * in the SearchView such as clicking on buttons or typing a query. */ public void setOnQueryTextListener(SearchView.OnQueryTextListener listener) { mBinding.searchView.setOnQueryTextListener(listener); } @Override public void onActionViewExpanded() { if (mFilterContainer != null && mFilterContainerAutoExpand) setFilterContainerVisibility(true); mBinding.searchView.onActionViewExpanded(); } @Override public void onActionViewCollapsed() { if (mFilterContainer != null) setFilterContainerVisibility(false); mBinding.searchView.onActionViewCollapsed(); } private void setFilterContainerVisibility(boolean visible) { mIsFilterContainerVisible = visible; int newFilterContainerVisibility; int newCollapseFilterButtonDrawableID; if (visible) { newFilterContainerVisibility = View.VISIBLE; newCollapseFilterButtonDrawableID = R.drawable.ic_arrow_up_24; } else { newFilterContainerVisibility = View.GONE; newCollapseFilterButtonDrawableID = R.drawable.ic_arrow_down_24; } mFilterContainer.setVisibility(newFilterContainerVisibility); Drawable newCollapseFilterButtonDrawable = ResourcesCompat.getDrawable(getResources(), newCollapseFilterButtonDrawableID, null); mBinding.collapseFilterContainerButton.setImageDrawable(newCollapseFilterButtonDrawable); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/ProgrammaticMaterialSwitchView.java ================================================ package com.jacopomii.gappsmod.ui.view; import android.content.Context; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.materialswitch.MaterialSwitch; /** * A {@link MaterialSwitch} that allows to programmatically set the checked / unchecked state * without triggering the onCheckedChangeListener. */ public class ProgrammaticMaterialSwitchView extends MaterialSwitch { private OnCheckedChangeListener mOnCheckedChangeListener = null; public ProgrammaticMaterialSwitchView(@NonNull Context context) { super(context); } public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) { mOnCheckedChangeListener = listener; super.setOnCheckedChangeListener(listener); } /** * Programmatically change the checked state of the switch without calling any * onCheckedChangeListener. Please note that any previously set onCheckedChangeListener will be * preserved, even if this method does not call it. * * @param checked {@code true} to check the switch, {@code false} to uncheck it. */ public void setCheckedProgrammatically(boolean checked) { super.setOnCheckedChangeListener(null); super.setChecked(checked); super.setOnCheckedChangeListener(mOnCheckedChangeListener); } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/SuggestedModsAppHeaderView.java ================================================ package com.jacopomii.gappsmod.ui.view; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.google.android.material.button.MaterialButton; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.SuggestedModsAppHeaderBinding; /** * The application name header used by the "Suggested Mods" fragment. * It includes a large title for the app name and two localized buttons "Beta" and "Install". */ public class SuggestedModsAppHeaderView extends LinearLayout { final SuggestedModsAppHeaderBinding mBinding; public SuggestedModsAppHeaderView(Context context) { super(context); mBinding = SuggestedModsAppHeaderBinding.inflate(LayoutInflater.from(context), this, true); } public SuggestedModsAppHeaderView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mBinding = SuggestedModsAppHeaderBinding.inflate(LayoutInflater.from(context), this, true); final TypedArray xmlAttrs = context.obtainStyledAttributes(attrs, R.styleable.SuggestedModsAppHeaderView); final String appName = xmlAttrs.getString(R.styleable.SuggestedModsAppHeaderView_app_name); xmlAttrs.recycle(); mBinding.appName.setText(appName); } public MaterialButton getBetaButton() { return mBinding.betaButton; } public MaterialButton getInstallButton() { return mBinding.installButton; } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/SwitchCardView.java ================================================ package com.jacopomii.gappsmod.ui.view; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.databinding.SwitchCardBinding; /** * A card that contains a {@link android.widget.TextView} and a * {@link ProgrammaticMaterialSwitchView} on a single line. * The text will be rendered in a separate textview from the switch to prevent accidentally * clicking on the text from triggering the switch. */ public class SwitchCardView extends LinearLayout { final SwitchCardBinding mBinding; public SwitchCardView(Context context) { super(context); mBinding = SwitchCardBinding.inflate(LayoutInflater.from(context), this, true); } public SwitchCardView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mBinding = SwitchCardBinding.inflate(LayoutInflater.from(context), this, true); final TypedArray xmlAttrs = context.obtainStyledAttributes(attrs, R.styleable.SwitchCardView); final String text = xmlAttrs.getString(R.styleable.SwitchCardView_text); final boolean enabled = xmlAttrs.getBoolean(R.styleable.SwitchCardView_enabled, true); xmlAttrs.recycle(); mBinding.switchCardTextview.setText(text); mBinding.switchCardSwitch.setEnabled(enabled); } public ProgrammaticMaterialSwitchView getSwitch() { return mBinding.switchCardSwitch; } } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/util/OnItemClickListener.java ================================================ package com.jacopomii.gappsmod.util; /** * A generic interface to handle clicks on recyclerview rows. */ public interface OnItemClickListener { void onItemClick(Object item); } ================================================ FILE: app/src/main/java/com/jacopomii/gappsmod/util/Utils.java ================================================ package com.jacopomii.gappsmod.util; import static com.jacopomii.gappsmod.data.Constants.VENDING_ANDROID_PACKAGE_NAME; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.view.LayoutInflater; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.CheckBox; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.JsonObjectRequest; import com.android.volley.toolbox.RequestFuture; import com.android.volley.toolbox.Volley; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.jacopomii.gappsmod.BuildConfig; import com.jacopomii.gappsmod.ICoreRootService; import com.jacopomii.gappsmod.R; import com.jacopomii.gappsmod.data.Version; import com.jacopomii.gappsmod.databinding.DialogSelectPackageBinding; import com.jacopomii.gappsmod.ui.adapter.SelectPackageRecyclerViewAdapter; import com.l4digital.fastscroll.FastScrollRecyclerView; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class Utils { public static void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException { byte[] buffer = new byte[1024]; int read; while ((read = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, read); } inputStream.close(); outputStream.flush(); outputStream.close(); } public static void openGooglePlay(Context context, String googlePlayLink) { try { Intent appStoreIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(googlePlayLink)); appStoreIntent.setPackage(VENDING_ANDROID_PACKAGE_NAME); context.startActivity(appStoreIntent); } catch (ActivityNotFoundException exception) { context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(googlePlayLink))); } } public static boolean checkUpdateAvailable(Context context) { RequestQueue requestQueue = Volley.newRequestQueue(context); RequestFuture future = RequestFuture.newFuture(); requestQueue.add( new JsonObjectRequest( Request.Method.GET, context.getString(R.string.github_api_link) + "/releases/latest", null, future, future ) ); try { JSONObject response = future.get(); Version actualVersion = new Version(BuildConfig.VERSION_NAME); Version fetchedVersion = new Version(response.getString("tag_name").substring(1)); if (actualVersion.compareTo(fetchedVersion) < 0) return true; } catch (Exception e) { e.printStackTrace(); } return false; } /** * This method generates strings used for IN queries. * It creates string containing "?" characters repeated {@code size} times and separated by ",". * * @param size size of the items. * @return IN query string of the form ?,?,?,?. */ public static String createInQueryString(int size) { StringBuilder stringBuilder = new StringBuilder(); String separator = ""; for (int i = 0; i < size; i++) { stringBuilder.append(separator); stringBuilder.append("?"); separator = ","; } return stringBuilder.toString(); } /** * This method returns, given an {@code androidPackageName}, the label of the corresponding * application, or a localized string "Unknown" if the application is not installed. * * @param context context. * @param androidPackageName the Android package name of the application to get the label for. * @return the application label if the application exists; The localized string * {@link R.string#unknown} otherwise. */ public static String getApplicationLabelOrUnknown(Context context, String androidPackageName) { String applicationLabel = context.getString(R.string.unknown); try { PackageManager packageManager = context.getPackageManager(); ApplicationInfo applicationInfo = packageManager.getApplicationInfo(androidPackageName, 0); if (applicationInfo != null) applicationLabel = (String) (packageManager.getApplicationLabel(applicationInfo)); } catch (PackageManager.NameNotFoundException ignored) { } return applicationLabel; } // Static variables for showSelectPackageDialog() private static CharSequence lastPackageSearched = null; private static Boolean lastPackageSearchedRemember = true; /** * Show the "Select Package" dialog, a custom view to select package names contained in the * Phenotype DB with search and fastscroll features. * * @param context context. * @param coreRootServiceIpc a {@code ICoreRootService} instance. * @param onItemClickListener an implementation of the {@link OnItemClickListener} interface, * to perform actions after the user has selected a package. * The received item is a string containing the selected Phenotype * (not Android) package name. */ public static void showSelectPackageDialog(Context context, ICoreRootService coreRootServiceIpc, OnItemClickListener onItemClickListener, DialogInterface.OnDismissListener onDismissListener) { // Dialog builder MaterialAlertDialogBuilder selectPackageDialogBuilder = new MaterialAlertDialogBuilder(context); // Inflate dialog layout DialogSelectPackageBinding dialogSelectPackageBinding = DialogSelectPackageBinding.inflate(LayoutInflater.from(context)); selectPackageDialogBuilder.setView(dialogSelectPackageBinding.getRoot()); // Create dialog AlertDialog selectPackageDialog = selectPackageDialogBuilder.create(); // Set dialog custom height and width selectPackageDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); selectPackageDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); // Set dialog onDismissListener selectPackageDialog.setOnDismissListener(onDismissListener); // Dialog components SearchView selectPackageSearchView = dialogSelectPackageBinding.searchview; FastScrollRecyclerView selectPackageRecyclerView = dialogSelectPackageBinding.recyclerview; CheckBox SelectPackageRememberCheckbox = dialogSelectPackageBinding.remembercheckbox; // Initialize the dialog adapter SelectPackageRecyclerViewAdapter selectPackageRecyclerViewAdapter = new SelectPackageRecyclerViewAdapter(context, coreRootServiceIpc, item -> { // Pass the received item to the caller onItemClickListener onItemClickListener.onItemClick(item); // Dismiss dialog selectPackageDialog.dismiss(); }); // Disable fast scroll if the selectPackageRecyclerView is empty or changes to empty selectPackageRecyclerView.setFastScrollEnabled(selectPackageRecyclerViewAdapter.getItemCount() != 0); selectPackageRecyclerViewAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { super.onChanged(); selectPackageRecyclerView.setFastScrollEnabled(selectPackageRecyclerViewAdapter.getItemCount() != 0); } }); // Set the dialog selectPackageRecyclerView LayoutManager and Adapter selectPackageRecyclerView.setLayoutManager(new LinearLayoutManager(context)); selectPackageRecyclerView.setAdapter(selectPackageRecyclerViewAdapter); // Add list dividers to the selectPackageRecyclerView selectPackageRecyclerView.addItemDecoration(new DividerItemDecoration(selectPackageRecyclerView.getContext(), DividerItemDecoration.VERTICAL)); // Dialog filter selectPackageSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { lastPackageSearched = newText; selectPackageRecyclerViewAdapter.getFilter().filter(newText); return false; } }); // Remember last package searched SelectPackageRememberCheckbox.setChecked(lastPackageSearchedRemember); SelectPackageRememberCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { lastPackageSearchedRemember = isChecked; lastPackageSearched = selectPackageSearchView.getQuery(); }); if (lastPackageSearched != null && lastPackageSearchedRemember) selectPackageSearchView.setQuery(lastPackageSearched, true); // Show dialog selectPackageDialog.show(); } } ================================================ FILE: app/src/main/proto/call_screen_i18n.proto ================================================ syntax = "proto3"; package com.jacopomii.gappsmod.protos; option java_multiple_files = true; option java_package = "com.jacopomii.gappsmod.protos"; option java_outer_classname = "CallScreenI18nProtos"; /* Reversed from Dialer. Hex 0a140a026974120e0a0c0a0569742d495412030a0102 corresponds to the following JSON. { "countryConfigs":[{ "country": "it", "languageConfig": { "languages":[{ "languageCode":"it-IT", "a6":{ "a7":2 } }] } }] } */ message Call_screen_i18n_config { message A6 { bytes a7 = 1; } message Language { string languageCode = 1; A6 a6 = 2; } message LanguageConfig { repeated Language languages = 1; } message CountryConfig { string country = 1; LanguageConfig languageConfig = 2; } repeated CountryConfig countryConfigs = 1; } ================================================ FILE: app/src/main/res/drawable/ic_arrow_down_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_up_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_beta_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_error_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_fail_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_install_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_menu_search_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nav_drawer_boolean_mods_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nav_drawer_delete_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nav_drawer_information_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_nav_drawer_suggested_mods_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_success_24.xml ================================================ ================================================ FILE: app/src/main/res/layouts/activities/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layouts/activities/layout/activity_splash_screen.xml ================================================ ================================================ FILE: app/src/main/res/layouts/dialogs/layout/dialog_select_package.xml ================================================ ================================================ FILE: app/src/main/res/layouts/fragments/layout/fragment_boolean_mods.xml ================================================ ================================================ FILE: app/src/main/res/layouts/fragments/layout/fragment_information.xml ================================================ ================================================ FILE: app/src/main/res/layouts/fragments/layout/fragment_revert_mods.xml ================================================