main 6c63c56da89a cached
82 files
251.0 KB
59.4k tokens
139 symbols
1 requests
Download .txt
Showing preview only (276K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <issue id="UnsafeNativeCodeLocation">
        <ignore path="src/main/res/raw/sqlite3_arm" />
        <ignore path="src/main/res/raw/sqlite3_x86" />
    </issue>
</lint>

================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission
        android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />

    <application
        android:name=".application.GAppsModApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="DataExtractionRules"
        tools:replace="android:allowBackup">

        <activity
            android:name=".ui.activity.SplashScreenActivity"
            android:exported="true"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme"
            tools:ignore="LockedOrientationActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".ui.activity.MainActivity"
            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize"
            android:exported="true"
            android:theme="@style/AppTheme"
            android:windowSoftInputMode="adjustResize" />

    </application>

</manifest>

================================================
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.<br>
     * For performance reasons, the value of this HashMap is a {@code List} structured as follows:<br>
     * - 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.<br>
     * - 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<String> 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<String> 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<Version> {
    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<String, String> phenotypeDBGetAllPackageNames() {
            return CoreRootService.this.phenotypeDBGetAllPackageNames();
        }

        @Override
        public Map<String, String> phenotypeDBGetAllOverriddenPackageNames() {
            return CoreRootService.this.phenotypeDBGetAllOverriddenPackageNames();
        }

        @Override
        public String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(String phenotypePackageName) {
            return CoreRootService.this.phenotypeDBGetAndroidPackageNameByPhenotypePackageName(phenotypePackageName);
        }

        @Override
        public Map<String, List<Object>> phenotypeDBGetBooleanFlagsOrOverridden(String phenotypePackageName) {
            return CoreRootService.this.phenotypeDBGetBooleanFlagsOrOverridden(phenotypePackageName);
        }

        @Override
        public boolean phenotypeDBAreAllFlagsOverridden(String phenotypePackageName, List<String> 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<String> 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<String, String> phenotypeDBGetAllPackageNames() {
        HashMap<String, String> 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<String, String> phenotypeDBGetAllOverriddenPackageNames() {
        HashMap<String, String> 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<String, List<Object>> phenotypeDBGetBooleanFlagsOrOverridden(String phenotypePackageName) {
        Map<String, List<Object>> 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<String> flags) {
        boolean areAllFlagsOverridden = false;

        SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName);
        String sql = "SELECT DISTINCT name FROM FlagOverrides WHERE packageName=? AND name IN (" + createInQueryString(flags.size()) + ")";
        List<String> 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<String> 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<String> flags, boolean deletePackagePhenotypeCache) {
        SQLiteDatabase phenotypeDB = getPhenotypeDBByPhenotypePackageName(phenotypePackageName);
        String whereClause = "packageName=? AND name IN (" + createInQueryString(flags.size()) + ")";
        List<String> 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<BooleanModsRecyclerViewAdapter.ViewHolder> implements Filterable, FastScroller.SectionIndexer {
    private final Context mContext;

    private List<BooleanFlag> mFlagsList = new ArrayList<>();
    private List<BooleanFlag> 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<String, List<Object>> map = new TreeMap<String, List<Object>>(mCoreRootServiceIpc.phenotypeDBGetBooleanFlagsOrOverridden(phenotypePackageName));
            for (Map.Entry<String, List<Object>> flag : map.entrySet()) {
                String flagName = flag.getKey();
                List<Object> 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<BooleanFlag> 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<BooleanFlag>) 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<SelectPackageRecyclerViewAdapter.ViewHolder> implements Filterable, FastScroller.SectionIndexer {
    private final Context mContext;

    private List<PhenotypeDBPackageName> mPackageList = new ArrayList<>();
    private List<PhenotypeDBPackageName> 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<String, String> map = new TreeMap<String, String>(mCoreRootServiceIpc.phenotypeDBGetAllPackageNames());
            for (Map.Entry<String, String> 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<PhenotypeDBPackageName> 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<PhenotypeDBPackageName>) 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<String, Boolean> DIALER_ENABLE_CALL_RECORDING_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, String> DIALER_SILENCE_CALL_RECORDING_ALERTS_FLAGS = new HashMap<String, String>() {{
        // 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<String, Boolean> DIALER_ENABLE_CALL_SCREEN_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, Boolean> MESSAGES_ENABLE_DEBUG_MENU_FLAGS = new HashMap<String, Boolean>() {{
        put("bugle_phenotype__debug_menu_default_available", true);
    }};

    // The following boolean flag force-enables marking conversations as unread in Messages app
    private final HashMap<String, Boolean> MESSAGES_ENABLE_MARKING_CONVERSATIONS_UNREAD_FLAGS = new HashMap<String, Boolean>() {{
        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<String, Boolean> MESSAGES_ENABLE_MESSAGE_ORGANIZATION_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, Boolean> MESSAGES_ENABLE_VERIFIED_SMS_FLAGS = new HashMap<String, Boolean>() {{
        put("bugle_phenotype__enabled_verified_sms", true);
    }};

    // The following boolean flag force-enables sending images via GPhotos links in Messages app
    private final HashMap<String, Boolean> MESSAGES_ENABLE_IMAGES_VIA_GPHOTOS_FLAGS = new HashMap<String, Boolean>() {{
        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<String, Boolean> MESSAGES_ENABLE_NUDGES_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, Boolean> MESSAGES_ENABLE_SPOTLIGHTS_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, Boolean> MESSAGES_ENABLE_SMART_COMPOSE_FLAGS = new HashMap<String, Boolean>() {{
        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<String, Boolean> MESSAGES_ENABLE_MAGIC_COMPOSE_FLAGS = new HashMap<String, Boolean>() {{
        // 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<String, Boolean> MESSAGES_ENABLE_SMART_ACTIONS_IN_NOTIFICATIONS_FLAGS = new HashMap<String, Boolean>() {{
        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<String, Boolean> MESSAGES_ENABLE_SUGGESTED_STICKERS_FLAGS = new HashMap<String, Boolean>() {{
        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<String> flags) {
        try {
            return mCoreRootServiceIpc.phenotypeDBAreAllFlagsOverridden(phenotypePackageName, flags);
        } catch (RemoteException e) {
            e.printStackTrace();
            return false;
        }
    }

    private void modSetBooleanFlags(boolean enableMod, String phenotypePackageName, HashMap<String, Boolean> flags) {
        if (enableMod) {
            for (Map.Entry<String, Boolean> 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<String, String> flags) {
        if (enableMod) {
            for (Map.Entry<String, String> 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<String> 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<JSONObject> 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
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_arrow_up_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_beta_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M19.8,18.4L14,10.67V6.5l1.35,-1.69C15.61,4.48 15.38,4 14.96,4H9.04C8.62,4 8.39,4.48 8.65,4.81L10,6.5v4.17L4.2,18.4C3.71,19.06 4.18,20 5,20h14C19.82,20 20.29,19.06 19.8,18.4z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_error_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_fail_24.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="#f44336" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
    <path android:fillColor="@android:color/white" android:pathData="M18.3,5.71L18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59L7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41L10.59,12L5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41l0,0c0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0l0,0c0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89C18.68,6.73 18.68,6.09 18.3,5.71z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_install_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_menu_search_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_nav_drawer_boolean_mods_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M17,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h10c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM17,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_nav_drawer_delete_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_nav_drawer_information_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_nav_drawer_suggested_mods_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M11,21h-1l1,-7H7.5c-0.58,0 -0.57,-0.32 -0.38,-0.66 0.19,-0.34 0.05,-0.08 0.07,-0.12C8.48,10.94 10.42,7.54 13,3h1l-1,7h3.5c0.49,0 0.56,0.33 0.47,0.51l-0.07,0.15C12.96,17.55 11,21 11,21z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_save_24.xml
================================================
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_success_24.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="@android:color/holo_green_dark"
        android:pathData="M18,6.7l-8.48,8.48l-3.54,-3.54c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41l4.24,4.24c0.39,0.39 1.02,0.39 1.41,0l9.18,-9.18c0.39,-0.39 0.39,-1.03 -0.01,-1.42l0,0C19.02,6.31 18.39,6.31 18,6.7z" />
</vector>


================================================
FILE: app/src/main/res/layouts/activities/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".ui.activity.MainActivity">

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:liftOnScroll="false">

            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize" />

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            app:navGraph="@navigation/mobile_navigation" />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:drawerLayoutCornerSize="0dp"
        app:headerLayout="@layout/nav_drawer_header"
        app:menu="@menu/nav_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>

================================================
FILE: app/src/main/res/layouts/activities/layout/activity_splash_screen.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true"
    android:animationCache="true"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="@dimen/l1">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/l2"
        android:gravity="center"
        android:orientation="horizontal"
        tools:ignore="UseCompoundDrawables">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="70dp"
            android:importantForAccessibility="no"
            app:srcCompat="@mipmap/ic_launcher" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/l1"
            android:fontFamily="sans-serif-condensed-medium"
            android:text="@string/app_name"
            android:textSize="34sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l_50"
            android:animateLayoutChanges="true"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/done_root"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:importantForAccessibility="no"
                android:visibility="gone" />

            <com.google.android.material.progressindicator.CircularProgressIndicator
                android:id="@+id/circular_root"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                app:indicatorSize="24dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="@dimen/l_50"
                android:text="@string/splash_screen_root"
                android:textAppearance="?attr/textAppearanceBody1" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l_50"
            android:animateLayoutChanges="true"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/done_root_service"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:importantForAccessibility="no"
                android:visibility="gone" />

            <com.google.android.material.progressindicator.CircularProgressIndicator
                android:id="@+id/circular_root_service"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                app:indicatorSize="24dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="@dimen/l_50"
                android:text="@string/splash_screen_root_service"
                android:textAppearance="?attr/textAppearanceBody1" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l_50"
            android:animateLayoutChanges="true"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/done_phenotype_gms"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:importantForAccessibility="no"
                android:visibility="gone" />

            <com.google.android.material.progressindicator.CircularProgressIndicator
                android:id="@+id/circular_phenotype_gms"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                app:indicatorSize="24dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="@dimen/l_50"
                android:text="@string/splash_screen_phenotype_gms"
                android:textAppearance="?attr/textAppearanceBody1" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l_50"
            android:animateLayoutChanges="true"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/done_updates"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:importantForAccessibility="no"
                android:visibility="gone" />

            <com.google.android.material.progressindicator.CircularProgressIndicator
                android:id="@+id/circular_updates"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                app:indicatorSize="24dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="@dimen/l_50"
                android:text="@string/splash_screen_updates"
                android:textAppearance="?attr/textAppearanceBody1" />
        </LinearLayout>

    </LinearLayout>

</LinearLayout>

================================================
FILE: app/src/main/res/layouts/dialogs/layout/dialog_select_package.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingHorizontal="@dimen/l_50"
    android:paddingVertical="@dimen/l1">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/l_50"
        android:text="@string/select_package"
        android:textAlignment="viewStart"
        android:textAppearance="?attr/textAppearanceTitleLarge" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical">

        <androidx.appcompat.widget.SearchView
            android:id="@+id/searchview"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l_50"
            android:layout_weight="1"
            app:iconifiedByDefault="false"
            app:queryHint="@string/search_by_package_or_app"
            app:searchIcon="@null" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical">

            <CheckBox
                android:id="@+id/remembercheckbox"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:minWidth="0dp"
                tools:ignore="TouchTargetSizeCheck" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="@string/remember"
                app:srcCompat="@drawable/ic_save_24"
                app:tint="?attr/colorControlNormal" />

        </LinearLayout>

    </LinearLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.l4digital.fastscroll.FastScrollRecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:bubbleColor="?attr/colorSecondary"
            app:bubbleSize="small"
            app:bubbleTextColor="?attr/colorOnSecondary"
            app:bubbleTextSize="15sp"
            app:handleColor="?attr/colorSecondary"
            app:hideScrollbar="false"
            app:trackColor="?attr/colorSecondary" />
    </FrameLayout>

</LinearLayout>

================================================
FILE: app/src/main/res/layouts/fragments/layout/fragment_boolean_mods.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.fragment.BooleanModsFragment">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:liftOnScroll="false">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="?android:dividerHorizontal"
            android:orientation="vertical"
            android:showDividers="middle">

            <LinearLayout
                android:id="@+id/filter_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:paddingHorizontal="@dimen/l1"
                android:paddingVertical="@dimen/l_50"
                android:visibility="gone"
                tools:visibility="visible">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/l_50"
                    android:text="@string/additional_filters" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:baselineAligned="false"
                    android:orientation="horizontal">

                    <com.google.android.material.textfield.TextInputLayout
                        style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_marginEnd="@dimen/l_50"
                        android:layout_weight="1"
                        android:hint="@string/enabled_status_filter">

                        <AutoCompleteTextView
                            android:id="@+id/filter_enabled_status_spinner"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:ellipsize="end"
                            android:inputType="none"
                            android:singleLine="true"
                            tools:ignore="LabelFor,TextContrastCheck,VisualLintTextFieldSize" />

                    </com.google.android.material.textfield.TextInputLayout>

                    <com.google.android.material.textfield.TextInputLayout
                        style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="@dimen/l_50"
                        android:layout_weight="1"
                        android:hint="@string/changed_status_filter">

                        <AutoCompleteTextView
                            android:id="@+id/filter_changed_status_spinner"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:ellipsize="end"
                            android:inputType="none"
                            android:singleLine="true"
                            tools:ignore="LabelFor,TextContrastCheck,VisualLintTextFieldSize" />

                    </com.google.android.material.textfield.TextInputLayout>

                </LinearLayout>

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginVertical="@dimen/l_50"
                android:gravity="center_vertical"
                android:paddingHorizontal="@dimen/l1">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="@dimen/l_50"
                    android:text="@string/package_spinner_label" />

                <TextView
                    android:id="@+id/select_package"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical"
                    android:hint="@string/select_package"
                    app:drawableEndCompat="@drawable/ic_arrow_down_24"
                    app:drawableTint="?attr/colorControlNormal"
                    tools:ignore="TextContrastCheck" />

            </LinearLayout>

        </LinearLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.l4digital.fastscroll.FastScrollRecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:fitsSystemWindows="true"
            app:bubbleColor="?attr/colorSecondary"
            app:bubbleTextColor="?attr/colorOnSecondary"
            app:handleColor="?attr/colorSecondary"
            app:hideScrollbar="false"
            app:trackColor="?attr/colorSecondary" />
    </FrameLayout>

</LinearLayout>

================================================
FILE: app/src/main/res/layouts/fragments/layout/fragment_information.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:fitsSystemWindows="true"
    tools:context=".ui.fragment.SuggestedModsFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/margin_generic">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/l1"
            android:text="@string/what_is_app_name"
            android:textAlignment="viewStart"
            android:textAppearance="?attr/textAppearanceTitleLarge" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:justificationMode="inter_word"
            android:paddingBottom="@dimen/l1"
            android:text="@string/what_is_it_explanation"
            android:textAlignment="viewStart"
            tools:ignore="VisualLintLongText"
            tools:targetApi="o" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/l1"
            android:text="@string/how_it_works"
            android:textAlignment="viewStart"
            android:textAppearance="?attr/textAppearanceTitleLarge" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:justificationMode="inter_word"
            android:paddingBottom="@dimen/l1"
            android:text="@string/how_it_works_explanation"
            android:textAlignment="viewStart"
            tools:ignore="VisualLintLongText"
            tools:targetApi="o" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/l1"
            android:text="@string/credits"
            android:textAlignment="viewStart"
            android:textAppearance="?attr/textAppearanceTitleLarge" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:justificationMode="inter_word"
            android:paddingBottom="@dimen/l1"
            android:text="@string/if_bug_found_open_github_issue"
            android:textAlignment="viewStart"
            tools:targetApi="o" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:autoLink="web"
            android:paddingBottom="@dimen/l_75"
            android:text="@string/github_link"
            android:textAlignment="viewStart"
            tools:ignore="TouchTargetSizeCheck" />

        <TextView
            android:id="@+id/made_with_love_by_jacopo_tediosi"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/made_with_love_by"
            android:textAlignment="viewStart" />

    </LinearLayout>
</ScrollView>

================================================
FILE: app/src/main/res/layouts/fragments/layout/fragment_revert_mods.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:fitsSystemWindows="true"
    tools:context=".ui.fragment.RevertModsFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/margin_generic">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:justificationMode="inter_word"
            android:text="@string/revert_mods_for_the_selected_package_explanation"
            android:textAlignment="viewStart"
            tools:targetApi="o" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l1_5"
            android:gravity="center_vertical">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/l_50"
                android:text="@string/package_spinner_label" />

            <TextView
                android:id="@+id/select_package"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:hint="@string/select_package"
                app:drawableEndCompat="@drawable/ic_arrow_down_24"
                app:drawableTint="?attr/colorControlNormal"
                tools:ignore="TextContrastCheck" />

        </LinearLayout>

        <Button
            android:id="@+id/revert_mods_selected_package_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/l1_5"
            android:backgroundTint="?attr/colorError"
            android:enabled="false"
            android:text="@string/revert_mods_for_the_selected_package"
            android:textColor="?attr/colorOnError"
            tools:ignore="VisualLintButtonSize" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l2"
            android:text="@string/or"
            android:textAlignment="center"
            android:textAllCaps="true"
            android:textStyle="bold" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="@dimen/l1_5"
            android:justificationMode="inter_word"
            android:text="@string/revert_mods_for_all_packages_explanation"
            android:textAlignment="viewStart"
            tools:targetApi="o" />

        <Button
            android:id="@+id/revert_all_mods_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="?attr/colorError"
            android:text="@string/revert_mods_for_all_packages"
            android:textColor="?attr/colorOnError"
            tools:ignore="VisualLintButtonSize" />
    </LinearLayout>

</ScrollView>

================================================
FILE: app/src/main/res/layouts/fragments/layout/fragment_suggested_mods.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:fitsSystemWindows="true"
    tools:context=".ui.fragment.SuggestedModsFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/margin_generic">

        <com.jacopomii.gappsmod.ui.view.SuggestedModsAppHeaderView
            android:id="@+id/dialer_app_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:app_name="@string/dialer_app_name" />

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/dialer_not_installed_alert"
            style="?attr/materialCardViewElevatedStyle"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="@dimen/l_25"
            android:layout_marginBottom="@dimen/l_50"
            android:visibility="gone"
            app:cardBackgroundColor="?attr/colorErrorContainer"
            app:cardElevation="4dp"
            app:contentPadding="@dimen/l1"
            tools:visibility="visible">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:justificationMode="inter_word"
                android:text="@string/app_not_installed_error"
                android:textAlignment="viewStart"
                android:textColor="?attr/colorOnErrorContainer"
                tools:targetApi="o" />
        </com.google.android.material.card.MaterialCardView>

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/dialer_permission_alert"
            style="?attr/materialCardViewElevatedStyle"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="@dimen/l_25"
            android:layout_marginBottom="@dimen/l_50"
            android:visibility="gone"
            app:cardBackgroundColor="?attr/colorErrorContainer"
            app:cardElevation="4dp"
            app:contentPadding="@dimen/l1"
            tools:visibility="visible">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:justificationMode="inter_word"
                android:text="@string/dialer_permission_alert"
                android:textAlignment="viewStart"
                android:textColor="?attr/colorOnErrorContainer"
                tools:targetApi="o" />
        </com.google.android.material.card.MaterialCardView>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <com.jacopomii.gappsmod.ui.view.SwitchCardView
                android:id="@+id/dialer_force_enable_call_recording"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:enabled="false"
                app:text="@string/force_enable_call_recording" />

            <com.jacopomii.gappsmod.ui.view.SwitchCardView
                android:id="@+id/dialer_silence_call_recording_alerts"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:enabled="false"
                app:text="@string/silence_call_recording_alerts" />

            <com.jacopomii.gappsmod.ui.view.SwitchCardView
                android:id="@+id/dialer_force_enable_call_screen"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:enabled="false"
                app:text="@string/force_enable_call_screen" />

        </LinearLayout>

        <com.jacopomii.gappsmod.ui.view.SuggestedModsAppHeaderView
            android:id="@+id/messages_app_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="@dimen/l1"
            app:app_name="@string/messages_app_name" />

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/messages_not_installed_alert"
            style="?attr/materialCardViewElevatedStyle"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="@dimen/l_25"
            android:layout_marginBottom="@dimen/l_50"
            android:visibility="gone"
            app:cardBackgroundColor="?attr/colorErrorContainer"
            app:cardElevation="4dp"
            app:contentPadding="@dimen/l1"
            tools:visibility="visible">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:justificationMode="inter_word"
                    android:text="@string/app_not_installed_error"
                    android:textAlignment="viewStart"
                    android:textColor="?attr/colorOnErrorContainer"
                    tools:targetApi="o" />
            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <com.jacopomii
Download .txt
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
Download .txt
SYMBOL INDEX (139 symbols across 20 files)

FILE: app/src/main/java/com/jacopomii/gappsmod/application/GAppsModApplication.java
  class GAppsModApplication (line 7) | public class GAppsModApplication extends Application {
    method onCreate (line 8) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/data/BooleanFlag.java
  class BooleanFlag (line 3) | public class BooleanFlag {
    method BooleanFlag (line 8) | public BooleanFlag(String flagName, boolean flagValue, boolean flagOve...
    method getFlagName (line 14) | public String getFlagName() {
    method getFlagValue (line 18) | public boolean getFlagValue() {
    method setFlagValue (line 22) | public void setFlagValue(boolean flagValue) {
    method setFlagOverriddenAndChanged (line 26) | public void setFlagOverriddenAndChanged(boolean flagOverriddenAndChang...
    method getFlagOverriddenAndChanged (line 30) | public boolean getFlagOverriddenAndChanged() {

FILE: app/src/main/java/com/jacopomii/gappsmod/data/Constants.java
  type Constants (line 5) | @SuppressLint("SdCardPath")

FILE: app/src/main/java/com/jacopomii/gappsmod/data/PhenotypeDBPackageName.java
  class PhenotypeDBPackageName (line 3) | public class PhenotypeDBPackageName {
    method PhenotypeDBPackageName (line 7) | public PhenotypeDBPackageName(String phenotypePackageName, String andr...
    method getPhenotypePackageName (line 12) | public String getPhenotypePackageName() {
    method getAndroidPackageName (line 16) | public String getAndroidPackageName() {

FILE: app/src/main/java/com/jacopomii/gappsmod/data/Version.java
  class Version (line 3) | public class Version implements Comparable<Version> {
    method Version (line 6) | public Version(String version) {
    method getVersion (line 14) | public final String getVersion() {
    method compareTo (line 18) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/service/CoreRootService.java
  class CoreRootService (line 37) | @SuppressWarnings("SameParameterValue")
    method onBind (line 50) | @Override
    method onUnbind (line 67) | @Override
    method onDestroy (line 75) | @Override
    class CoreRootServiceIPC (line 82) | private class CoreRootServiceIPC extends ICoreRootService.Stub {
      method getFileSystemService (line 83) | @Override
      method phenotypeDBGetAllPackageNames (line 88) | @Override
      method phenotypeDBGetAllOverriddenPackageNames (line 93) | @Override
      method phenotypeDBGetAndroidPackageNameByPhenotypePackageName (line 98) | @Override
      method phenotypeDBGetBooleanFlagsOrOverridden (line 103) | @Override
      method phenotypeDBAreAllFlagsOverridden (line 108) | @Override
      method phenotypeDBDeleteAllFlagOverrides (line 113) | @Override
      method phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName (line 118) | @Override
      method phenotypeDBDeleteFlagOverrides (line 123) | @Override
      method phenotypeDBOverrideBooleanFlag (line 128) | @Override
      method phenotypeDBOverrideExtensionFlag (line 133) | @Override
      method phenotypeDBOverrideStringFlag (line 138) | @Override
    method phenotypeDBGetAllPackageNames (line 145) | private Map<String, String> phenotypeDBGetAllPackageNames() {
    method phenotypeDBGetAllOverriddenPackageNames (line 175) | private Map<String, String> phenotypeDBGetAllOverriddenPackageNames() {
    method phenotypeDBGetAndroidPackageNameByPhenotypePackageName (line 205) | private String phenotypeDBGetAndroidPackageNameByPhenotypePackageName(...
    method phenotypeDBGetBooleanFlagsOrOverridden (line 221) | private Map<String, List<Object>> phenotypeDBGetBooleanFlagsOrOverridd...
    method phenotypeDBAreAllFlagsOverridden (line 252) | private boolean phenotypeDBAreAllFlagsOverridden(String phenotypePacka...
    method phenotypeDBDeleteAllFlagOverrides (line 272) | private void phenotypeDBDeleteAllFlagOverrides(boolean deletePackagePh...
    method phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName (line 294) | private void phenotypeDBDeleteAllFlagOverridesByPhenotypePackageName(S...
    method phenotypeDBDeleteFlagOverrides (line 307) | private void phenotypeDBDeleteFlagOverrides(String phenotypePackageNam...
    method phenotypeDBOverrideBooleanFlag (line 324) | private void phenotypeDBOverrideBooleanFlag(String phenotypePackageNam...
    method phenotypeDBOverrideExtensionFlag (line 352) | private void phenotypeDBOverrideExtensionFlag(String phenotypePackageN...
    method phenotypeDBOverrideStringFlag (line 380) | private void phenotypeDBOverrideStringFlag(String phenotypePackageName...
    method getPhenotypeDBByPhenotypePackageName (line 416) | private SQLiteDatabase getPhenotypeDBByPhenotypePackageName(String phe...
    method killPackageAndDeletePhenotypeCache (line 431) | private void killPackageAndDeletePhenotypeCache(String phenotypePackag...

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/activity/MainActivity.java
  class MainActivity (line 29) | public class MainActivity extends AppCompatActivity {
    method onCreate (line 38) | @Override
    method getCoreRootServiceFSManager (line 109) | public FileSystemManager getCoreRootServiceFSManager() {
    method getCoreRootServiceIpc (line 113) | public ICoreRootService getCoreRootServiceIpc() {
    method onSupportNavigateUp (line 117) | @Override
    method onBackPressed (line 124) | @Override
    method onDestroy (line 132) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/activity/SplashScreenActivity.java
  class SplashScreenActivity (line 35) | @SuppressLint("CustomSplashScreen")
    method onCreate (line 54) | @Override
    method checkRoot (line 192) | private boolean checkRoot() {
    method checkGMSPhenotypeDB (line 196) | private boolean checkGMSPhenotypeDB() {
    method setCheckUIDone (line 200) | private void setCheckUIDone(int circularID, int doneImageID, boolean s...
    method onDestroy (line 210) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/adapter/BooleanModsRecyclerViewAdapter.java
  class BooleanModsRecyclerViewAdapter (line 33) | @SuppressWarnings("unchecked")
    method BooleanModsRecyclerViewAdapter (line 44) | public BooleanModsRecyclerViewAdapter(Context context, ICoreRootServic...
    method selectPhenotypePackageName (line 49) | @SuppressLint("NotifyDataSetChanged")
    method onCreateViewHolder (line 75) | @NonNull
    method onBindViewHolder (line 103) | @Override
    method getItemCount (line 123) | @Override
    method getFilter (line 128) | @Override
    method getSectionText (line 172) | @Override
    class ViewHolder (line 177) | public static class ViewHolder extends RecyclerView.ViewHolder {
      method ViewHolder (line 181) | public ViewHolder(SwitchCardBinding binding) {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/adapter/SelectPackageRecyclerViewAdapter.java
  class SelectPackageRecyclerViewAdapter (line 38) | @SuppressWarnings("unchecked")
    method SelectPackageRecyclerViewAdapter (line 49) | @SuppressLint("NotifyDataSetChanged")
    method onCreateViewHolder (line 69) | @NonNull
    method onBindViewHolder (line 86) | @Override
    method getItemCount (line 113) | @Override
    method getFilter (line 118) | @Override
    method getSectionText (line 149) | @Override
    class ViewHolder (line 176) | public static class ViewHolder extends RecyclerView.ViewHolder {
      method ViewHolder (line 182) | public ViewHolder(PackageRowBinding binding) {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/BooleanModsFragment.java
  class BooleanModsFragment (line 39) | public class BooleanModsFragment extends Fragment {
    method BooleanModsFragment (line 52) | public BooleanModsFragment() {
    method onCreate (line 55) | @Override
    method onCreateView (line 66) | @Override
    method setupMenu (line 155) | private void setupMenu() {
    method resetFlagsFilters (line 230) | private void resetFlagsFilters() {
    method applyFlagsFilters (line 240) | private void applyFlagsFilters() {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/InformationFragment.java
  class InformationFragment (line 14) | public class InformationFragment extends Fragment {
    method InformationFragment (line 17) | public InformationFragment() {}
    method onCreate (line 19) | @Override
    method onCreateView (line 24) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/RevertModsFragment.java
  class RevertModsFragment (line 31) | public class RevertModsFragment extends Fragment {
    method RevertModsFragment (line 37) | public RevertModsFragment() {
    method onCreate (line 40) | @Override
    method onCreateView (line 53) | @Override

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/fragment/SuggestedModsFragment.java
  class SuggestedModsFragment (line 49) | @SuppressWarnings("FieldCanBeLocal")
    method SuggestedModsFragment (line 219) | public SuggestedModsFragment() {
    method onCreate (line 222) | @Override
    method onCreateView (line 235) | @Override
    method modCheckAreAllFlagsOverridden (line 407) | private boolean modCheckAreAllFlagsOverridden(String phenotypePackageN...
    method modSetBooleanFlags (line 416) | private void modSetBooleanFlags(boolean enableMod, String phenotypePac...
    method modSetStringFlags (line 430) | private void modSetStringFlags(boolean enableMod, String phenotypePack...
    method modDeleteFlagOverrides (line 444) | private void modDeleteFlagOverrides(String phenotypePackageName, List<...
    method dialerSilenceCallRecordingAlerts (line 452) | private void dialerSilenceCallRecordingAlerts(boolean enableMod) {
    method dialerForceEnableCallScreen (line 490) | private void dialerForceEnableCallScreen(boolean enableMod) {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/FilterableSearchView.java
  class FilterableSearchView (line 25) | @SuppressWarnings("deprecation")
    method FilterableSearchView (line 34) | public FilterableSearchView(Context context) {
    method FilterableSearchView (line 40) | public FilterableSearchView(Context context, AttributeSet attrs) {
    method init (line 46) | private void init() {
    method setFilterContainer (line 61) | public void setFilterContainer(View filterContainer, boolean filterCon...
    method setQueryHint (line 72) | public void setQueryHint(CharSequence hint) {
    method setOnQueryTextListener (line 82) | public void setOnQueryTextListener(SearchView.OnQueryTextListener list...
    method onActionViewExpanded (line 86) | @Override
    method onActionViewCollapsed (line 93) | @Override
    method setFilterContainerVisibility (line 99) | private void setFilterContainerVisibility(boolean visible) {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/ProgrammaticMaterialSwitchView.java
  class ProgrammaticMaterialSwitchView (line 15) | public class ProgrammaticMaterialSwitchView extends MaterialSwitch {
    method ProgrammaticMaterialSwitchView (line 18) | public ProgrammaticMaterialSwitchView(@NonNull Context context) {
    method ProgrammaticMaterialSwitchView (line 22) | public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nulla...
    method ProgrammaticMaterialSwitchView (line 26) | public ProgrammaticMaterialSwitchView(@NonNull Context context, @Nulla...
    method setOnCheckedChangeListener (line 30) | @Override
    method setCheckedProgrammatically (line 43) | public void setCheckedProgrammatically(boolean checked) {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/SuggestedModsAppHeaderView.java
  class SuggestedModsAppHeaderView (line 19) | public class SuggestedModsAppHeaderView extends LinearLayout {
    method SuggestedModsAppHeaderView (line 22) | public SuggestedModsAppHeaderView(Context context) {
    method SuggestedModsAppHeaderView (line 28) | public SuggestedModsAppHeaderView(Context context, @Nullable Attribute...
    method getBetaButton (line 40) | public MaterialButton getBetaButton() {
    method getInstallButton (line 44) | public MaterialButton getInstallButton() {

FILE: app/src/main/java/com/jacopomii/gappsmod/ui/view/SwitchCardView.java
  class SwitchCardView (line 20) | public class SwitchCardView extends LinearLayout {
    method SwitchCardView (line 23) | public SwitchCardView(Context context) {
    method SwitchCardView (line 29) | public SwitchCardView(Context context, @Nullable AttributeSet attrs) {
    method getSwitch (line 43) | public ProgrammaticMaterialSwitchView getSwitch() {

FILE: app/src/main/java/com/jacopomii/gappsmod/util/OnItemClickListener.java
  type OnItemClickListener (line 6) | public interface OnItemClickListener {
    method onItemClick (line 7) | void onItemClick(Object item);

FILE: app/src/main/java/com/jacopomii/gappsmod/util/Utils.java
  class Utils (line 43) | public class Utils {
    method copyFile (line 44) | public static void copyFile(InputStream inputStream, OutputStream outp...
    method openGooglePlay (line 55) | public static void openGooglePlay(Context context, String googlePlayLi...
    method checkUpdateAvailable (line 65) | public static boolean checkUpdateAvailable(Context context) {
    method createInQueryString (line 99) | public static String createInQueryString(int size) {
    method getApplicationLabelOrUnknown (line 119) | public static String getApplicationLabelOrUnknown(Context context, Str...
    method showSelectPackageDialog (line 148) | public static void showSelectPackageDialog(Context context, ICoreRootS...
Condensed preview — 82 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (273K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 62,
    "preview": "github: jacopotediosi\ncustom: https://paypal.me/jacopotediosi\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 1775,
    "preview": "---\nname: Bug Report\nabout: Report something that isn't working\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n## Overview\n[N"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "chars": 600,
    "preview": "---\nname: Feature Request\nabout: Suggest an idea for a new feature you want\ntitle: ''\nlabels: enhancement\nassignees: ''\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 434,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/push.yml",
    "chars": 2067,
    "preview": "name: Assemble on push\n\non:\n  push:\n    branches: [ main ]\n    paths-ignore:\n      - '.github/**'\n      - '**.md'\n  work"
  },
  {
    "path": ".gitignore",
    "chars": 54,
    "preview": "*.iml\n.gradle\n.idea\n.DS_Store\n/build\nlocal.properties\n"
  },
  {
    "path": "README.md",
    "chars": 5413,
    "preview": "# Deprecation notice\n## !!! This project is no longer maintained !!!\n\nI had started researching how Phenotype DB works a"
  },
  {
    "path": "app/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "app/build.gradle",
    "chars": 3741,
    "preview": "plugins {\n    id \"com.android.application\"\n    id \"com.google.protobuf\"\n    id \"com.likethesalad.stem\"\n}\n\ndef keyfile\nde"
  },
  {
    "path": "app/lint.xml",
    "chars": 218,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n    <issue id=\"UnsafeNativeCodeLocation\">\n        <ignore path=\"src/main/r"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 971,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1545,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/aidl/com/jacopomii/gappsmod/ICoreRootService.aidl",
    "chars": 6633,
    "preview": "package com.jacopomii.gappsmod;\n\ninterface ICoreRootService {\n    IBinder getFileSystemService();\n\n    /**\n    * Query t"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/application/GAppsModApplication.java",
    "chars": 324,
    "preview": "package com.jacopomii.gappsmod.application;\n\nimport android.app.Application;\n\nimport com.google.android.material.color.D"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/data/BooleanFlag.java",
    "chars": 883,
    "preview": "package com.jacopomii.gappsmod.data;\n\npublic class BooleanFlag {\n    private final String mFlagName;\n    private boolean"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/data/Constants.java",
    "chars": 1262,
    "preview": "package com.jacopomii.gappsmod.data;\n\nimport android.annotation.SuppressLint;\n\n@SuppressLint(\"SdCardPath\")\npublic interf"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/data/PhenotypeDBPackageName.java",
    "chars": 553,
    "preview": "package com.jacopomii.gappsmod.data;\n\npublic class PhenotypeDBPackageName {\n    private final String mPhenotypePackageNa"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/data/Version.java",
    "chars": 1203,
    "preview": "package com.jacopomii.gappsmod.data;\n\npublic class Version implements Comparable<Version> {\n    private final String mVe"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/service/CoreRootService.java",
    "chars": 19689,
    "preview": "package com.jacopomii.gappsmod.service;\n\nimport static com.jacopomii.gappsmod.data.Constants.DATA_DATA_PREFIX;\nimport st"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/activity/MainActivity.java",
    "chars": 6223,
    "preview": "package com.jacopomii.gappsmod.ui.activity;\n\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/activity/SplashScreenActivity.java",
    "chars": 9510,
    "preview": "package com.jacopomii.gappsmod.ui.activity;\n\nimport static com.jacopomii.gappsmod.data.Constants.GMS_ANDROID_PACKAGE_NAM"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/adapter/BooleanModsRecyclerViewAdapter.java",
    "chars": 7913,
    "preview": "package com.jacopomii.gappsmod.ui.adapter;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimpo"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/adapter/SelectPackageRecyclerViewAdapter.java",
    "chars": 7719,
    "preview": "package com.jacopomii.gappsmod.ui.adapter;\n\nimport static com.jacopomii.gappsmod.util.Utils.getApplicationLabelOrUnknown"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/fragment/BooleanModsFragment.java",
    "chars": 10937,
    "preview": "package com.jacopomii.gappsmod.ui.fragment;\n\nimport static com.jacopomii.gappsmod.util.Utils.showSelectPackageDialog;\n\ni"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/fragment/InformationFragment.java",
    "chars": 999,
    "preview": "package com.jacopomii.gappsmod.ui.fragment;\n\nimport android.os.Bundle;\nimport android.text.method.LinkMovementMethod;\nim"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/fragment/RevertModsFragment.java",
    "chars": 7534,
    "preview": "package com.jacopomii.gappsmod.ui.fragment;\n\nimport static com.jacopomii.gappsmod.data.Constants.DIALER_CALLRECORDINGPRO"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/fragment/SuggestedModsFragment.java",
    "chars": 36986,
    "preview": "package com.jacopomii.gappsmod.ui.fragment;\n\nimport static android.Manifest.permission.CAPTURE_AUDIO_OUTPUT;\nimport stat"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/view/FilterableSearchView.java",
    "chars": 4564,
    "preview": "package com.jacopomii.gappsmod.ui.view;\n\nimport android.content.Context;\nimport android.graphics.drawable.Drawable;\nimpo"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/view/ProgrammaticMaterialSwitchView.java",
    "chars": 1725,
    "preview": "package com.jacopomii.gappsmod.ui.view;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport andro"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/view/SuggestedModsAppHeaderView.java",
    "chars": 1592,
    "preview": "package com.jacopomii.gappsmod.ui.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport a"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/ui/view/SwitchCardView.java",
    "chars": 1606,
    "preview": "package com.jacopomii.gappsmod.ui.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport a"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/util/OnItemClickListener.java",
    "chars": 183,
    "preview": "package com.jacopomii.gappsmod.util;\n\n/**\n * A generic interface to handle clicks on recyclerview rows.\n */\npublic inter"
  },
  {
    "path": "app/src/main/java/com/jacopomii/gappsmod/util/Utils.java",
    "chars": 9795,
    "preview": "package com.jacopomii.gappsmod.util;\n\nimport static com.jacopomii.gappsmod.data.Constants.VENDING_ANDROID_PACKAGE_NAME;\n"
  },
  {
    "path": "app/src/main/proto/call_screen_i18n.proto",
    "chars": 929,
    "preview": "syntax = \"proto3\";\r\n\r\npackage com.jacopomii.gappsmod.protos;\r\n\r\noption java_multiple_files = true;\r\noption java_package "
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_down_24.xml",
    "chars": 339,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_up_24.xml",
    "chars": 328,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_beta_24.xml",
    "chars": 453,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_error_24.xml",
    "chars": 384,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_fail_24.xml",
    "chars": 658,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:tint=\""
  },
  {
    "path": "app/src/main/res/drawable/ic_install_24.xml",
    "chars": 323,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_menu_search_24.xml",
    "chars": 528,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_nav_drawer_boolean_mods_24.xml",
    "chars": 427,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_nav_drawer_delete_24.xml",
    "chars": 363,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_nav_drawer_information_24.xml",
    "chars": 383,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_nav_drawer_suggested_mods_24.xml",
    "chars": 464,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_save_24.xml",
    "chars": 448,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_success_24.xml",
    "chars": 542,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/layouts/activities/layout/activity_main.xml",
    "chars": 2093,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.drawerlayout.widget.DrawerLayout xmlns:android=\"http://schemas.android."
  },
  {
    "path": "app/src/main/res/layouts/activities/layout/activity_splash_screen.xml",
    "chars": 6493,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/dialogs/layout/dialog_select_package.xml",
    "chars": 2740,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/fragments/layout/fragment_boolean_mods.xml",
    "chars": 5816,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/fragments/layout/fragment_information.xml",
    "chars": 3361,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:"
  },
  {
    "path": "app/src/main/res/layouts/fragments/layout/fragment_revert_mods.xml",
    "chars": 3534,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:"
  },
  {
    "path": "app/src/main/res/layouts/fragments/layout/fragment_suggested_mods.xml",
    "chars": 9830,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:"
  },
  {
    "path": "app/src/main/res/layouts/items/layout/filterable_searchview.xml",
    "chars": 1106,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/items/layout/nav_drawer_header.xml",
    "chars": 981,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/items/layout/package_row.xml",
    "chars": 1351,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/items/layout/suggested_mods_app_header.xml",
    "chars": 1884,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layouts/items/layout/switch_card.xml",
    "chars": 1588,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/menu/nav_drawer.xml",
    "chars": 1150,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools="
  },
  {
    "path": "app/src/main/res/menu/search_menu.xml",
    "chars": 497,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"h"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 325,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/navigation/mobile_navigation.xml",
    "chars": 1271,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "chars": 751,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"SwitchCardView\">\n        <!-- The text a"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 3347,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Light theme colors -->\n    <color name=\"md_theme_light_prima"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 420,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"margin_generic\">16dp</dimen>\n\n    <dimen name=\"l_125"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 7457,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- App name -->\n    <string name=\"app_name\" translatabl"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "chars": 2413,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Application light theme -->\n    <style name=\"AppTheme\" paren"
  },
  {
    "path": "app/src/main/res/values-es/strings.xml",
    "chars": 7536,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- GApp names, taken from https://play.google.com/store"
  },
  {
    "path": "app/src/main/res/values-fr/strings.xml",
    "chars": 7929,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- GApp names, taken from https://play.google.com/store"
  },
  {
    "path": "app/src/main/res/values-it/strings.xml",
    "chars": 7534,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- GApp names, taken from https://play.google.com/store"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "chars": 2627,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Application dark theme -->\n    <style name=\"AppTheme\" parent"
  },
  {
    "path": "app/src/main/res/values-notnight-v23/themes.xml",
    "chars": 479,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Override application light theme on API 23+ (because on lowe"
  },
  {
    "path": "app/src/main/res/values-notnight-v27/themes.xml",
    "chars": 709,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Override application light theme on API 27+ (because on lowe"
  },
  {
    "path": "build.gradle",
    "chars": 590,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    re"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 233,
    "preview": "#Wed Apr 26 10:10:02 CEST 2023\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\:/"
  },
  {
    "path": "gradle.properties",
    "chars": 1003,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 5766,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2674,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "settings.gradle",
    "chars": 253,
    "preview": "dependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n   "
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the jacopotediosi/GoogleDialerMod GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 82 files (251.0 KB), approximately 59.4k tokens, and a symbol index with 139 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!