coreply/coreply/main 56k tokens More Tools
```
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── config.yml
      ├── please-read-before-opening-an-issue.md (100 tokens)
├── LICENSE (omitted)
├── README.md (1500 tokens)
├── coreply-android/
   ├── .gitignore
   ├── app/
      ├── .gitignore
      ├── build.gradle.kts (400 tokens)
      ├── proguard-rules.pro (200 tokens)
      ├── src/
         ├── main/
            ├── AndroidManifest.xml (400 tokens)
            ├── java/
               ├── app/
                  ├── coreply/
                     ├── coreplyapp/
                        ├── AppSelectorActivity.kt (2.1k tokens)
                        ├── MainApplication.kt (200 tokens)
                        ├── SettingsActivity.kt (600 tokens)
                        ├── WelcomeActivity.kt (1600 tokens)
                        ├── applistener/
                           ├── AppListener.kt (1800 tokens)
                           ├── AppSupportStatus.kt
                           ├── LayoutAnalyzer.kt (2.5k tokens)
                           ├── SupportedAppProperty.kt (100 tokens)
                           ├── SupportedApps.kt (1900 tokens)
                           ├── TriggerDetector.kt (900 tokens)
                        ├── data/
                           ├── PreferencesManager.kt (3.3k tokens)
                           ├── SuggestionPresentationType.kt (100 tokens)
                        ├── network/
                           ├── CustomAPISuggestionRequester.kt (1400 tokens)
                           ├── FIMSuggestionRequester.kt (500 tokens)
                           ├── SuggestionRequester.kt (100 tokens)
                        ├── suggestions/
                           ├── CallAI.kt (800 tokens)
                           ├── SuggestionStorage.kt (1000 tokens)
                           ├── TypingInfo.kt (400 tokens)
                        ├── theme/
                           ├── Color.kt (2.2k tokens)
                           ├── Theme.kt (2.4k tokens)
                           ├── Type.kt (400 tokens)
                        ├── ui/
                           ├── Overlay.kt (3.3k tokens)
                           ├── OverlayContent.kt (500 tokens)
                           ├── compose/
                              ├── LifeCycleThings.kt (200 tokens)
                              ├── ModernSettingsScreen.kt (4.1k tokens)
                              ├── OverlayComposables.kt (1400 tokens)
                           ├── viewmodel/
                              ├── AppSelectorViewModel.kt (900 tokens)
                              ├── OverlayViewModel.kt (3.3k tokens)
                              ├── SettingsViewModel.kt (1400 tokens)
                        ├── utils/
                           ├── ChatContents.kt (1600 tokens)
                           ├── ChatMessage.kt (300 tokens)
                           ├── GlobalPref.kt (300 tokens)
                           ├── JsonEscapeExtension.kt (200 tokens)
                           ├── PixelCalculator.kt (300 tokens)
                           ├── PreferenceHelper.kt (400 tokens)
                           ├── SuggestionUpdateListener.kt
                           ├── TokenizerUtil.kt (300 tokens)
            ├── res/
               ├── drawable/
                  ├── accessibility_disable_permission.png
                  ├── accessibility_grant_permission.png
                  ├── allow_overlay.png
                  ├── arrow_back_24px.xml (100 tokens)
                  ├── bubble_backgroud.xml (100 tokens)
                  ├── check_24px.xml (100 tokens)
                  ├── refresh_24px.xml (100 tokens)
                  ├── visibility_24px.xml (200 tokens)
                  ├── visibility_off_24px.xml (300 tokens)
               ├── layout/
                  ├── activity_accessibility_disable_permission.xml (500 tokens)
                  ├── activity_accessibility_permission.xml (700 tokens)
                  ├── activity_overlay_permission.xml (500 tokens)
                  ├── overlay_main.xml (200 tokens)
                  ├── overlay_trailing.xml (200 tokens)
               ├── mipmap-anydpi-v26/
                  ├── ic_launcher.xml (100 tokens)
               ├── mipmap-hdpi/
                  ├── ic_launcher.png
                  ├── ic_launcher_background.png
                  ├── ic_launcher_foreground.png
                  ├── ic_launcher_monochrome.png
               ├── mipmap-mdpi/
                  ├── ic_launcher.png
                  ├── ic_launcher_background.png
                  ├── ic_launcher_foreground.png
                  ├── ic_launcher_monochrome.png
               ├── mipmap-xhdpi/
                  ├── ic_launcher.png
                  ├── ic_launcher_background.png
                  ├── ic_launcher_foreground.png
                  ├── ic_launcher_monochrome.png
               ├── mipmap-xxhdpi/
                  ├── ic_launcher.png
                  ├── ic_launcher_background.png
                  ├── ic_launcher_foreground.png
                  ├── ic_launcher_monochrome.png
               ├── mipmap-xxxhdpi/
                  ├── ic_launcher.png
                  ├── ic_launcher_background.png
                  ├── ic_launcher_foreground.png
                  ├── ic_launcher_monochrome.png
               ├── values-v21/
                  ├── styles.xml (100 tokens)
               ├── values-v23/
                  ├── font_certs.xml (800 tokens)
               ├── values-w820dp/
                  ├── dimens.xml (100 tokens)
               ├── values/
                  ├── colors.xml (100 tokens)
                  ├── dimens.xml (100 tokens)
                  ├── strings.xml (600 tokens)
                  ├── styles.xml (100 tokens)
               ├── xml/
                  ├── accessibility_config.xml (100 tokens)
   ├── build.gradle.kts (100 tokens)
   ├── gradle.properties (200 tokens)
   ├── gradle/
      ├── libs.versions.toml (100 tokens)
   ├── gradlew (1700 tokens)
   ├── gradlew.bat (600 tokens)
   ├── settings.gradle.kts (100 tokens)
├── docs/
   ├── prompting.md (1300 tokens)
   ├── providers.md (800 tokens)
   ├── static/
      ├── coreply_banner.png
      ├── coreply_demo.gif
      ├── coreply_logo_cropped.png
      ├── insta.gif
      ├── narrowbanner.png
      ├── notifications.gif
      ├── signal.gif
      ├── tinder.gif
      ├── whatsapp.gif
```


## /.github/ISSUE_TEMPLATE/config.yml

```yml path="/.github/ISSUE_TEMPLATE/config.yml" 
blank_issues_enabled: false

```

## /.github/ISSUE_TEMPLATE/please-read-before-opening-an-issue.md

---
name: Please read before opening an issue
about: Please read the template in the next page carefully before you open an issue
title: ''
labels: ''
assignees: ''

---

**Please do not open issues related to the following problems**

- Seeing `...Illegal input... "code": 42...`, especially when using Gemini:
  - You hit the rate limit. There's nothing Coreply can do.

- Google Play Protect/ Other security feature blocking this app
  - This is the security feature of your system.

✅ I confirm that the issue I'm reporting is not the above two

Please type your report below:


## /README.md

![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/coreply/coreply/total)
![GitHub Tag](https://img.shields.io/github/v/tag/coreply/coreply)
![GitHub License](https://img.shields.io/github/license/coreply/coreply)
[![Discord](https://img.shields.io/discord/1367457809742172192?logo=discord&color=violet)](https://discord.gg/zCsQKmTFTk)
[![Telegram](https://img.shields.io/badge/telegram-group-blue?logo=telegram&link=https://t.me/coreplyappgroup)](https://t.me/coreplyappgroup)

![Coreply banner](./docs/static/narrowbanner.png)
**Coreply** is an open-source Android app providing texting suggestions while you type. It enhances
your typing experience with intelligent, context-aware suggestions.

<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://github.com/coreply/coreply">
<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/refs/heads/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="60"/>
</a>

SHA-256 hash of the signing certificate: `87:95:62:D0:13:BD:E2:44:8E:D9:B2:F3:78:F0:DB:96:02:BF:BB:CF:70:E8:65:A0:25:F4:D2:52:D0:EB:AA:94`

## Supported Texting Apps

|                                                           |
|-----------------------------------------------------------|
| **WhatsApp**                                              |
| <img src="./docs/static/whatsapp.gif" width="360" />      |
| **Instagram**                                             |
| <img src="./docs/static/insta.gif" width="360" />         |
| **Tinder**                                                |
| <img src="./docs/static/tinder.gif" width="360" />        |
| **Signal**                                                |
| <img src="./docs/static/signal.gif" width="360" />        |
| **Notification Replies**<sup>1,2</sup>                    |
| <img src="./docs/static/notifications.gif" width="360" /> |
| **Hinge**                                                 |
| **LINE**                                                  |
| **Heymandi**                                              |
| **Gmail**<sup>3</sup>                                     |
| **Telegram**<sup>4</sup>                                  |
| **Mattermost**<sup>2</sup>                                |
| **Facebook Messenger**<sup>1</sup>                        |
| **Google Messages**<sup>1</sup>                           |
| **Snapchat**<sup>2</sup>                                  |
| **Microsoft Teams**                                       |
| **Viber**                                                 |
| **Discord**                                               |
| **Beeper**                                                |

<sup>1</sup> Performance issues: Coreply may not follow smoothly the animations and transitions.  
<sup>2</sup> Limited role detection: Coreply cannot detect whether the message is sent or
received.  
<sup>3</sup> In Gmail, Coreply only works on the quick reply text field at the bottom of the
email.  
<sup>4</sup> Including Direct Download version, Play Store version, and Nekogram.

_DISCLAIMER: Coreply is not affiliated with or endorsed by the above-mentioned apps or their parent
companies._

## Features

  <img src="./docs/static/coreply_demo.gif" width="360" />

- **Real-time AI Suggestions**: Get accurate, context-aware suggestions as you type.
- **Customizable LLM Settings**: Supports any inference service having an OpenAI compatible API.
- **No Data Collection**: All traffic goes directly to the inference API. No data passes through
  intermediate servers (except for the hosted version).

## Getting Started

### Prerequisites

- **Android 8 or higher** (Android 13 or higher recommended)

### Installation & Usage

1. Install the latest APK from the [releases page](https://github.com/coreply/coreply/releases)
2. Configure the app with your API key, URL and model name (see the section below).
3. Toggle on the switch and grant necessary permissions. **If you encountered the "Restricted
   settings" dialog, you can
   follow [these steps](https://support.google.com/android/answer/12623953?hl=en).**
4. Start typing in your messaging app, and see suggestions appear!
    - Single tap on the suggestion to insert one word
    - Long press to insert the entire suggestion.

### Configurations

#### Coreply Cloud

Sign up and get an access key from [Coreply Cloud](https://coreply.up.nadles.com/), and paste it in
the app.

#### OpenAI-Compatible APIs

| Provider                                                      | Guide                                        |
|---------------------------------------------------------------|----------------------------------------------|
| [Google AI Studio (Gemini API)](https://aistudio.google.com/) | [Here](./docs/providers.md#google-ai-studio) |
| [Groq](https://groq.com/)                                     | [Here](./docs/providers.md#groq)             |
| [Openrouter](https://openrouter.ai/)                          | [Here](./docs/providers.md#openrouter)       |
| [OpenAI](https://platform.openai.com/)                        | [Here](./docs/providers.md#openai)           |
| [Mistral](https://mistral.ai/)                                | [Here](./docs/providers.md#mistral)          |
| Other OpenAI-compatible endpoints                             | [Here](./docs/providers.md#others)           |

## How does it work?

See [Prompting](docs/prompting.md) for details.

## Build From Source

1. Clone the repository:
2. Open the project in Android Studio.
3. Sync the Gradle files and resolve any dependencies.
4. Build and run the app on your preferred device or emulator.


## Contributing

All contributions are welcome. However, please expect breaking changes as this project is in active
development. A contributor license agreement (CLA), or change in license is under consideration.
Please to reach out before making significant contributions.

## Known Issues

- The app cannot read images, videos, voice notes, or other non-text content. Contextual suggestions
  may be limited in these cases.
- Hint text 'Message' in WhatsApp is treated as typed text on devices running Android 12 or lower.
- RTL support is limited.
- Banking apps in asia commonly block apps from unknown sources having accessibility services
  permission due to security reasons. If you are facing this issue, you can
  setup [an accessibility shortcut](https://support.google.com/accessibility/android/answer/7650693?hl=en#step_1)
  to toggle the coreply on/off quickly. In the future there might be a Play Store listing to avoid
  this issue.

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=coreply/coreply&type=Date)](https://www.star-history.com/#coreply/coreply&Date)

## License Notice

Coreply

Copyright (C) 2024 Coreply

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.


## /coreply-android/.gitignore

```gitignore path="/coreply-android/.gitignore" 
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
/.idea
/.gradle
/.kotlin
/app/release
/app/debug
/gradle/wrapper/
```

## /coreply-android/app/.gitignore

```gitignore path="/coreply-android/app/.gitignore" 
/build

```

## /coreply-android/app/build.gradle.kts

```kts path="/coreply-android/app/build.gradle.kts" 
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.compose.compiler)
}

android {
    namespace = "app.coreply.coreplyapp"
    compileSdk = 36
    defaultConfig {
        applicationId = "app.coreply.coreplyapp"
        minSdk = 26
        targetSdk = 36
        versionCode = 17
        versionName = "2.3.2"
        vectorDrawables.useSupportLibrary = true
    }
    buildFeatures {
        compose = true
    }
    buildTypes {
        debug{
            applicationIdSuffix = ".debug"
            versionNameSuffix = "-debug"
        }
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

kotlin{
    jvmToolchain(21)
}
val ktor_version: String by project
val kotlin_version: String by project

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    implementation("com.aallam.openai:openai-client:4.0.1")
    implementation("io.ktor:ktor-client-android:$ktor_version")
    implementation("androidx.core:core-ktx:1.17.0")
    implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
    
    val preferenceVersion = "1.2.1"
    implementation("androidx.preference:preference-ktx:$preferenceVersion")
    
    // Jetpack Compose BOM
    implementation(platform("androidx.compose:compose-bom:2026.02.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose:1.12.4")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
    implementation("androidx.compose.runtime:runtime-livedata")
    implementation("androidx.datastore:datastore-preferences:1.2.0")
    implementation("androidx.compose.ui:ui-text-google-fonts:1.10.3")

    implementation("com.squareup.okhttp3:okhttp:5.3.2")
    implementation("com.samskivert:jmustache:1.16")
}

```

## /coreply-android/app/proguard-rules.pro

```pro path="/coreply-android/app/proguard-rules.pro" 
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# 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 *;
#}
-keepattributes *Annotation*
-keepclassmembers class com.google.**.R$* {
    public static <fields>;
}
-keep public class com.google.ads.** {*;}
-keep public class com.google.android.gms.** {*;}
-dontwarn org.slf4j.impl.StaticLoggerBinder

```

## /coreply-android/app/src/main/AndroidManifest.xml

```xml path="/coreply-android/app/src/main/AndroidManifest.xml" 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.coreply.coreplyapp">

    <uses-permission android:name="android.permission.INTERNET" />

    <queries>
        <!-- Query apps that have a launcher icon (MAIN + LAUNCHER) -->
        <intent>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent>
    </queries>

    <application
        android:name="app.coreply.coreplyapp.MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:theme="@style/AppTheme">
        <activity
            android:exported="true"
            android:name="app.coreply.coreplyapp.SettingsActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <service
            android:exported="false"
            android:name="app.coreply.coreplyapp.applistener.AppListener"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_config" />
        </service>

        <activity
            android:name="app.coreply.coreplyapp.WelcomeActivity"
            android:label="Welcome Activity"
            android:theme="@style/AppTheme.NoActionBar" />

        <activity
            android:name="app.coreply.coreplyapp.AppSelectorActivity"
            android:label="Select Apps"
            android:parentActivityName="app.coreply.coreplyapp.SettingsActivity" />

    </application>

</manifest>
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/AppSelectorActivity.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/AppSelectorActivity.kt" 
package app.coreply.coreplyapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.viewmodel.compose.viewModel
import app.coreply.coreplyapp.theme.CoreplyTheme
import app.coreply.coreplyapp.ui.viewmodel.AppInfo
import app.coreply.coreplyapp.ui.viewmodel.AppSelectorViewModel

class AppSelectorActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            CoreplyTheme {
                AppSelectorScreen(
                    onBackPressed = { finish() }
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppSelectorScreen(
    onBackPressed: () -> Unit,
    viewModel: AppSelectorViewModel = viewModel()
) {
    val uiState = viewModel.uiState
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            TopAppBar(
                scrollBehavior = scrollBehavior,
                title = { Text("Select Apps") },
                navigationIcon = {
                    IconButton(onClick = onBackPressed) {
                        Icon(painter = painterResource(R.drawable.arrow_back_24px), contentDescription = "Back")
                    }
                },
                actions = {
                    if (!uiState.isLoading) {
                        IconButton(onClick = { viewModel.retryLoadApps() }) {
                            Icon(painter = painterResource(R.drawable.refresh_24px), contentDescription = "Refresh")
                        }
                    }
                }
            )
        }
    ) { paddingValues ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            when {
                uiState.isLoading -> {
                    LoadingContent()
                }

                uiState.error != null -> {
                    ErrorContent(
                        error = uiState.error,
                        onRetry = { viewModel.retryLoadApps() }
                    )
                }

                else -> {
                    AppListContent(
                        supportedApps = uiState.supportedApps,
                        otherApps = uiState.otherApps,
                        selectedApps = uiState.selectedApps,
                        onAppToggle = viewModel::toggleAppSelection
                    )
                }
            }
        }
    }
}

@Composable
fun LoadingContent() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            CircularProgressIndicator(
                modifier = Modifier.size(48.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = "Loading installed apps...",
                style = MaterialTheme.typography.bodyLarge,
                textAlign = TextAlign.Center
            )
        }
    }
}

@Composable
fun ErrorContent(
    error: String,
    onRetry: () -> Unit
) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
            modifier = Modifier.padding(32.dp)
        ) {
            Text(
                text = "Error",
                style = MaterialTheme.typography.headlineSmall,
                color = MaterialTheme.colorScheme.error
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = error,
                style = MaterialTheme.typography.bodyMedium,
                textAlign = TextAlign.Center,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = onRetry) {
                Text("Retry")
            }
        }
    }
}

@Composable
fun AppListContent(
    supportedApps: List<AppInfo>,
    otherApps: List<AppInfo>,
    selectedApps: Set<String>,
    onAppToggle: (String) -> Unit
) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Supported Apps Section
        if (supportedApps.isNotEmpty()) {
            item {
                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    Text(
                        text = "Coreply Supported Apps",
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(top = 8.dp)
                    )
                    InfoMessageCard(
                        text = "Coreply is not affiliated with or endorsed by the apps listed here. Because third-party apps can change at any time, Coreply may stop working or behave differently even for apps shown as supported."
                    )
                }
            }

            items(supportedApps) { app ->
                AppSelectionItem(
                    app = app,
                    isSelected = selectedApps.contains(app.packageName),
                    onSelectionChanged = { onAppToggle(app.packageName) }
                )
            }
        }

        // Other Apps Section
        if (otherApps.isNotEmpty()) {
            item {
                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    Text(
                        text = "Other Apps",
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(top = 8.dp)
                    )
                    InfoMessageCard(
                        text = "Coreply may not work in these apps, or may behave unexpectedly, because every app is different. If enabled, on-screen content from these apps may be sent to the API service you configured to generate suggestions."
                    )
                }
            }

            items(otherApps) { app ->
                AppSelectionItem(
                    app = app,
                    isSelected = selectedApps.contains(app.packageName),
                    onSelectionChanged = { onAppToggle(app.packageName) }
                )
            }
        }

        // Empty state
        if (supportedApps.isEmpty() && otherApps.isEmpty()) {
            item {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(32.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "No apps found",
                        style = MaterialTheme.typography.bodyLarge,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        textAlign = TextAlign.Center
                    )
                }
            }
        }
    }
}

@Composable
private fun InfoMessageCard(text: String) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.secondaryContainer
        )
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSecondaryContainer,
            modifier = Modifier.padding(16.dp)
        )
    }
}

@Composable
fun AppSelectionItem(
    app: AppInfo,
    isSelected: Boolean,
    onSelectionChanged: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.weight(1f)
        ) {
            // App Icon
            app.appIcon?.let { icon ->
                Image(
                    bitmap = icon.toBitmap(48, 48).asImageBitmap(),
                    contentDescription = "${app.appName} icon",
                    modifier = Modifier.size(48.dp)
                )
            } ?: run {
                Box(
                    modifier = Modifier.size(48.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text("?", style = MaterialTheme.typography.headlineMedium)
                }
            }

            Spacer(modifier = Modifier.width(16.dp))

            Column {
                Text(
                    text = app.appName,
                    style = MaterialTheme.typography.bodyLarge,
                    fontWeight = FontWeight.Medium
                )
                Text(
                    text = app.packageName,
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
                if (app.isSupported) {
                    Text(
                        text = "Supported",
                        style = MaterialTheme.typography.labelSmall,
                        color = MaterialTheme.colorScheme.primary
                    )
                }
            }
        }

        Spacer(modifier = Modifier.width(8.dp))

        Switch(
            checked = isSelected,
            onCheckedChange = { onSelectionChanged() }
        )
    }

}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/MainApplication.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/MainApplication.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */


package app.coreply.coreplyapp

import android.app.Application
import app.coreply.coreplyapp.utils.PreferenceHelper

/**
 * Created on 1/25/17.
 */
open class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        PreferenceHelper.init(this)
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/SettingsActivity.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/SettingsActivity.kt" 
package app.coreply.coreplyapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.material3.Text as ComposeText
import app.coreply.coreplyapp.ui.compose.ModernSettingsScreen
import app.coreply.coreplyapp.theme.CoreplyTheme

/**
 * Created on 12/24/16.
 * Updated to use Jetpack Compose
 */
class SettingsActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
            CoreplyTheme {
                Scaffold(
                    modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
                    topBar = {
                        TopAppBar(
                            scrollBehavior = scrollBehavior,
                            navigationIcon = {
                                Icon(
                                    painter = painterResource(id = R.mipmap.ic_launcher_foreground),
                                    contentDescription = "App Icon",
                                    modifier = Modifier.height(48.dp),
                                    tint = MaterialTheme.colorScheme.onSurface
                                )
                            },
                            title = {
                                ComposeText("Coreply", maxLines = 1,fontWeight = FontWeight.Black,)
                            }
                        )
                    }
                ) { innerPadding ->
                    Surface(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        ModernSettingsScreen()
                    }
                }
            }
        }
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/WelcomeActivity.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/WelcomeActivity.kt" 
package app.coreply.coreplyapp

import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.coreply.coreplyapp.theme.CoreplyTheme
import app.coreply.coreplyapp.utils.GlobalPref.isAccessibilityEnabled

class WelcomeActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        
        val page = intent.getIntExtra("page", 2)
        
        setContent {
            CoreplyTheme {
                WelcomeScreen(
                    page = page,
                    onFinish = { finish() }
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WelcomeScreen(
    page: Int,
    onFinish: () -> Unit
) {
    val context = LocalContext.current
    
    Scaffold(
        modifier = Modifier.fillMaxSize(),
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(16.dp)
                .verticalScroll(rememberScrollState()),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            when (page) {
                2 -> PermissionContent(
                    title = "Accessibility Service Disclosure",
                    description = "Please read the following disclosure carefully.",
                    cardColors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.secondaryContainer
                    ),
                    cardContent = {
                        Text(
                            text = "What data is being accessed",
                            style = MaterialTheme.typography.titleMedium,
                            fontWeight = FontWeight.Bold,
                            color = MaterialTheme.colorScheme.onSecondaryContainer
                        )
                        Spacer(modifier = Modifier.height(3.dp))
                        Text(
                            text = "Coreply's accessibility service reads on-screen text content, detects active text input fields and reads the text being typed.",
                            style = MaterialTheme.typography.bodyMedium,
                            color = MaterialTheme.colorScheme.onSecondaryContainer
                        )
                        Spacer(modifier = Modifier.height(6.dp))
                        Text(
                            text = "How your data is shared",
                            style = MaterialTheme.typography.titleMedium,
                            fontWeight = FontWeight.Bold,
                            color = MaterialTheme.colorScheme.onSecondaryContainer
                        )
                        Spacer(modifier = Modifier.height(3.dp))
                        Text(
                            text = "The data described above will be sent to the API or service according to your setup, in order to generate context-aware typing suggestions.",
                            style = MaterialTheme.typography.bodyMedium,
                            color = MaterialTheme.colorScheme.onSecondaryContainer
                        )
                    },
                    buttonContent = {
                        Button(
                            onClick = {
                                context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
                                onFinish()
                            },
                            modifier = Modifier.fillMaxWidth()
                        ) {
                            Text("I Agree & Enable")
                        }
                        
                        TextButton(
                            onClick = onFinish
                        ) {
                            Text("Cancel")
                        }
                    }
                )
                3 -> PermissionContent(
                    title = "Disable Accessibility Service",
                    description = "To turn off Coreply, you need to disable the accessibility service in your device settings.",
                    cardColors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.errorContainer
                    ),
                    cardContent = {
                        Text(
                            text = "⚠️ Important",
                            style = MaterialTheme.typography.titleMedium,
                            fontWeight = FontWeight.Bold,
                            color = MaterialTheme.colorScheme.onErrorContainer
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(
                            text = "Disabling the accessibility service will stop all Coreply features. You can re-enable it anytime from the app settings.",
                            style = MaterialTheme.typography.bodyMedium,
                            color = MaterialTheme.colorScheme.onErrorContainer
                        )
                    },
                    buttonContent = {
                        Button(
                            onClick = {
                                context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
                                onFinish()
                            },
                            modifier = Modifier.fillMaxWidth(),
                        ) {
                            Text("Open Accessibility Settings")
                        }
                        
                        TextButton(
                            onClick = onFinish
                        ) {
                            Text("Cancel")
                        }
                    }
                )
                else -> onFinish()
            }
        }
    }
}

@Composable
private fun ColumnScope.PermissionContent(
    title: String,
    description: String,
    cardColors: CardColors,
    cardHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
    cardContent: @Composable ColumnScope.() -> Unit,
    buttonContent: @Composable ColumnScope.() -> Unit
) {
    // App icon
    Image(
        painter = painterResource(id = R.mipmap.ic_launcher_foreground),
        contentDescription = "Coreply Icon",
        modifier = Modifier.size(60.dp)
    )
    
    Text(
        text = title,
        style = MaterialTheme.typography.titleLarge,
        fontWeight = FontWeight.Bold,
        textAlign = TextAlign.Center
    )
    
    Text(
        text = description,
        style = MaterialTheme.typography.bodyMedium,
        textAlign = TextAlign.Center,
        color = MaterialTheme.colorScheme.onSurfaceVariant
    )
    
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = cardColors
    ) {
        Column(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            horizontalAlignment = cardHorizontalAlignment,
            content = cardContent
        )
    }
    Spacer(modifier = Modifier.weight(1f))
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        content = buttonContent
    )
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/AppListener.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/AppListener.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.applistener

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.os.Build
import android.util.Log
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import app.coreply.coreplyapp.R
import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.ui.Overlay
import app.coreply.coreplyapp.ui.viewmodel.OverlayViewModel
import app.coreply.coreplyapp.ui.viewmodel.RefreshType
import app.coreply.coreplyapp.utils.PixelCalculator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch

/**
 * Created on 10/13/16.
 */
@OptIn(FlowPreview::class)
open class AppListener : AccessibilityService() {
    private lateinit var overlay: Overlay
    private lateinit var overlayViewModel: OverlayViewModel
    private val pixelCalculator: PixelCalculator = PixelCalculator(this)
    private lateinit var preferencesManager: PreferencesManager


    // Coroutine scope for background operations
    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    // Flow channels for throttling heavy operations
    private val measureWindowFlow = MutableSharedFlow<AccessibilityNodeInfo>(
        replay = 0,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    private val getMessagesFlow = MutableSharedFlow<AccessibilityNodeInfo>(
        replay = 0,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        Log.v("CoWA", "event triggered")
        if (event != null && event.getPackageName() != null) {
            if (event.packageName.startsWith("app.coreply")) return
            Log.v("CoWA", event.getPackageName().toString())
        }
        if (event != null && event.getClassName() != null) {
            Log.v("CoWA", event.getClassName().toString())
        }
        if (event == null || event.getPackageName() == null || event.getClassName() == null) {
            Log.v("CoWA", "Either event or package name or class name is null")
            return
        }
        val root1 = rootInActiveWindow
        if (root1 == null) {
            Log.v("CoWA", "root is null")
        } else {
            refreshOverlay(event, root1)
        }
    }


    override fun onInterrupt() {
        overlay.removeOverlays()
    }


    private fun refreshOverlay(event: AccessibilityEvent, root: AccessibilityNodeInfo): Boolean {
        var isSupportedApp = false
        val previousInputNodeStillHere: Boolean =
            overlayViewModel.refresh(RefreshType.NORMAL, false)
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){
            overlayViewModel.updateInputMethod(inputMethod)
        }

        val (supportedAppProperty, inputWidget) = if (previousInputNodeStillHere) Pair(
            overlayViewModel.uiState.value.currentApp,
            overlayViewModel.uiState.value.currentInput
        ) else detectSupportedApp(root, preferencesManager.selectedAppsState.value)
        if (supportedAppProperty != null && inputWidget != null) {
            isSupportedApp = true
            val info = this.serviceInfo
            info.notificationTimeout = 0
            info.eventTypes =
                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED or AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_SCROLLED
            this.serviceInfo = info
            // Update state instead of direct overlay calls

            overlayViewModel.enable(
                supportedAppProperty,
                inputWidget,
                root,
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    inputMethod
                } else null
            )


            measureWindowFlow.tryEmit(inputWidget)
            getMessagesFlow.tryEmit(root)

        }
        if (!isSupportedApp) {
            if (overlayViewModel.uiState.value.isRunning) {
                val info = this.serviceInfo
                info.notificationTimeout = 2000
                info.eventTypes =
                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
                this.serviceInfo = info

                // Update state instead of direct overlay calls
                overlayViewModel.disable()
            }

        }
        return isSupportedApp
    }

    override fun onServiceConnected() {
        super.onServiceConnected()
        val info = this.serviceInfo
        info.eventTypes =
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED or AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_SCROLLED
        info.flags =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
            } else AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
        this.serviceInfo = info
        Toast.makeText(
            applicationContext,
            getString(R.string.app_accessibility_started),
            Toast.LENGTH_SHORT
        )
            .show()
        val appContext = applicationContext

        overlay = Overlay(
            appContext,
            getSystemService(WINDOW_SERVICE) as WindowManager,
        )
        overlayViewModel = overlay.viewModel

        // Initialize throttled flows for heavy operations
        initializeThrottledFlows()
        preferencesManager = PreferencesManager.getInstance(appContext)
        MainScope().launch {
            preferencesManager.loadPreferences()
        }
        observeMasterSwitch()
    }

    private fun observeMasterSwitch() {
        serviceScope.launch {
            preferencesManager.disableSelfRequests.collect {
                overlay.removeOverlays()
                overlayViewModel.disable()
                disableSelf()
            }
        }
    }

    /**
     * Initialize throttled flows for heavy operations with proper debouncing
     * Ensures the latest event is always processed while throttling intermediate events
     */
    private fun initializeThrottledFlows() {
        serviceScope.launch {
            measureWindowFlow
                .collect { node ->
                    try {
                        overlayViewModel.refresh(RefreshType.CHAR_LOCATION, true)

                    } catch (e: Exception) {
                        Log.e("CoWA", "Error in measureWindow background operation", e)
                    }
                }
        }
        serviceScope.launch {
            getMessagesFlow
                .debounce(500)
                .collect { rootNode ->
                    try {
                        getMessagesInternal()
                    } catch (e: Exception) {
                        Log.e("CoWA", "Error in getMessages background operation", e)
                    }
                }
        }
    }


    /**
     * Internal implementation of getMessages that runs on background thread
     */
    private fun getMessagesInternal() {
        overlayViewModel.refresh(RefreshType.TEXT_SIZE, false, pixelCalculator.spToPx(16f))
        overlayViewModel.refreshMessageListNode()
    }


    override fun onDestroy() {
        super.onDestroy()
        overlayViewModel.disable()
        // Cancel all background operations
        serviceScope.cancel()
    }

}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/AppSupportStatus.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/AppSupportStatus.kt" 
package app.coreply.coreplyapp.applistener

enum class AppSupportStatus {
    TYPING,
    HINT_TEXT,
    UNKNOWN,
    UNSUPPORTED,
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/LayoutAnalyzer.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/LayoutAnalyzer.kt" 
package app.coreply.coreplyapp.applistener

import android.graphics.Rect
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo
import app.coreply.coreplyapp.utils.ChatMessage
import java.util.ArrayList

fun generalTextInputFinder(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
    return node.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
}

val nodeComparator: Comparator<AccessibilityNodeInfo> =
    Comparator { o1: AccessibilityNodeInfo?, o2: AccessibilityNodeInfo? ->
        val rect1 = Rect()
        val rect2 = Rect()
        o1!!.getBoundsInScreen(rect1)
        o2!!.getBoundsInScreen(rect2)
        rect1.top - rect2.top
    }

fun generalMessageListProcessor(
    node: AccessibilityNodeInfo,
    messageWidgets: ArrayList<String>,
    getChild: (AccessibilityNodeInfo) -> AccessibilityNodeInfo = { it }
): MutableList<ChatMessage> {
    val chatWidgets: MutableList<AccessibilityNodeInfo> = ArrayList<AccessibilityNodeInfo>()
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()

    for (messageWidget in messageWidgets) {
        chatWidgets.addAll(node.findAccessibilityNodeInfosByViewId(messageWidget).map(getChild))
    }
    chatWidgets.sortWith(nodeComparator)

    val rootRect = Rect()
    node.getBoundsInScreen(rootRect)
    for (chatNodeInfo in chatWidgets) {
        val bounds = Rect()
        chatNodeInfo.getBoundsInScreen(bounds)
        val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
    }

    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

/**
 * Recursively finds all nodes with the specified ID.
 *
 * @param rootNode The node to start searching from
 * @param targetId The ID to search for, e.g. "android:id/message_text"
 * @return List of nodes matching the target ID
 */
fun findNodesByCriteria(
    rootNode: AccessibilityNodeInfo?,
    nodeChecker: (AccessibilityNodeInfo) -> Boolean
): MutableList<AccessibilityNodeInfo> {
    val results = mutableListOf<AccessibilityNodeInfo>()
    if (rootNode == null) return results
    try {
        if (nodeChecker(rootNode)) {
            results.add(rootNode)

        }
    } catch (e: Exception) {
        Log.e("findNodesWithId", "Error accessing viewIdResourceName: ${e.message}")
    }

    // Recursively search through child nodes
    for (i in 0 until rootNode.childCount) {
        try {
            val childNode = rootNode.getChild(i)
            if (childNode != null) {
                results.addAll(findNodesByCriteria(childNode, nodeChecker))
            }
        } catch (e: Exception) {
            Log.e("findNodesWithId", "Error accessing child node: ${e.message}")
        }
    }

    return results
}

fun notificationMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val textInputNode = node.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
    if (textInputNode == null) {
        return mutableListOf()
    }
    var targetAreas = node.findAccessibilityNodeInfosByViewId("com.android.systemui:id/expanded")
    if (targetAreas.isEmpty()) {
        targetAreas =
            node.findAccessibilityNodeInfosByViewId("com.android.systemui:id/expandableNotificationRow")
    }

    // Get the rect of the text input node
    val textInputRect = Rect()
    textInputNode.getBoundsInScreen(textInputRect)

    // Find the target area that is above the text input but closest to it
    var closestTarget: AccessibilityNodeInfo? = null
    var minDistance = Int.MAX_VALUE

    for (targetArea in targetAreas) {
        val targetRect = Rect()
        targetArea.getBoundsInScreen(targetRect)

        // Check if the target is above the text input
        if (targetRect.top <= textInputRect.top) {
            // Calculate the vertical distance between the bottom of target and top of input
            val distance = textInputRect.top - targetRect.top

            // Update closest target if this one is closer
            if (distance < minDistance) {
                minDistance = distance
                closestTarget = targetArea
            }
        }
    }

    // Process the closest target for chat messages
    // For now, return empty list if no suitable target is found
    return if (closestTarget != null) {
        val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
        val chatWidgets = findNodesByCriteria(closestTarget, {
            val nodeId = it.viewIdResourceName
            nodeId != null && nodeId.endsWith("android:id/message_text")
        })
        chatWidgets.sortWith(nodeComparator)

        val rootRect = Rect()
        node.getBoundsInScreen(rootRect)
        for (chatNodeInfo in chatWidgets) {
            val bounds = Rect()
            chatNodeInfo.getBoundsInScreen(bounds)
            val message_text = chatNodeInfo.text?.toString() ?: ""
            chatMessages.add(ChatMessage("Others", message_text, ""))
        }

        //Log.v("CoWA", conversationList.toString())
        chatMessages
    } else {
        mutableListOf()
    }
}

fun onScreenContentProcessor(
    node: AccessibilityNodeInfo,
): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val textInputNode = generalTextInputFinder(node)
    val inputRect = Rect()
    textInputNode?.getBoundsInScreen(inputRect)

    val chatWidgets = findNodesByCriteria(node, {
        if (it.text?.isBlank() ?: true || it.isShowingHintText || it.isFocused) false
        else{
            val tmpRect = Rect()
            it.getBoundsInScreen(tmpRect)
            tmpRect.top <= inputRect.top
        }
    })
    chatWidgets.sortWith(nodeComparator)
    for (chatNodeInfo in chatWidgets) {
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage("OnScreen", message_text, ""))
    }
    return chatMessages
}

fun telegramMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val contentNodes = node.findAccessibilityNodeInfosByViewId("android:id/content")
    if (contentNodes != null && contentNodes.size == 1) {
        val startTime = System.currentTimeMillis()
        val chatWidgets: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
            node,
            { (it.className == "android.view.ViewGroup" && it.text != null && it.text.isNotBlank()) })
        val endTime = System.currentTimeMillis()
        Log.d("TelegramProcessor", "Time taken to find chat widgets: ${endTime - startTime} ms")
        chatWidgets.sortWith(nodeComparator)

        val rootRect = Rect()
        node.getBoundsInScreen(rootRect)
        for (chatNodeInfo in chatWidgets) {
            val bounds = Rect()
            chatNodeInfo.getBoundsInScreen(bounds)
            val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
            val message_text = chatNodeInfo.text?.toString() ?: ""
            chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
        }
    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun mattermostMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val chatWidgetsParents: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
        node,
        { (it.className == "android.view.ViewGroup" && it.viewIdResourceName == "markdown_paragraph") })

    val chatWidgets: MutableList<AccessibilityNodeInfo> = ArrayList<AccessibilityNodeInfo>()

    chatWidgets.addAll(chatWidgetsParents.map { findNodesByCriteria(it, { it.text.isNotBlank() }) }
        .flatten())
    chatWidgets.sortWith(nodeComparator)

    for (chatNodeInfo in chatWidgets) {
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage("Others", message_text, ""))
    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun googleMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val chatWidgets: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
        node,
        { it.viewIdResourceName == "message_text" })
    chatWidgets.sortWith(nodeComparator)

    val rootRect = Rect()
    node.getBoundsInScreen(rootRect)
    for (chatNodeInfo in chatWidgets) {
        val bounds = Rect()
        chatNodeInfo.getBoundsInScreen(bounds)
        val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun scMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()

    val chatWidgets: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
        node,
        { (it.text != null && it.text.isNotBlank() && it.className == "javaClass") })

    chatWidgets.sortWith(nodeComparator)
    for (chatNodeInfo in chatWidgets) {
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage("Others", message_text, ""))

    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun teamsMessageListProcessor(
    node: AccessibilityNodeInfo,
    messageWidgets: ArrayList<String>
): MutableList<ChatMessage> {
    val chatWidgets: MutableList<AccessibilityNodeInfo> = ArrayList<AccessibilityNodeInfo>()
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()

    for (messageWidget in messageWidgets) {
        chatWidgets.addAll(node.findAccessibilityNodeInfosByViewId(messageWidget))
    }
    chatWidgets.sortWith(nodeComparator)

    val rootRect = Rect()
    node.getBoundsInScreen(rootRect)
    for (chatNodeInfo in chatWidgets) {
        val bounds = Rect()
        chatNodeInfo.getBoundsInScreen(bounds)
        val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
        val message_text = chatNodeInfo.contentDescription?.toString() ?: ""
        chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
    }

    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun beeperMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val chatWidgets: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
        node,
        { it.viewIdResourceName == "messageBubbleTextContent" })
    chatWidgets.sortWith(nodeComparator)

    val rootRect = Rect()
    node.getBoundsInScreen(rootRect)
    for (chatNodeInfo in chatWidgets) {
        val bounds = Rect()
        chatNodeInfo.getBoundsInScreen(bounds)
        val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

fun perplexityMessageListProcessor(node: AccessibilityNodeInfo): MutableList<ChatMessage> {
    val rootNodes = findNodesByCriteria(node){ it.viewIdResourceName == "thread-screen" }
    if (rootNodes.isEmpty()) {
        return mutableListOf()
    }
    val chatMessages: MutableList<ChatMessage> = ArrayList<ChatMessage>()
    val chatWidgets: MutableList<AccessibilityNodeInfo> = findNodesByCriteria(
        rootNodes[0],
    ) { it.className == "android.widget.TextView" && it.text != null && it.text.isNotBlank() && !(it.parent?.viewIdResourceName?.contains("related") ?: false) }
    chatWidgets.sortWith(nodeComparator)

    val rootRect = Rect()
    node.getBoundsInScreen(rootRect)
    for (chatNodeInfo in chatWidgets) {
        val bounds = Rect()
        chatNodeInfo.getBoundsInScreen(bounds)
        val isMe = (bounds.left + bounds.right) / 2 > (rootRect.left + rootRect.right) / 2
        val message_text = chatNodeInfo.text?.toString() ?: ""
        chatMessages.add(ChatMessage(if (isMe) "Me" else "Others", message_text, ""))
    }
    //Log.v("CoWA", conversationList.toString())
    return chatMessages
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/SupportedAppProperty.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/SupportedAppProperty.kt" 
package app.coreply.coreplyapp.applistener

import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import app.coreply.coreplyapp.utils.ChatMessage

/**
 * Created on 1/18/17.
 */
data class SupportedAppProperty(
    val pkgName: String,
    val inputJudger: (AccessibilityNodeInfo, AccessibilityNodeInfo, String, String) -> Boolean,
    val excludeWidgets: Array<String>,
    val messageListProcessor: (AccessibilityNodeInfo) -> MutableList<ChatMessage>,
    val quotedMessageWidgets: String? = null
)

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/SupportedApps.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/SupportedApps.kt" 
package app.coreply.coreplyapp.applistener

import android.view.accessibility.AccessibilityNodeInfo

/**
 * Created on 1/16/17.
 */
object SupportedApps {
    val supportedApps: Array<SupportedAppProperty> = arrayOf(

        SupportedAppProperty(
            "com.whatsapp.w4b",
            { _, _, id, _ -> id == "com.whatsapp.w4b:id/entry" },
            arrayOf<String>("com.whatsapp.w4b/menuitem_delete"),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.whatsapp.w4b:id/message_text", "com.whatsapp.w4b:id/caption")
                )
            }
        ),
        SupportedAppProperty(
            "jp.naver.line.android",
            { _, _, id, _ -> id == "jp.naver.line.android:id/chat_ui_message_edit" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("jp.naver.line.android:id/chat_ui_message_text")
                )
            }
        ),

        SupportedAppProperty(
            "com.instagram.android",
            { _, _, id, _ -> id == "com.instagram.android:id/row_thread_composer_edittext" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.instagram.android:id/direct_text_message_text_view")
                )
            }
        ),

        SupportedAppProperty(
            "org.thoughtcrime.securesms",
            { _, _, id, _ -> id == "org.thoughtcrime.securesms:id/embedded_text_editor" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("org.thoughtcrime.securesms:id/conversation_item_body")
                )
            }
        ),

        SupportedAppProperty(
            "co.hinge.app",
            { _, _, id, _ -> id == "co.hinge.app:id/messageComposition" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("co.hinge.app:id/chatBubble")
                )
            }
        ),
        SupportedAppProperty(
            "com.tinder",
            { _, _, id, _ -> id == "com.tinder:id/textMessageInput" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.tinder:id/chatTextMessageContent")
                )
            }
        ),
        SupportedAppProperty(
            "com.vr.heymandi",
            { _, _, id, _ -> id == "com.vr.heymandi:id/messageInput" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.vr.heymandi:id/messageText")
                )
            }
        ),
        SupportedAppProperty(
            "com.google.android.gm",
            { _, _, id, _ -> id == "com.google.android.gm:id/inline_reply_compose_edit_text" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf(
                        "com.google.android.gm:id/subject_and_folder_view",
                        "com.google.android.gm:id/email_snippet"
                    )
                )
            }
        ),
        SupportedAppProperty(
            "com.android.systemui",
            { root, _, id, pkg ->
                root.findAccessibilityNodeInfosByViewId("com.android.systemui:id/expandableNotificationRow")
                    .isNotEmpty() || root.findAccessibilityNodeInfosByViewId("com.android.systemui:id/expanded")
                    .isNotEmpty()
            },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                notificationMessageListProcessor(node)
            }
        ),
        SupportedAppProperty(
            "com.whatsapp",
            { _, _, id, _ -> id == "com.whatsapp:id/entry" },
            arrayOf<String>("com.whatsapp:id/menuitem_delete"),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.whatsapp:id/message_text", "com.whatsapp:id/caption")
                )
            }
        ),
        SupportedAppProperty(
            "org.telegram.messenger",
            { root, focus, id, pkg -> pkg == "org.telegram.messenger" && telegramDetector(root).first },
            arrayOf<String>(),
            {
                telegramMessageListProcessor(it)
            }
        ),
        SupportedAppProperty(
            "org.telegram.messenger.web",
            { root, focus, id, pkg ->
                pkg == "org.telegram.messenger.web" && telegramDetector(
                    root,
                    "org.telegram.messenger.web"
                ).first
            },
            arrayOf<String>(),
            {
                telegramMessageListProcessor(it)
            }
        ),
        SupportedAppProperty(
            "tw.nekomimi.nekogram",
            { root, focus, id, pkg ->
                pkg == "tw.nekomimi.nekogram" && telegramDetector(
                    root,
                    "tw.nekomimi.nekogram"
                ).first
            },
            arrayOf<String>(),
            {
                telegramMessageListProcessor(it)
            }
        ),
        SupportedAppProperty(
            "com.mattermost.rn",
            { _, _, id, _ -> id == "channel.post_draft.post.input" },
            arrayOf<String>(),
            {
                mattermostMessageListProcessor(it)
            }
        ),

        SupportedAppProperty(
            "com.google.android.apps.messaging",
            { _, _, id, _ -> id == "com.google.android.apps.messaging:id/compose_message_text" },
            arrayOf<String>(),
            {
                googleMessageListProcessor(it)
            }
        ),
        SupportedAppProperty(
            "com.facebook.orca",
            { root, focus, id, pkg -> pkg == "com.facebook.orca" },
            arrayOf<String>(),
            {
                telegramMessageListProcessor(it)
            }
        ),
        SupportedAppProperty(
            "com.snapchat.android:id",
            { root, focus, id, pkg -> id == "com.snapchat.android:id/chat_input_text_field" },
            arrayOf<String>(),
            {
                scMessageListProcessor(it)
            }
        ),

        SupportedAppProperty(
            "com.microsoft.teams:id",
            { _, _, id, _ -> id == "com.microsoft.teams:id/message_area_edit_text" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                teamsMessageListProcessor(
                    node,
                    arrayListOf("com.microsoft.teams:id/rich_text_layout")
                )
            }
        ),
        SupportedAppProperty(
            "com.viber.voip",
            { _, _, id, _ -> id == "com.viber.voip:id/send_text" },
            arrayOf<String>(),
            { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node,
                    arrayListOf("com.viber.voip:id/textMessageView")
                )
            }
        ),
        SupportedAppProperty(
            pkgName = "com.discord",
            inputJudger = { _, _, id, _ -> id == "com.discord:id/chat_input_edit_text" },
            excludeWidgets = arrayOf<String>(),
            messageListProcessor = { node: AccessibilityNodeInfo ->
                generalMessageListProcessor(
                    node = node,
                    messageWidgets = arrayListOf("com.discord:id/accessories_view"),
                    getChild = { msgNode: AccessibilityNodeInfo ->
                        if (msgNode.childCount > 0) msgNode.getChild(
                            0
                        ) else msgNode
                    }
                )
            }
        ),
        SupportedAppProperty(
            pkgName = "com.beeper.android",
            inputJudger = { root, focus, id, pkgName ->
                pkgName == "com.beeper.android" && contentAboveInputDetector(
                    root
                )
            },
            excludeWidgets = arrayOf<String>(),
            messageListProcessor = { node: AccessibilityNodeInfo ->
                beeperMessageListProcessor(node)
            }
        ),
        SupportedAppProperty(
            pkgName = "com.openai.chatgpt",
            inputJudger = { root, focus, id, pkgName ->
                pkgName == "com.openai.chatgpt" && focus.className != null && focus.className == "android.widget.EditText" && contentAboveInputDetector(
                    root
                )
            },
            excludeWidgets = arrayOf<String>(),
            messageListProcessor = { node: AccessibilityNodeInfo ->
                onScreenContentProcessor(node)
            }
        ),

        SupportedAppProperty(
            pkgName = "ai.perplexity.app.android",
            inputJudger = { root, focus, id, pkgName ->
                id == "input-search"
            },
            excludeWidgets = arrayOf<String>(),
            messageListProcessor = { node: AccessibilityNodeInfo ->
                perplexityMessageListProcessor(node)
            }
        ),

        )
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/TriggerDetector.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/applistener/TriggerDetector.kt" 
package app.coreply.coreplyapp.applistener

import android.graphics.Rect
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo


fun detectSupportedApp(
    rootNode: AccessibilityNodeInfo?,
    selectedApps: Set<String>
): Pair<SupportedAppProperty?, AccessibilityNodeInfo?> {
    try {
        val inputNode = rootNode?.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
//        iterNode(rootNode!!)
        if (inputNode != null) {
            val inputNodeId = inputNode.viewIdResourceName ?: ""
            val inputNodePackage = inputNode.packageName ?: ""
            val app = SupportedApps.supportedApps.firstOrNull { supportedAppProperty -> supportedAppProperty.pkgName == inputNodePackage }
            app?.let {
                if (selectedApps.contains(app.pkgName)
                ) {
                    return if(app.inputJudger(
                            rootNode,
                            inputNode,
                            inputNodeId,
                            inputNodePackage.toString()
                        )){
                        Pair(
                            app,
                            inputNode
                        )
                    } else{
                        Pair(null, null)
                    }

                }
            }
            if (selectedApps.contains(inputNodePackage) && inputNode.className != null && inputNode.className.contains("android.widget.EditText")) {
                return Pair(
                    SupportedAppProperty(
                        inputNodePackage.toString(),
                        { _, _, id, _ -> true },
                        arrayOf<String>(),
                        {
                            onScreenContentProcessor(it)
                        }), inputNode
                )
            }

        }
        return Pair(null, null)
    } catch (e: Exception) {
//        Log.e("TriggerDetector", "Error finding input node: ${e.message}")
        return Pair(null, null)
    }


}


/**
 * Checks if a content node is above an input widget in screen coordinates
 */
fun isContentNodeAboveInput(
    contentNode: AccessibilityNodeInfo?,
    inputNode: AccessibilityNodeInfo?
): Boolean {
    if (contentNode == null || inputNode == null) {
        return false
    }

    val contentRect = Rect()
    val inputRect = Rect()

    try {
        contentNode.getBoundsInScreen(contentRect)
        inputNode.getBoundsInScreen(inputRect)
        // Check if the content node's bottom is above the input's top

        return (contentRect.top + contentRect.bottom) / 2 < inputRect.bottom
    } catch (e: Exception) {
        Log.e("TriggerDetector", "Error checking node positions: ${e.message}")
        return false
    }
}

fun telegramDetector(
    node: AccessibilityNodeInfo,
    tgPkgName: String = "org.telegram.messenger"
): Pair<Boolean, AccessibilityNodeInfo?> {
    val contentNodes = node.findAccessibilityNodeInfosByViewId("android:id/content")
    if (contentNodes != null && contentNodes.size == 1) {
        val contentNode = contentNodes[0]
        if (contentNode.packageName == tgPkgName) {
            val inputWidget = node.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
            if (inputWidget != null && inputWidget.packageName == tgPkgName && inputWidget.className == "android.widget.EditText") {
                // Verify the content node is above the input widget
                if (isContentNodeAboveInput(contentNode, inputWidget)) {
                    return Pair(true, inputWidget)
                }
            }
        }
    }
    return Pair(false, null)
}

fun contentAboveInputDetector(node: AccessibilityNodeInfo): Boolean {
    return isContentNodeAboveInput(
        node.findAccessibilityNodeInfosByViewId("android:id/content").firstOrNull(), node.findFocus(
            AccessibilityNodeInfo.FOCUS_INPUT
        )
    )

}

fun iterNode(node: AccessibilityNodeInfo, prefix: String = "") {
    Log.v(
        "CoWA",
        "$prefix node=${node.className}, text=${node.text}, contentDescription=${node.contentDescription}, viewId=${node.viewIdResourceName}, rect=${
            Rect().also {
                node.getBoundsInScreen(
                    it
                )
            }
        }"
    )
    for (i in 0 until node.childCount) {
        val child = node.getChild(i)
        if (child != null) {
            iterNode(child, "$prefix-")
        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/data/PreferencesManager.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/data/PreferencesManager.kt" 
package app.coreply.coreplyapp.data

import android.content.Context
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.preference.PreferenceManager
import app.coreply.coreplyapp.applistener.SupportedApps
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration({ PreferenceManager.getDefaultSharedPreferences(context) }))
    })

class PreferencesManager private constructor(private val dataStore: DataStore<Preferences>) {

    companion object {
        @Volatile
        private var INSTANCE: PreferencesManager? = null

        fun getInstance(context: Context): PreferencesManager {
            return INSTANCE ?: synchronized(this) {
                val instance = PreferencesManager(context.dataStore)
                INSTANCE = instance
                instance
            }
        }

        // Preference keys
        val MASTER_SWITCH = booleanPreferencesKey("master_switch")
        val API_TYPE = stringPreferencesKey("api_type")
        val CUSTOM_API_URL = stringPreferencesKey("customApiUrl")
        val CUSTOM_API_KEY = stringPreferencesKey("customApiKey")
        val CUSTOM_MODEL_NAME = stringPreferencesKey("customModelName")
        val CUSTOM_SYSTEM_PROMPT = stringPreferencesKey("customSystemPrompt")
        val TEMPERATURE = floatPreferencesKey("temperature_float")
        val TOP_P = floatPreferencesKey("topp_float")
        val SUGGESTION_PRESENTATION_TYPE = intPreferencesKey("suggestion_presentation_type")
        val SHOW_ERRORS = booleanPreferencesKey("show_errors")
        val SELECTED_APPS = stringSetPreferencesKey("selected_apps_set")
        val CONFIG_TYPE = stringPreferencesKey("config_type")
        val ADVANCED_CONFIG_BODY = stringPreferencesKey("advanced_config_body")
        val TYPING_REGEX_PATTERN = stringPreferencesKey("typing_regex_pattern")
        val TYPING_REGEX_ENABLED = booleanPreferencesKey("typing_regex_enabled")
        val CUSTOM_DEBOUNCE_MS = intPreferencesKey("custom_debounce_ms")
        val SUGGESTION_CONTENT_TEMPLATE = stringPreferencesKey("suggestion_content_template")

        // Default values
        private const val DEFAULT_MASTER_SWITCH = true
        private const val DEFAULT_API_TYPE = "custom"
        private const val DEFAULT_API_URL = "https://api.openai.com/v1/"
        private const val DEFAULT_API_KEY = ""
        private const val DEFAULT_MODEL_NAME = "gpt-4.1-mini"
        private const val DEFAULT_SYSTEM_PROMPT =
            "You are an AI texting assistant. You will be given a list of text messages between the user (indicated by 'Message I sent:'), and other people (indicated by their names or simply 'Message I received:'). You may also receive a screenshot of the conversation. Your job is to suggest the next message the user should send. Match the tone and style of the conversation. The user may request the message start or end with a certain prefix (both could be parts of a longer word) . The user may quote a specific message. In this case, make sure your suggestions are about the quoted message.\nOutput the suggested text only. Do not output anything else. Do not surround output with quotation marks"
        private const val DEFAULT_TEMPERATURE = 0.3f
        private val DEFAULT_SELECTED_APPS = SupportedApps.supportedApps.map { it.pkgName }.toSet()
        private const val DEFAULT_TOP_P = 1.0f
        private const val DEFAULT_SUGGESTION_PRESENTATION_TYPE = 2 // Both
        private const val DEFAULT_SHOW_ERRORS = true
        private const val DEFAULT_CONFIG_TYPE = "simple"
        private var DEFAULT_ADVANCED_CONFIG_BODY = """{
            |  "model": "gpt-4o-mini",
            |  "temperature": 0.7,
            |  "top_p": 1.0,
            |  "messages": [
            |    {
            |      "role": "system",
            |      "content": "You are an AI texting assistant. Generate a suggested reply based on the conversation history and current typing. Output only the suggested text without quotation marks or extra formatting."
            |    },
            |    {
            |      "role": "user",
            |      "content": "Chat history:\n{{#pastMessages}}{{#sent}}Me: {{/sent}}{{#received}}Them: {{/received}}{{content.jsonEscaped}}\n{{/pastMessages}}{{#currentTyping}}Current typing: {{currentTyping.jsonEscaped}}{{/currentTyping}}{{^currentTyping}}Suggest a reply.{{/currentTyping}}"
            |    }
            |  ],
            |  "max_tokens": 50,
            |  "stream": false
            |}
        """.trimMargin()
        private const val DEFAULT_TYPING_REGEX_PATTERN = "^.*[\\s.!?,;:]{{contextString}}quot;
        private const val DEFAULT_TYPING_REGEX_ENABLED = false
        private const val DEFAULT_CUSTOM_DEBOUNCE_MS = 350
        private const val DEFAULT_SUGGESTION_CONTENT_TEMPLATE = "{{assistantMessage}}"
    }

    // Mutable state for each preference field
    val masterSwitchState: MutableState<Boolean> = mutableStateOf(DEFAULT_MASTER_SWITCH)
    private val _disableSelfRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
    val disableSelfRequests: SharedFlow<Unit> = _disableSelfRequests.asSharedFlow()
    val apiTypeState: MutableState<String> = mutableStateOf(DEFAULT_API_TYPE)
    val customApiUrlState: MutableState<String> = mutableStateOf(DEFAULT_API_URL)
    val customApiKeyState: MutableState<String> = mutableStateOf(DEFAULT_API_KEY)
    val customModelNameState: MutableState<String> = mutableStateOf(DEFAULT_MODEL_NAME)
    val customSystemPromptState: MutableState<String> = mutableStateOf(DEFAULT_SYSTEM_PROMPT)
    val temperatureState: MutableState<Float> = mutableStateOf(DEFAULT_TEMPERATURE)
    val topPState: MutableState<Float> = mutableStateOf(DEFAULT_TOP_P)
    val selectedAppsState: MutableState<Set<String>> = mutableStateOf(DEFAULT_SELECTED_APPS)
    val suggestionPresentationTypeState: MutableState<SuggestionPresentationType> =
        mutableStateOf(SuggestionPresentationType.BOTH)
    val showErrorsState: MutableState<Boolean> = mutableStateOf(DEFAULT_SHOW_ERRORS)
    val configTypeState: MutableState<String> = mutableStateOf(DEFAULT_CONFIG_TYPE)
    val advancedConfigBodyState: MutableState<String> = mutableStateOf(DEFAULT_ADVANCED_CONFIG_BODY)
    val typingRegexPatternState: MutableState<String> = mutableStateOf(DEFAULT_TYPING_REGEX_PATTERN)
    val typingRegexEnabledState: MutableState<Boolean> =
        mutableStateOf(DEFAULT_TYPING_REGEX_ENABLED)
    val customDebounceState: MutableState<Int> = mutableStateOf(DEFAULT_CUSTOM_DEBOUNCE_MS)
    val suggestionContentTemplateState: MutableState<String> =
        mutableStateOf(DEFAULT_SUGGESTION_CONTENT_TEMPLATE)


    data class PreferenceUpdate(
        val masterSwitch: Boolean? = null,
        val apiType: String? = null,
        val customApiUrl: String? = null,
        val customApiKey: String? = null,
        val customModelName: String? = null,
        val customSystemPrompt: String? = null,
        val temperature: Float? = null,
        val selectedApps: Set<String>? = null,
        val topP: Float? = null,
        val suggestionPresentationType: SuggestionPresentationType? = null,
        val showErrors: Boolean? = null,
        val configType: String? = null,
        val advancedConfigBody: String? = null,
        val typingRegexPattern: String? = null,
        val typingRegexEnabled: Boolean? = null,
        val customDebounceMs: Int? = null,
        val suggestionContentTemplate: String? = null
    )

    /**
     * Helper function to update multiple preferences in a single transaction
     */
    suspend fun updatePreferences(updates: PreferenceUpdate) {
        dataStore.edit { preferences ->
            updates.masterSwitch?.let { preferences[MASTER_SWITCH] = it }
            updates.apiType?.let { preferences[API_TYPE] = it }
            updates.customApiUrl?.let { preferences[CUSTOM_API_URL] = it }
            updates.customApiKey?.let { preferences[CUSTOM_API_KEY] = it }
            updates.customModelName?.let { preferences[CUSTOM_MODEL_NAME] = it }
            updates.customSystemPrompt?.let { preferences[CUSTOM_SYSTEM_PROMPT] = it }
            updates.temperature?.let { preferences[TEMPERATURE] = it }
            updates.topP?.let { preferences[TOP_P] = it }
            updates.suggestionPresentationType?.let {
                preferences[SUGGESTION_PRESENTATION_TYPE] = it.value
            }
            updates.showErrors?.let { preferences[SHOW_ERRORS] = it }
            updates.selectedApps?.let { preferences[SELECTED_APPS] = it }
            updates.configType?.let { preferences[CONFIG_TYPE] = it }
            updates.advancedConfigBody?.let { preferences[ADVANCED_CONFIG_BODY] = it }
            updates.typingRegexPattern?.let { preferences[TYPING_REGEX_PATTERN] = it }
            updates.typingRegexEnabled?.let { preferences[TYPING_REGEX_ENABLED] = it }
            updates.customDebounceMs?.let { preferences[CUSTOM_DEBOUNCE_MS] = it }
            updates.suggestionContentTemplate?.let { preferences[SUGGESTION_CONTENT_TEMPLATE] = it }
        }

    }

    /**
     * Load all preferences from datastore and update the state
     */
    suspend fun loadPreferences() {
        val preferences = dataStore.data.firstOrNull()
        preferences?.let { prefs ->
            masterSwitchState.value = prefs[MASTER_SWITCH] ?: DEFAULT_MASTER_SWITCH
            apiTypeState.value = prefs[API_TYPE] ?: DEFAULT_API_TYPE
            customApiUrlState.value = prefs[CUSTOM_API_URL] ?: DEFAULT_API_URL
            customApiKeyState.value = prefs[CUSTOM_API_KEY] ?: DEFAULT_API_KEY
            customModelNameState.value = prefs[CUSTOM_MODEL_NAME] ?: DEFAULT_MODEL_NAME
            customSystemPromptState.value = prefs[CUSTOM_SYSTEM_PROMPT] ?: DEFAULT_SYSTEM_PROMPT
            temperatureState.value = prefs[TEMPERATURE] ?: DEFAULT_TEMPERATURE
            topPState.value = prefs[TOP_P] ?: DEFAULT_TOP_P
            selectedAppsState.value = prefs[SELECTED_APPS] ?: DEFAULT_SELECTED_APPS
            suggestionPresentationTypeState.value = SuggestionPresentationType.fromInt(
                prefs[SUGGESTION_PRESENTATION_TYPE] ?: DEFAULT_SUGGESTION_PRESENTATION_TYPE
            )
            showErrorsState.value = prefs[SHOW_ERRORS] ?: DEFAULT_SHOW_ERRORS
            configTypeState.value = prefs[CONFIG_TYPE] ?: DEFAULT_CONFIG_TYPE
            advancedConfigBodyState.value =
                prefs[ADVANCED_CONFIG_BODY] ?: DEFAULT_ADVANCED_CONFIG_BODY
            typingRegexPatternState.value =
                prefs[TYPING_REGEX_PATTERN] ?: DEFAULT_TYPING_REGEX_PATTERN
            typingRegexEnabledState.value =
                prefs[TYPING_REGEX_ENABLED] ?: DEFAULT_TYPING_REGEX_ENABLED
            customDebounceState.value = prefs[CUSTOM_DEBOUNCE_MS] ?: DEFAULT_CUSTOM_DEBOUNCE_MS
            suggestionContentTemplateState.value =
                prefs[SUGGESTION_CONTENT_TEMPLATE] ?: DEFAULT_SUGGESTION_CONTENT_TEMPLATE
        }
    }

    /**
     * Update master switch state and persist to datastore
     */
    suspend fun updateMasterSwitch(enabled: Boolean) {
        val wasEnabled = masterSwitchState.value
        masterSwitchState.value = enabled
        updatePreferences(PreferenceUpdate(masterSwitch = enabled))
        if (wasEnabled && !enabled) {
            _disableSelfRequests.tryEmit(Unit)
        }
    }

    /**
     * Update API type state and persist to datastore
     */
    suspend fun updateApiType(type: String) {
        apiTypeState.value = type
        updatePreferences(PreferenceUpdate(apiType = type))
    }

    /**
     * Update custom API URL state and persist to datastore
     */
    suspend fun updateCustomApiUrl(url: String) {
        customApiUrlState.value = url
        updatePreferences(PreferenceUpdate(customApiUrl = url))
    }

    /**
     * Update custom API key state and persist to datastore
     */
    suspend fun updateCustomApiKey(key: String) {
        customApiKeyState.value = key
        updatePreferences(PreferenceUpdate(customApiKey = key))
    }

    /**
     * Update custom model name state and persist to datastore
     */
    suspend fun updateCustomModelName(model: String) {
        customModelNameState.value = model
        updatePreferences(PreferenceUpdate(customModelName = model))
    }

    /**
     * Update custom system prompt state and persist to datastore
     */
    suspend fun updateCustomSystemPrompt(prompt: String) {
        customSystemPromptState.value = prompt
        updatePreferences(PreferenceUpdate(customSystemPrompt = prompt))
    }

    /**
     * Update temperature state and persist to datastore
     */
    suspend fun updateTemperature(temperature: Float) {
        temperatureState.value = temperature
        updatePreferences(PreferenceUpdate(temperature = temperature))
    }

    /**
     * Update top-P state and persist to datastore
     */
    suspend fun updateTopP(topP: Float) {
        topPState.value = topP
        updatePreferences(PreferenceUpdate(topP = topP))
    }

    suspend fun updateSuggestionPresentationType(type: SuggestionPresentationType) {
        suggestionPresentationTypeState.value = type
        updatePreferences(PreferenceUpdate(suggestionPresentationType = type))
    }

    /**
     * Update show errors state and persist to datastore
     */
    suspend fun updateShowErrors(show: Boolean) {
        showErrorsState.value = show
        updatePreferences(PreferenceUpdate(showErrors = show))
    }

    /**
     * Update selected apps state and persist to datastore
     */
    suspend fun updateSelectedApps(apps: Set<String>) {
        selectedAppsState.value = apps
        updatePreferences(PreferenceUpdate(selectedApps = apps))
    }

    /**
     * Update config type state and persist to datastore
     */
    suspend fun updateConfigType(type: String) {
        configTypeState.value = type
        updatePreferences(PreferenceUpdate(configType = type))
    }

    /**
     * Update advanced config JSON state and persist to datastore
     */
    suspend fun updateAdvancedConfigBody(json: String) {
        advancedConfigBodyState.value = json
        updatePreferences(PreferenceUpdate(advancedConfigBody = json))
    }

    /**
     * Update typing regex pattern state and persist to datastore
     */
    suspend fun updateTypingRegexPattern(pattern: String) {
        typingRegexPatternState.value = pattern
        updatePreferences(PreferenceUpdate(typingRegexPattern = pattern))
    }

    /**
     * Update typing regex enabled state and persist to datastore
     */
    suspend fun updateTypingRegexEnabled(enabled: Boolean) {
        typingRegexEnabledState.value = enabled
        updatePreferences(PreferenceUpdate(typingRegexEnabled = enabled))
    }

    /**
     * Update custom debounce milliseconds state and persist to datastore
     */
    suspend fun updateCustomDebounceMs(debounceMs: Int) {
        customDebounceState.value = debounceMs
        updatePreferences(PreferenceUpdate(customDebounceMs = debounceMs))
    }

    /**
     * Update suggestion content template state and persist to datastore
     */
    suspend fun updateSuggestionContentTemplate(template: String) {
        suggestionContentTemplateState.value = template
        updatePreferences(PreferenceUpdate(suggestionContentTemplate = template))
    }

    /**
     * Get current master switch value (for backward compatibility)
     */
    suspend fun getMasterSwitch(): Boolean {
        return masterSwitchState.value
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/data/SuggestionPresentationType.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/data/SuggestionPresentationType.kt" 
package app.coreply.coreplyapp.data

enum class SuggestionPresentationType(val value: Int) {
    BUBBLE(0),
    INLINE(1),
    BOTH(2);
    companion object {
        fun fromInt(value: Int): SuggestionPresentationType {
            return entries.firstOrNull { it.value == value } ?: BOTH
        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/network/CustomAPISuggestionRequester.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/network/CustomAPISuggestionRequester.kt" 
package app.coreply.coreplyapp.network

import android.util.Log
import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.suggestions.TypingInfo
import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.ChatRole
import com.aallam.openai.api.core.RequestOptions
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import com.samskivert.mustache.Mustache
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

object CustomAPISuggestionRequester : SuggestionRequester {
    override suspend fun requestSuggestionsFromServer(
        typingInfo: TypingInfo, preferencesManager: PreferencesManager
    ): String {
        val customApiMode = preferencesManager.configTypeState.value

        if (customApiMode == "advanced") {
            val fullUrl = preferencesManager.customApiUrlState.value
            val template = preferencesManager.advancedConfigBodyState.value
            val contextMap = typingInfo.contextMap

            // Compile and execute the mustache template
            val compiledTemplate = Mustache.compiler().escapeHTML(false).compile(template)
            val renderedJson = compiledTemplate.execute(contextMap)

            // Make HTTP request using OkHttp
            val client = OkHttpClient()
            val mediaType = "application/json".toMediaTypeOrNull()
            val requestBody = renderedJson.toRequestBody(mediaType)

            val request = Request.Builder()
                .url(fullUrl)
                .post(requestBody)
                .addHeader("Authorization", "Bearer ${preferencesManager.customApiKeyState.value}")
                .addHeader("HTTP-Referer", "https://coreply.app")
                .addHeader("X-Title", "Coreply: Autocomplete for Texting")
                .build()

            val response = client.newCall(request).execute()
            val responseBody = response.body.string()


            // Parse the response to extract the completion text
            val jsonResponse = JSONObject(responseBody)
            val choices = jsonResponse.getJSONArray("choices")
            if (choices.length() == 0) {
                return ""
            }
            val firstChoice = choices.getJSONObject(0)
            val message = firstChoice.getJSONObject("message")
            val completionText = message.getString("content")

            // Apply suggestion content template using mustache
            val suggestionTemplate = preferencesManager.suggestionContentTemplateState.value
                .ifBlank { "{{assistantMessage}}" }
            val suggestionContextMap = contextMap.toMutableMap()
            suggestionContextMap["assistantMessage"] = completionText
            suggestionContextMap["assistantMessageAutoTrimCurrentTyping"] = if (completionText.startsWith(typingInfo.currentTyping)) completionText.substring(typingInfo.currentTyping.length) else completionText
            suggestionContextMap["assistantMessageAutoTrimCurrentTypingTrimmed"] = if (completionText.startsWith(typingInfo.currentTypingTrimmed)) completionText.substring(typingInfo.currentTypingTrimmed.length) else completionText
            val compiledSuggestionTemplate = Mustache.compiler().escapeHTML(false).compile(suggestionTemplate)
            val finalSuggestion = compiledSuggestionTemplate.execute(suggestionContextMap)

            return finalSuggestion.trim()

        } else {
            var baseUrl = preferencesManager.customApiUrlState.value
            if (!baseUrl.endsWith("/")) {
                baseUrl += "/"
            }
            val host = OpenAIHost(
                baseUrl = baseUrl,
            )
            val config = OpenAIConfig(
                host = host,
                token = preferencesManager.customApiKeyState.value,
            )
            val modelName = preferencesManager.customModelNameState.value

            val openAI = OpenAI(config)

            var userPrompt = "Given this chat history\n" +
                    typingInfo.pastMessages.getCoreply2Format() + "\nIn addition to the message I sent,\n" +
                    "What else should I send? Or start a new topic?"
            if (typingInfo.currentTyping.isNotBlank()) {
                userPrompt += "The reply should start with '${
                    typingInfo.currentTyping.replace(
                        "\\s+".toRegex(),
                        " "
                    )
                }'\n"
            }
            val request = ChatCompletionRequest(
                temperature = preferencesManager.temperatureState.value.toDouble(),
                model = ModelId(modelName),
                topP = preferencesManager.topPState.value.toDouble(),
                maxTokens = 1000,
                messages = listOf(
                    ChatMessage(
                        role = ChatRole.System,
                        content = preferencesManager.customSystemPromptState.value.takeIf { it.isNotBlank() }
                            ?: "You are an AI texting assistant. You will be given a list of text messages between the user (indicated by 'Message I sent:'), and other people (indicated by their names or simply 'Message I received:'). You may also receive a screenshot of the conversation. Your job is to suggest the next message the user should send. Match the tone and style of the conversation. The user may request the message start or end with a certain prefix (both could be parts of a longer word) . The user may quote a specific message. In this case, make sure your suggestions are about the quoted message.\nOutput the suggested text only. Do not output anything else. Do not surround output with quotation marks"
                    ),
                    ChatMessage(
                        role = ChatRole.User,
                        content = userPrompt
                    ),
                    ),
            )
            //Log.v("CallAI", "Requesting suggestions with prompt: $userPrompt")
            val response = openAI.chatCompletion(
                request,
                // Headers for Openrouter
                RequestOptions(
                    headers = mapOf(
                        "HTTP-Referer" to "https://coreply.app",
                        "X-Title" to "Coreply: Autocomplete for Texting"
                    )
                )
            )
            //Log.v("CallAI", "Response: ${response.choices.first().message.content?.trim()}")
            return response.choices.first().message.content?.trim() ?: ""
        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/network/FIMSuggestionRequester.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/network/FIMSuggestionRequester.kt" 
package app.coreply.coreplyapp.network

import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.suggestions.TypingInfo
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody

object FIMSuggestionRequester : SuggestionRequester {
    override suspend fun requestSuggestionsFromServer(
        typingInfo: TypingInfo, preferencesManager: PreferencesManager
    ): String {
        var baseUrl = preferencesManager.customApiUrlState.value
        if (!baseUrl.endsWith("/")) {
            baseUrl += "/"
        }
        val modelName = preferencesManager.customModelNameState.value

        val userPrompt =
            "# Mocking a texting conversation. Messages never repeat. send_message() sends a message. mock_received() means receiving a message from others.\n# Start of Chat History\n" +
                    typingInfo.pastMessages.getFIMFormat() + "\n" +
                    "# Craft a new text\nsend_message(\"" + typingInfo.currentTyping.replace(
                "\\s+".toRegex(),
                " "
            )

        val client = okhttp3.OkHttpClient()
        val mediaType = "application/json".toMediaTypeOrNull()
        val requestBody = org.json.JSONObject().apply {
            put("model", modelName)
            put("temperature", preferencesManager.temperatureState.value.toDouble())
            put("top_p", preferencesManager.topPState.value.toDouble())
            put("max_tokens", 100)
            put("stream", false)
            put("stop", "\")")
            put("suffix", "\")")
            put("prompt", userPrompt)
        }.toString().toRequestBody(mediaType)

        val request = okhttp3.Request.Builder()
            .url("${baseUrl}completions") // Replace with actual endpoint
            .post(requestBody)
            .addHeader("Authorization", "Bearer ${preferencesManager.customApiKeyState.value}")
            .addHeader("Content-Type", "application/json")
            .build()

        val response = client.newCall(request).execute()
        val responseBody = response.body?.string() ?: ""
        //Log.v("CallAI", "Response: $responseBody")
        val jsonResponse = org.json.JSONObject(responseBody)
        val choices = jsonResponse.getJSONArray("choices")
        val message = choices.getJSONObject(0).getJSONObject("message")
        val completionText = message.getString("content")
        return (typingInfo.currentTyping.replace("\\s+".toRegex(), " ") + completionText).trim()

    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/network/SuggestionRequester.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/network/SuggestionRequester.kt" 
package app.coreply.coreplyapp.network

import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.suggestions.TypingInfo

interface SuggestionRequester {
    suspend fun requestSuggestionsFromServer(typingInfo: TypingInfo, preferencesManager: PreferencesManager): String
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/CallAI.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/CallAI.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.suggestions

import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.network.CustomAPISuggestionRequester
import app.coreply.coreplyapp.network.FIMSuggestionRequester
import app.coreply.coreplyapp.network.SuggestionRequester
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch


@OptIn(FlowPreview::class)
open class CallAI(
    open val suggestionStorage: SuggestionStorage,
    private val preferencesManager: PreferencesManager
) {
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
    private val networkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    // Flow to handle debouncing of user input
    private val _userInputFlow = MutableSharedFlow<TypingInfo>(replay = 1)
    val userInputFlow: MutableSharedFlow<TypingInfo>
        get() = _userInputFlow

    init {
        // Launch a coroutine to collect debounced user input and fetch suggestions
        coroutineScope.launch {
            _userInputFlow
                .debounce { preferencesManager.customDebounceState.value.toLong() }
                .collect { typingInfo ->
                    networkScope.launch {
                        fetchSuggestions(typingInfo)
                    }
                }
        }
    }


    private suspend fun fetchSuggestions(typingInfo: TypingInfo) {
        try {
            if (typingInfo.currentTyping.isBlank() && typingInfo.pastMessages.chatContents.isEmpty()) {
                // If no current typing and no past messages, do nothing
                return
            }

            // Check regex filter
            if (preferencesManager.typingRegexEnabledState.value) {
                val pattern = preferencesManager.typingRegexPatternState.value
                if (pattern.isNotEmpty()) {
                    val regex = try {
                        Regex(pattern)
                    } catch (e: Exception) {
                        null
                    }
                    if (regex != null && !regex.containsMatchIn(typingInfo.currentTyping)) {
                        return
                    }
                }
            }
            val baseURL = preferencesManager.customApiUrlState.value
            val apiType = preferencesManager.apiTypeState.value
            val suggestionRequester: SuggestionRequester =
                if (baseURL.endsWith("/fim") || baseURL.endsWith("/fim/")) {
                    FIMSuggestionRequester
                } else {
                    CustomAPISuggestionRequester
                }

            var suggestions =
                suggestionRequester.requestSuggestionsFromServer(typingInfo, preferencesManager)
            suggestions = suggestions.replace("\n", " ")
            if (suggestions.startsWith(" ")) {
                suggestions = " " + suggestions.trim()
            }
            suggestionStorage.updateSuggestion(typingInfo, suggestions.trimEnd())
        } catch (e: Exception) {
            if (preferencesManager.showErrorsState.value) {
                val errorMessage = e.toString()
                suggestionStorage.listener.onSuggestionError(typingInfo, errorMessage)
            }

        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/SuggestionStorage.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/SuggestionStorage.kt" 
package app.coreply.coreplyapp.suggestions

import app.coreply.coreplyapp.utils.SuggestionUpdateListener
import java.util.concurrent.ConcurrentHashMap
import kotlin.text.startsWith
import kotlin.text.substring

class SuggestionStorage(var listener: SuggestionUpdateListener) {
    private val _suggestionHistory = ConcurrentHashMap<String, String>()
    private val PUNCTUATIONS = listOf(
        "!", "\"", ")", ",", ".", ":",
        ";", "?", "]", "~", ",", "。", ":", ";", "?", ")", "】", "!", "、", "」",
    )
    private val PUNCTUATIONS_REGEX = "(?=[!\")\\],.:;?~,。:;?)】!、」])".toRegex()

    fun splitAndKeepPunctuations(text: String): List<String> {
        val parts = text.split(PUNCTUATIONS_REGEX).filter { it.isNotEmpty() }

        if (parts.size < 2) return parts

        // Check if the last part is just punctuation
        else {
            val lastPart = parts.last()
            if (lastPart.length == 1 && PUNCTUATIONS.contains(lastPart)) {
                // Merge the last punctuation with the second-to-last part
                val modifiedParts = parts.dropLast(2).toMutableList()
                modifiedParts.add(parts[parts.size - 2] + lastPart)
                return modifiedParts
            }
        }
        return parts
    }


    // Remove all punctuations from the text, remove whitespaces, and lower all characters
    fun getKeyFromText(text: String): String {
        var key = text.trim()
        for (punctuation in PUNCTUATIONS) {
            key = key.replace(punctuation, "")
        }
        key = key.replace(" ", "")
        key = key.lowercase()
        if (!text.isBlank() && PUNCTUATIONS.contains(text.last().toString())) {
            key += "-"

        }
        return key
    }

    fun String.replaceWhiteSpaces(): String {
        return this.replace("\\s+".toRegex(), " ")
    }

    fun String?.removeMessageISent(): String {
        //Log.v("CallAI", "Response: $this")
        if (this == null) return ""
        else if (this.startsWith("Message I sent: ")) {
            return this.substring("Message I sent: ".length)
        } else if (this.startsWith("Message I received: ")) {
            return this.substring("Message I received: ".length)
        }
        return this
    }

    fun getSuggestion(toBeCompleted: String): String? {
        if (toBeCompleted.isBlank()) {
            if (_suggestionHistory.containsKey("")) {
                return _suggestionHistory[""]!!
            }
        }
        for (i in 0..toBeCompleted.length) {
            val target: String = getKeyFromText(toBeCompleted.substring(0, i))
            if (_suggestionHistory.containsKey(target)) {
                val starting = toBeCompleted.substring(i)
                val suggestion = _suggestionHistory[target]!!
                if (starting.isEmpty() || (suggestion.startsWith(starting) &&
                            suggestion.length > starting.length)
                ) {
                    return suggestion.substring(starting.length)
                }
            }
        }
        return null;
    }

    fun clearSuggestion() {
        _suggestionHistory.clear()
    }

    fun setSuggestionUpdateListener(listener: SuggestionUpdateListener) {
        this.listener = listener
    }

    fun addSuggestionWithoutReplacement(key: String, suggestion: String) {
        if (!_suggestionHistory.containsKey(key)) {
            _suggestionHistory[key] = suggestion
        }
    }

    fun updateSuggestion(typingInfo: TypingInfo, newSuggestion: String) {
        if (newSuggestion.replaceWhiteSpaces().removeMessageISent().lowercase()
                .startsWith(
                    typingInfo.currentTyping.replaceWhiteSpaces().removeMessageISent().lowercase()
                )
        ) {
            val frontTrimmedSuggestion = newSuggestion.replaceWhiteSpaces().removeMessageISent()
                .substring(
                    typingInfo.currentTyping.replaceWhiteSpaces().removeMessageISent().length
                )
            val splittedText = splitAndKeepPunctuations(frontTrimmedSuggestion)
//            Log.v("CallAI", "Splitted text: $splittedText")
            for (i in 0..splittedText.size - 2) {
//                Log.v("CallAI", getKeyFromText(typingInfo.currentTyping + splittedText.subList(0, i + 1).joinToString("")))
                addSuggestionWithoutReplacement(
                    getKeyFromText(
                        typingInfo.currentTyping + splittedText.subList(
                            0,
                            i + 1
                        ).joinToString("")
                    ), splittedText[i + 1]
                )
            }
            addSuggestionWithoutReplacement(
                getKeyFromText(typingInfo.currentTyping),
                if (splittedText.isNotEmpty()) splittedText[0] else ""
            )
            listener.onSuggestionUpdated()
        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/TypingInfo.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/suggestions/TypingInfo.kt" 
package app.coreply.coreplyapp.suggestions

import app.coreply.coreplyapp.utils.ChatContents
import app.coreply.coreplyapp.utils.TokenizerUtil
import app.coreply.coreplyapp.utils.toTemplateMap

data class TypingInfo(val pastMessages: ChatContents, val currentTyping: String, val pkgName: String = "") {
    private val tokens: List<String> = TokenizerUtil.tokenizeText(currentTyping)

    val currentTypingEndsWithSeparator: Boolean
        get() {
            if (currentTyping.isEmpty()) return false
            val lastChar = currentTyping.last().toString()
            return lastChar == " " || TokenizerUtil.PUNCTUATIONS.contains(lastChar)
        }

    val currentTypingLastToken: String
        get() = tokens.lastOrNull().orEmpty()

    val currentTypingTrimmed: String
        get() = if (tokens.isNotEmpty()) {
            tokens.dropLast(1).joinToString("")
        } else {
            ""
        }

    /**
     * Unified context map for both request body templates and suggestion templates.
     * String fields are maps with raw/jsonEscaped/regexLiteral/regexLiteralEscaped variants.
     *
     * For request body templates (JSON), use: {{currentTyping.jsonEscaped}}
     * For suggestion templates (raw text), use: {{currentTyping.raw}}
     *
     * The "assistantMessage" field should be added by the caller for suggestion templates.
     */
    val contextMap: Map<String, Any?>
        get() {
            val baseMap = mutableMapOf<String, Any?>(
                "pastMessages" to pastMessages.getMessageMapList(),
                "currentTyping" to currentTyping.toTemplateMap(),
                "currentTypingTrimmed" to currentTypingTrimmed.toTemplateMap(),
                "currentTypingLastToken" to currentTypingLastToken.toTemplateMap(),
                "currentTypingEndsWithSeparator" to currentTypingEndsWithSeparator,
                "pkgName" to pkgName.toTemplateMap()
            )

            // Add a dynamic field with dots replaced by underscores, set to true
            if (pkgName.isNotEmpty()) {
                val pkgNameKey = pkgName.replace(".", "_")
                baseMap[pkgNameKey] = true
            }

            return baseMap
        }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Color.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Color.kt" 
package app.coreply.coreplyapp.theme
import androidx.compose.ui.graphics.Color

val primaryLight = Color(0xFF006B60)
val onPrimaryLight = Color(0xFFFFFFFF)
val primaryContainerLight = Color(0xFF9EF2E4)
val onPrimaryContainerLight = Color(0xFF005048)
val secondaryLight = Color(0xFF4A635E)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFCCE8E2)
val onSecondaryContainerLight = Color(0xFF334B47)
val tertiaryLight = Color(0xFF456179)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFCCE5FF)
val onTertiaryContainerLight = Color(0xFF2D4960)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF93000A)
val backgroundLight = Color(0xFFF4FBF8)
val onBackgroundLight = Color(0xFF161D1B)
val surfaceLight = Color(0xFFF4FBF8)
val onSurfaceLight = Color(0xFF161D1B)
val surfaceVariantLight = Color(0xFFDAE5E1)
val onSurfaceVariantLight = Color(0xFF3F4947)
val outlineLight = Color(0xFF6F7977)
val outlineVariantLight = Color(0xFFBEC9C6)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2B3230)
val inverseOnSurfaceLight = Color(0xFFECF2EF)
val inversePrimaryLight = Color(0xFF82D5C8)
val surfaceDimLight = Color(0xFFD5DBD9)
val surfaceBrightLight = Color(0xFFF4FBF8)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFEFF5F2)
val surfaceContainerLight = Color(0xFFE9EFED)
val surfaceContainerHighLight = Color(0xFFE3EAE7)
val surfaceContainerHighestLight = Color(0xFFDDE4E1)

val primaryLightMediumContrast = Color(0xFF003E37)
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
val primaryContainerLightMediumContrast = Color(0xFF1E7A6E)
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val secondaryLightMediumContrast = Color(0xFF223B36)
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
val secondaryContainerLightMediumContrast = Color(0xFF58726D)
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryLightMediumContrast = Color(0xFF1B394F)
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightMediumContrast = Color(0xFF547089)
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val errorLightMediumContrast = Color(0xFF740006)
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
val errorContainerLightMediumContrast = Color(0xFFCF2C27)
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
val backgroundLightMediumContrast = Color(0xFFF4FBF8)
val onBackgroundLightMediumContrast = Color(0xFF161D1B)
val surfaceLightMediumContrast = Color(0xFFF4FBF8)
val onSurfaceLightMediumContrast = Color(0xFF0C1211)
val surfaceVariantLightMediumContrast = Color(0xFFDAE5E1)
val onSurfaceVariantLightMediumContrast = Color(0xFF2E3836)
val outlineLightMediumContrast = Color(0xFF4B5452)
val outlineVariantLightMediumContrast = Color(0xFF656F6D)
val scrimLightMediumContrast = Color(0xFF000000)
val inverseSurfaceLightMediumContrast = Color(0xFF2B3230)
val inverseOnSurfaceLightMediumContrast = Color(0xFFECF2EF)
val inversePrimaryLightMediumContrast = Color(0xFF82D5C8)
val surfaceDimLightMediumContrast = Color(0xFFC1C8C5)
val surfaceBrightLightMediumContrast = Color(0xFFF4FBF8)
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightMediumContrast = Color(0xFFEFF5F2)
val surfaceContainerLightMediumContrast = Color(0xFFE3EAE7)
val surfaceContainerHighLightMediumContrast = Color(0xFFD8DEDC)
val surfaceContainerHighestLightMediumContrast = Color(0xFFCDD3D0)

val primaryLightHighContrast = Color(0xFF00332D)
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
val primaryContainerLightHighContrast = Color(0xFF00534A)
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
val secondaryLightHighContrast = Color(0xFF18302C)
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
val secondaryContainerLightHighContrast = Color(0xFF354E49)
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
val tertiaryLightHighContrast = Color(0xFF0F2F44)
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightHighContrast = Color(0xFF304C63)
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
val errorLightHighContrast = Color(0xFF600004)
val onErrorLightHighContrast = Color(0xFFFFFFFF)
val errorContainerLightHighContrast = Color(0xFF98000A)
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
val backgroundLightHighContrast = Color(0xFFF4FBF8)
val onBackgroundLightHighContrast = Color(0xFF161D1B)
val surfaceLightHighContrast = Color(0xFFF4FBF8)
val onSurfaceLightHighContrast = Color(0xFF000000)
val surfaceVariantLightHighContrast = Color(0xFFDAE5E1)
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
val outlineLightHighContrast = Color(0xFF252E2C)
val outlineVariantLightHighContrast = Color(0xFF414B49)
val scrimLightHighContrast = Color(0xFF000000)
val inverseSurfaceLightHighContrast = Color(0xFF2B3230)
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
val inversePrimaryLightHighContrast = Color(0xFF82D5C8)
val surfaceDimLightHighContrast = Color(0xFFB4BAB8)
val surfaceBrightLightHighContrast = Color(0xFFF4FBF8)
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightHighContrast = Color(0xFFECF2EF)
val surfaceContainerLightHighContrast = Color(0xFFDDE4E1)
val surfaceContainerHighLightHighContrast = Color(0xFFCFD6D3)
val surfaceContainerHighestLightHighContrast = Color(0xFFC1C8C5)

val primaryDark = Color(0xFF82D5C8)
val onPrimaryDark = Color(0xFF003731)
val primaryContainerDark = Color(0xFF005048)
val onPrimaryContainerDark = Color(0xFF9EF2E4)
val secondaryDark = Color(0xFFB1CCC6)
val onSecondaryDark = Color(0xFF1C3531)
val secondaryContainerDark = Color(0xFF334B47)
val onSecondaryContainerDark = Color(0xFFCCE8E2)
val tertiaryDark = Color(0xFFADCAE6)
val onTertiaryDark = Color(0xFF143349)
val tertiaryContainerDark = Color(0xFF2D4960)
val onTertiaryContainerDark = Color(0xFFCCE5FF)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF0E1513)
val onBackgroundDark = Color(0xFFDDE4E1)
val surfaceDark = Color(0xFF0E1513)
val onSurfaceDark = Color(0xFFDDE4E1)
val surfaceVariantDark = Color(0xFF3F4947)
val onSurfaceVariantDark = Color(0xFFBEC9C6)
val outlineDark = Color(0xFF899390)
val outlineVariantDark = Color(0xFF3F4947)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFDDE4E1)
val inverseOnSurfaceDark = Color(0xFF2B3230)
val inversePrimaryDark = Color(0xFF006B60)
val surfaceDimDark = Color(0xFF0E1513)
val surfaceBrightDark = Color(0xFF343B39)
val surfaceContainerLowestDark = Color(0xFF090F0E)
val surfaceContainerLowDark = Color(0xFF161D1B)
val surfaceContainerDark = Color(0xFF1A211F)
val surfaceContainerHighDark = Color(0xFF252B2A)
val surfaceContainerHighestDark = Color(0xFF303635)

val primaryDarkMediumContrast = Color(0xFF98ECDD)
val onPrimaryDarkMediumContrast = Color(0xFF002B26)
val primaryContainerDarkMediumContrast = Color(0xFF4A9E92)
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
val secondaryDarkMediumContrast = Color(0xFFC6E2DC)
val onSecondaryDarkMediumContrast = Color(0xFF112A26)
val secondaryContainerDarkMediumContrast = Color(0xFF7C9691)
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
val tertiaryDarkMediumContrast = Color(0xFFC2E0FC)
val onTertiaryDarkMediumContrast = Color(0xFF06283E)
val tertiaryContainerDarkMediumContrast = Color(0xFF7794AE)
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
val errorDarkMediumContrast = Color(0xFFFFD2CC)
val onErrorDarkMediumContrast = Color(0xFF540003)
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
val backgroundDarkMediumContrast = Color(0xFF0E1513)
val onBackgroundDarkMediumContrast = Color(0xFFDDE4E1)
val surfaceDarkMediumContrast = Color(0xFF0E1513)
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkMediumContrast = Color(0xFF3F4947)
val onSurfaceVariantDarkMediumContrast = Color(0xFFD4DFDB)
val outlineDarkMediumContrast = Color(0xFFAAB4B1)
val outlineVariantDarkMediumContrast = Color(0xFF889290)
val scrimDarkMediumContrast = Color(0xFF000000)
val inverseSurfaceDarkMediumContrast = Color(0xFFDDE4E1)
val inverseOnSurfaceDarkMediumContrast = Color(0xFF252B2A)
val inversePrimaryDarkMediumContrast = Color(0xFF005249)
val surfaceDimDarkMediumContrast = Color(0xFF0E1513)
val surfaceBrightDarkMediumContrast = Color(0xFF3F4644)
val surfaceContainerLowestDarkMediumContrast = Color(0xFF040807)
val surfaceContainerLowDarkMediumContrast = Color(0xFF181F1D)
val surfaceContainerDarkMediumContrast = Color(0xFF232928)
val surfaceContainerHighDarkMediumContrast = Color(0xFF2D3432)
val surfaceContainerHighestDarkMediumContrast = Color(0xFF383F3D)

val primaryDarkHighContrast = Color(0xFFAFFFF1)
val onPrimaryDarkHighContrast = Color(0xFF000000)
val primaryContainerDarkHighContrast = Color(0xFF7ED1C4)
val onPrimaryContainerDarkHighContrast = Color(0xFF000E0B)
val secondaryDarkHighContrast = Color(0xFFDAF6EF)
val onSecondaryDarkHighContrast = Color(0xFF000000)
val secondaryContainerDarkHighContrast = Color(0xFFADC8C2)
val onSecondaryContainerDarkHighContrast = Color(0xFF000E0B)
val tertiaryDarkHighContrast = Color(0xFFE5F1FF)
val onTertiaryDarkHighContrast = Color(0xFF000000)
val tertiaryContainerDarkHighContrast = Color(0xFFA9C6E1)
val onTertiaryContainerDarkHighContrast = Color(0xFF000C18)
val errorDarkHighContrast = Color(0xFFFFECE9)
val onErrorDarkHighContrast = Color(0xFF000000)
val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
val onErrorContainerDarkHighContrast = Color(0xFF220001)
val backgroundDarkHighContrast = Color(0xFF0E1513)
val onBackgroundDarkHighContrast = Color(0xFFDDE4E1)
val surfaceDarkHighContrast = Color(0xFF0E1513)
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkHighContrast = Color(0xFF3F4947)
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
val outlineDarkHighContrast = Color(0xFFE8F2EF)
val outlineVariantDarkHighContrast = Color(0xFFBAC5C2)
val scrimDarkHighContrast = Color(0xFF000000)
val inverseSurfaceDarkHighContrast = Color(0xFFDDE4E1)
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
val inversePrimaryDarkHighContrast = Color(0xFF005249)
val surfaceDimDarkHighContrast = Color(0xFF0E1513)
val surfaceBrightDarkHighContrast = Color(0xFF4B5150)
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
val surfaceContainerLowDarkHighContrast = Color(0xFF1A211F)
val surfaceContainerDarkHighContrast = Color(0xFF2B3230)
val surfaceContainerHighDarkHighContrast = Color(0xFF363D3B)
val surfaceContainerHighestDarkHighContrast = Color(0xFF414846)








```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Theme.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Theme.kt" 
package app.coreply.coreplyapp.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext

private val lightScheme = lightColorScheme(
    primary = primaryLight,
    onPrimary = onPrimaryLight,
    primaryContainer = primaryContainerLight,
    onPrimaryContainer = onPrimaryContainerLight,
    secondary = secondaryLight,
    onSecondary = onSecondaryLight,
    secondaryContainer = secondaryContainerLight,
    onSecondaryContainer = onSecondaryContainerLight,
    tertiary = tertiaryLight,
    onTertiary = onTertiaryLight,
    tertiaryContainer = tertiaryContainerLight,
    onTertiaryContainer = onTertiaryContainerLight,
    error = errorLight,
    onError = onErrorLight,
    errorContainer = errorContainerLight,
    onErrorContainer = onErrorContainerLight,
    background = backgroundLight,
    onBackground = onBackgroundLight,
    surface = surfaceLight,
    onSurface = onSurfaceLight,
    surfaceVariant = surfaceVariantLight,
    onSurfaceVariant = onSurfaceVariantLight,
    outline = outlineLight,
    outlineVariant = outlineVariantLight,
    scrim = scrimLight,
    inverseSurface = inverseSurfaceLight,
    inverseOnSurface = inverseOnSurfaceLight,
    inversePrimary = inversePrimaryLight,
    surfaceDim = surfaceDimLight,
    surfaceBright = surfaceBrightLight,
    surfaceContainerLowest = surfaceContainerLowestLight,
    surfaceContainerLow = surfaceContainerLowLight,
    surfaceContainer = surfaceContainerLight,
    surfaceContainerHigh = surfaceContainerHighLight,
    surfaceContainerHighest = surfaceContainerHighestLight,
)

private val darkScheme = darkColorScheme(
    primary = primaryDark,
    onPrimary = onPrimaryDark,
    primaryContainer = primaryContainerDark,
    onPrimaryContainer = onPrimaryContainerDark,
    secondary = secondaryDark,
    onSecondary = onSecondaryDark,
    secondaryContainer = secondaryContainerDark,
    onSecondaryContainer = onSecondaryContainerDark,
    tertiary = tertiaryDark,
    onTertiary = onTertiaryDark,
    tertiaryContainer = tertiaryContainerDark,
    onTertiaryContainer = onTertiaryContainerDark,
    error = errorDark,
    onError = onErrorDark,
    errorContainer = errorContainerDark,
    onErrorContainer = onErrorContainerDark,
    background = backgroundDark,
    onBackground = onBackgroundDark,
    surface = surfaceDark,
    onSurface = onSurfaceDark,
    surfaceVariant = surfaceVariantDark,
    onSurfaceVariant = onSurfaceVariantDark,
    outline = outlineDark,
    outlineVariant = outlineVariantDark,
    scrim = scrimDark,
    inverseSurface = inverseSurfaceDark,
    inverseOnSurface = inverseOnSurfaceDark,
    inversePrimary = inversePrimaryDark,
    surfaceDim = surfaceDimDark,
    surfaceBright = surfaceBrightDark,
    surfaceContainerLowest = surfaceContainerLowestDark,
    surfaceContainerLow = surfaceContainerLowDark,
    surfaceContainer = surfaceContainerDark,
    surfaceContainerHigh = surfaceContainerHighDark,
    surfaceContainerHighest = surfaceContainerHighestDark,
)

private val mediumContrastLightColorScheme = lightColorScheme(
    primary = primaryLightMediumContrast,
    onPrimary = onPrimaryLightMediumContrast,
    primaryContainer = primaryContainerLightMediumContrast,
    onPrimaryContainer = onPrimaryContainerLightMediumContrast,
    secondary = secondaryLightMediumContrast,
    onSecondary = onSecondaryLightMediumContrast,
    secondaryContainer = secondaryContainerLightMediumContrast,
    onSecondaryContainer = onSecondaryContainerLightMediumContrast,
    tertiary = tertiaryLightMediumContrast,
    onTertiary = onTertiaryLightMediumContrast,
    tertiaryContainer = tertiaryContainerLightMediumContrast,
    onTertiaryContainer = onTertiaryContainerLightMediumContrast,
    error = errorLightMediumContrast,
    onError = onErrorLightMediumContrast,
    errorContainer = errorContainerLightMediumContrast,
    onErrorContainer = onErrorContainerLightMediumContrast,
    background = backgroundLightMediumContrast,
    onBackground = onBackgroundLightMediumContrast,
    surface = surfaceLightMediumContrast,
    onSurface = onSurfaceLightMediumContrast,
    surfaceVariant = surfaceVariantLightMediumContrast,
    onSurfaceVariant = onSurfaceVariantLightMediumContrast,
    outline = outlineLightMediumContrast,
    outlineVariant = outlineVariantLightMediumContrast,
    scrim = scrimLightMediumContrast,
    inverseSurface = inverseSurfaceLightMediumContrast,
    inverseOnSurface = inverseOnSurfaceLightMediumContrast,
    inversePrimary = inversePrimaryLightMediumContrast,
    surfaceDim = surfaceDimLightMediumContrast,
    surfaceBright = surfaceBrightLightMediumContrast,
    surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
    surfaceContainerLow = surfaceContainerLowLightMediumContrast,
    surfaceContainer = surfaceContainerLightMediumContrast,
    surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
    surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)

private val highContrastLightColorScheme = lightColorScheme(
    primary = primaryLightHighContrast,
    onPrimary = onPrimaryLightHighContrast,
    primaryContainer = primaryContainerLightHighContrast,
    onPrimaryContainer = onPrimaryContainerLightHighContrast,
    secondary = secondaryLightHighContrast,
    onSecondary = onSecondaryLightHighContrast,
    secondaryContainer = secondaryContainerLightHighContrast,
    onSecondaryContainer = onSecondaryContainerLightHighContrast,
    tertiary = tertiaryLightHighContrast,
    onTertiary = onTertiaryLightHighContrast,
    tertiaryContainer = tertiaryContainerLightHighContrast,
    onTertiaryContainer = onTertiaryContainerLightHighContrast,
    error = errorLightHighContrast,
    onError = onErrorLightHighContrast,
    errorContainer = errorContainerLightHighContrast,
    onErrorContainer = onErrorContainerLightHighContrast,
    background = backgroundLightHighContrast,
    onBackground = onBackgroundLightHighContrast,
    surface = surfaceLightHighContrast,
    onSurface = onSurfaceLightHighContrast,
    surfaceVariant = surfaceVariantLightHighContrast,
    onSurfaceVariant = onSurfaceVariantLightHighContrast,
    outline = outlineLightHighContrast,
    outlineVariant = outlineVariantLightHighContrast,
    scrim = scrimLightHighContrast,
    inverseSurface = inverseSurfaceLightHighContrast,
    inverseOnSurface = inverseOnSurfaceLightHighContrast,
    inversePrimary = inversePrimaryLightHighContrast,
    surfaceDim = surfaceDimLightHighContrast,
    surfaceBright = surfaceBrightLightHighContrast,
    surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
    surfaceContainerLow = surfaceContainerLowLightHighContrast,
    surfaceContainer = surfaceContainerLightHighContrast,
    surfaceContainerHigh = surfaceContainerHighLightHighContrast,
    surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)

private val mediumContrastDarkColorScheme = darkColorScheme(
    primary = primaryDarkMediumContrast,
    onPrimary = onPrimaryDarkMediumContrast,
    primaryContainer = primaryContainerDarkMediumContrast,
    onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
    secondary = secondaryDarkMediumContrast,
    onSecondary = onSecondaryDarkMediumContrast,
    secondaryContainer = secondaryContainerDarkMediumContrast,
    onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
    tertiary = tertiaryDarkMediumContrast,
    onTertiary = onTertiaryDarkMediumContrast,
    tertiaryContainer = tertiaryContainerDarkMediumContrast,
    onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
    error = errorDarkMediumContrast,
    onError = onErrorDarkMediumContrast,
    errorContainer = errorContainerDarkMediumContrast,
    onErrorContainer = onErrorContainerDarkMediumContrast,
    background = backgroundDarkMediumContrast,
    onBackground = onBackgroundDarkMediumContrast,
    surface = surfaceDarkMediumContrast,
    onSurface = onSurfaceDarkMediumContrast,
    surfaceVariant = surfaceVariantDarkMediumContrast,
    onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
    outline = outlineDarkMediumContrast,
    outlineVariant = outlineVariantDarkMediumContrast,
    scrim = scrimDarkMediumContrast,
    inverseSurface = inverseSurfaceDarkMediumContrast,
    inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
    inversePrimary = inversePrimaryDarkMediumContrast,
    surfaceDim = surfaceDimDarkMediumContrast,
    surfaceBright = surfaceBrightDarkMediumContrast,
    surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
    surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
    surfaceContainer = surfaceContainerDarkMediumContrast,
    surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
    surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)

private val highContrastDarkColorScheme = darkColorScheme(
    primary = primaryDarkHighContrast,
    onPrimary = onPrimaryDarkHighContrast,
    primaryContainer = primaryContainerDarkHighContrast,
    onPrimaryContainer = onPrimaryContainerDarkHighContrast,
    secondary = secondaryDarkHighContrast,
    onSecondary = onSecondaryDarkHighContrast,
    secondaryContainer = secondaryContainerDarkHighContrast,
    onSecondaryContainer = onSecondaryContainerDarkHighContrast,
    tertiary = tertiaryDarkHighContrast,
    onTertiary = onTertiaryDarkHighContrast,
    tertiaryContainer = tertiaryContainerDarkHighContrast,
    onTertiaryContainer = onTertiaryContainerDarkHighContrast,
    error = errorDarkHighContrast,
    onError = onErrorDarkHighContrast,
    errorContainer = errorContainerDarkHighContrast,
    onErrorContainer = onErrorContainerDarkHighContrast,
    background = backgroundDarkHighContrast,
    onBackground = onBackgroundDarkHighContrast,
    surface = surfaceDarkHighContrast,
    onSurface = onSurfaceDarkHighContrast,
    surfaceVariant = surfaceVariantDarkHighContrast,
    onSurfaceVariant = onSurfaceVariantDarkHighContrast,
    outline = outlineDarkHighContrast,
    outlineVariant = outlineVariantDarkHighContrast,
    scrim = scrimDarkHighContrast,
    inverseSurface = inverseSurfaceDarkHighContrast,
    inverseOnSurface = inverseOnSurfaceDarkHighContrast,
    inversePrimary = inversePrimaryDarkHighContrast,
    surfaceDim = surfaceDimDarkHighContrast,
    surfaceBright = surfaceBrightDarkHighContrast,
    surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
    surfaceContainerLow = surfaceContainerLowDarkHighContrast,
    surfaceContainer = surfaceContainerDarkHighContrast,
    surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
    surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)

@Immutable
data class ColorFamily(
    val color: Color,
    val onColor: Color,
    val colorContainer: Color,
    val onColorContainer: Color
)

val unspecified_scheme = ColorFamily(
    Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)

@Composable
fun CoreplyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable() () -> Unit
) {
  val colorScheme = when {
      dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
          val context = LocalContext.current
          if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
      }
      
      darkTheme -> darkScheme
      else -> lightScheme
  }

  MaterialTheme(
    colorScheme = colorScheme,
    typography = AppTypography,
    content = content
  )
}


```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Type.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/theme/Type.kt" 
package app.coreply.coreplyapp.theme

import androidx.compose.material3.Typography
import androidx.compose.ui.text.font.FontFamily

import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.text.googlefonts.Font
import app.coreply.coreplyapp.R

val provider = GoogleFont.Provider(
    providerAuthority = "com.google.android.gms.fonts",
    providerPackage = "com.google.android.gms",
    certificates = R.array.com_google_android_gms_fonts_certs
)

val bodyFontFamily = FontFamily(
    Font(
        googleFont = GoogleFont("Spline Sans"),
        fontProvider = provider,
    )
)

val displayFontFamily = FontFamily(
    Font(
        googleFont = GoogleFont("Spline Sans Mono"),
        fontProvider = provider,
    )
)

// Default Material 3 typography values
val baseline = Typography()

val AppTypography = Typography(
    displayLarge = baseline.displayLarge.copy(fontFamily = displayFontFamily),
    displayMedium = baseline.displayMedium.copy(fontFamily = displayFontFamily),
    displaySmall = baseline.displaySmall.copy(fontFamily = displayFontFamily),
    headlineLarge = baseline.headlineLarge.copy(fontFamily = displayFontFamily),
    headlineMedium = baseline.headlineMedium.copy(fontFamily = displayFontFamily),
    headlineSmall = baseline.headlineSmall.copy(fontFamily = displayFontFamily),
    titleLarge = baseline.titleLarge.copy(fontFamily = displayFontFamily),
    titleMedium = baseline.titleMedium.copy(fontFamily = displayFontFamily),
    titleSmall = baseline.titleSmall.copy(fontFamily = displayFontFamily),
    bodyLarge = baseline.bodyLarge.copy(fontFamily = bodyFontFamily),
    bodyMedium = baseline.bodyMedium.copy(fontFamily = bodyFontFamily),
    bodySmall = baseline.bodySmall.copy(fontFamily = bodyFontFamily),
    labelLarge = baseline.labelLarge.copy(fontFamily = bodyFontFamily),
    labelMedium = baseline.labelMedium.copy(fontFamily = bodyFontFamily),
    labelSmall = baseline.labelSmall.copy(fontFamily = bodyFontFamily),
)


```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/Overlay.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/Overlay.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.ui

import android.content.Context
import android.content.ContextWrapper
import android.graphics.Paint
import android.graphics.PixelFormat
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.WindowManager
import android.view.accessibility.AccessibilityNodeInfo
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import app.coreply.coreplyapp.applistener.AppSupportStatus
import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.data.SuggestionPresentationType
import app.coreply.coreplyapp.suggestions.CallAI
import app.coreply.coreplyapp.suggestions.SuggestionStorage
import app.coreply.coreplyapp.theme.CoreplyTheme
import app.coreply.coreplyapp.ui.compose.InlineSuggestionOverlay
import app.coreply.coreplyapp.ui.compose.LifeCycleThings
import app.coreply.coreplyapp.ui.compose.TrailingSuggestionOverlay
import app.coreply.coreplyapp.ui.viewmodel.OverlayUiState
import app.coreply.coreplyapp.ui.viewmodel.OverlayViewModel
import app.coreply.coreplyapp.utils.PixelCalculator
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlin.math.min

private const val OVERLAY_LOGO_EXTRA_DP = 15

/**
 * Created on 1/16/17.
 */

class Overlay(
    context: Context,
    val windowManager: WindowManager,
) : ContextWrapper(context), ViewModelStoreOwner {

    private var pixelCalculator: PixelCalculator = PixelCalculator(this)
    private var mainParams: WindowManager.LayoutParams = WindowManager.LayoutParams()
    private var trailingParams: WindowManager.LayoutParams = WindowManager.LayoutParams()
    private var inlineComposeView: ComposeView
    private var trailingComposeView: ComposeView
    private var _viewModel: OverlayViewModel
    private var DP8 = pixelCalculator.dpToPx(8)
    private var DP48 = pixelCalculator.dpToPx(48)
    private var DP20 = pixelCalculator.dpToPx(20)
    private var overlayLogoExtraWidth = pixelCalculator.dpToPx(OVERLAY_LOGO_EXTRA_DP)

    private val dummyPaint: Paint = Paint().apply {
        isAntiAlias = true
        typeface = android.graphics.Typeface.DEFAULT
        textSize = 48f // S
    }

    override val viewModelStore = ViewModelStore()
    private val lifeCycleThings = LifeCycleThings()
    private val preferencesManager: PreferencesManager = PreferencesManager.getInstance(context)

    val viewModel: OverlayViewModel
        get() = _viewModel

    init {
        _viewModel = ViewModelProvider(this)[OverlayViewModel::class.java]
        MainScope().launch {
            preferencesManager.loadPreferences()
            _viewModel.uiState.collect { uiState ->
                updateFromState(uiState)
            }
        }
        val suggestionStorage = SuggestionStorage(_viewModel)
        val ai = CallAI(suggestionStorage, preferencesManager)
        _viewModel.supplyExtras(ai.userInputFlow, suggestionStorage)


        // Create ComposeViews with click handlers pointing to Overlay methods
        inlineComposeView = ComposeView(this).apply {
            setViewTreeLifecycleOwner(lifeCycleThings)
            setViewTreeSavedStateRegistryOwner(lifeCycleThings)
            setViewTreeViewModelStoreOwner(this@Overlay)
            setContent {
                CoreplyTheme {
                    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
                    if (getInlineText().isNotBlank()) {
                        InlineSuggestionOverlay(
                            text = uiState.content.fullText.trimEnd(),
                            textSize = pixelCalculator.pxToSp(uiState.inlineTextSize),
                            showBackground = uiState.showBubbleBackground,
                            onClick = { onInlineClick() },
                            onLongClick = { onInlineLongClick() }
                        )
                    }
                }
            }
        }

        trailingComposeView = ComposeView(this).apply {
            setViewTreeLifecycleOwner(lifeCycleThings)
            setViewTreeSavedStateRegistryOwner(lifeCycleThings)
            setViewTreeViewModelStoreOwner(this@Overlay)
            setContent {
                CoreplyTheme {
                    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
                    if (getBubbleText().isNotBlank()) {
                        val showInlineLogo = getInlineText().isNotBlank() && uiState.showBubbleBackground
                        TrailingSuggestionOverlay(
                            text = uiState.content.fullText.trimEnd(),
                            onClick = { onTrailingClick() },
                            onLongClick = { onTrailingLongClick() },
                            isError = uiState.content.type == OverlayContentType.ERROR,
                            showLogo = !showInlineLogo
                        )
                    }
                }
            }
        }

        // Configure window parameters for inline overlay
        mainParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
        mainParams.flags =
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        mainParams.format = PixelFormat.TRANSLUCENT
        mainParams.gravity = Gravity.TOP or Gravity.START
        mainParams.height = DP48
        mainParams.alpha = 1.0f
        if (Build.VERSION.SDK_INT >= 30) {
            mainParams.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        } else if (Build.VERSION.SDK_INT >= 28) {
            mainParams.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
        }

        // Credit: https://stackoverflow.com/questions/39671343/how-to-move-a-view-via-windowmanager-updateviewlayout-without-any-animation
        val className = "android.view.WindowManager\$LayoutParams"
        try {
            val layoutParamsClass = Class.forName(className)
            val privateFlags = layoutParamsClass.getField("privateFlags")
            val noAnim = layoutParamsClass.getField("PRIVATE_FLAG_NO_MOVE_ANIMATION")

            var privateFlagsValue = privateFlags.getInt(mainParams)
            val noAnimFlag = noAnim.getInt(mainParams)
            privateFlagsValue = privateFlagsValue or noAnimFlag
            privateFlags.setInt(mainParams, privateFlagsValue)
        } catch (e: Exception) {
            Log.e("EXCEPT", "EXCEPTION: ${e.localizedMessage}")
        }

        // Configure window parameters for trailing overlay
        trailingParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
        trailingParams.flags =
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        trailingParams.format = PixelFormat.TRANSLUCENT
        trailingParams.gravity = Gravity.TOP or Gravity.START
        trailingParams.height = DP20
        trailingParams.alpha = 1.0f
        trailingParams.x = DP8
        if (Build.VERSION.SDK_INT >= 30) {
            trailingParams.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        } else if (Build.VERSION.SDK_INT >= 28) {
            trailingParams.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
        }
    }

    // Update reactive state method to use shared state directly
    fun updateFromState(state: OverlayUiState) {
        if (state.isRunning) {
            update()
        } else {
            removeOverlays()
        }
    }

    // Text action methods now use shared state and pre-tokenized content
    fun onInlineClick() {
        val uiState = viewModel.uiState.value
        performTextAction(uiState.content)
    }

    fun onInlineLongClick() {
        val uiState = viewModel.uiState.value
        performFullTextAction(uiState.content)
    }

    fun onTrailingClick() {
        val uiState = viewModel.uiState.value
        performTextAction(uiState.content)
    }

    fun onTrailingLongClick() {
        val uiState = viewModel.uiState.value
        performFullTextAction(uiState.content)
    }

    private fun performTextAction(content: OverlayContent) {
        val arguments = Bundle()
        val addText = content.getFirstToken()

        val currentState = viewModel.uiState.value
        if (currentState.currentInput?.isShowingHintText == true || currentState.currentStatus == AppSupportStatus.HINT_TEXT) {
            arguments.putCharSequence(
                AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
                addText
            )
        } else {
            Log.v("CoWA", "Performing text action with addText: ${currentState.currentInput?.text}")
            arguments.putCharSequence(
                AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
                currentState.currentTyping.replace("Compose Message", "") + addText
            )
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && currentState.currentInputMethod?.currentInputConnection != null) {
            currentState.currentInputMethod?.currentInputConnection?.setSelection(
                currentState.currentTyping.length,
                currentState.currentTyping.length
            )
            currentState.currentInputMethod?.currentInputConnection?.commitText(addText, 1, null)
        } else {
            currentState.currentInput?.performAction(
                AccessibilityNodeInfo.ACTION_SET_TEXT,
                arguments
            )
        }


    }

    private fun performFullTextAction(content: OverlayContent) {
        val arguments = Bundle()
        val currentState = viewModel.uiState.value

        // Build the text we want to set/commit depending on whether hint text is showing
        val toSet = if (currentState.currentInput?.isShowingHintText == true || currentState.currentStatus == AppSupportStatus.HINT_TEXT) {
            content.fullText.trimEnd()
        } else {
            currentState.currentTyping.replace(
                "Compose Message",
                ""
            ) + content.fullText.trimEnd()
        }

        // Prepare arguments for the Accessibility action (used on older APIs)
        arguments.putCharSequence(
            AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
            toSet
        )

        // On newer APIs prefer committing text via the input connection (mirrors performTextAction)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && currentState.currentInputMethod?.currentInputConnection != null) {
            currentState.currentInputMethod?.currentInputConnection?.setSelection(
                currentState.currentTyping.length,
                currentState.currentTyping.length
            )
            currentState.currentInputMethod?.currentInputConnection?.commitText(content.fullText.trimEnd(), 1, null)
        } else {
            currentState.currentInput?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
        }
    }

    fun getInlineText(): String {
        val content = viewModel.uiState.value.content
        return if (preferencesManager.suggestionPresentationTypeState.value == SuggestionPresentationType.BUBBLE || content.type == OverlayContentType.ERROR) ""
        else content.fullText.trimEnd()
    }

    fun getBubbleText(): String {
        val content = viewModel.uiState.value.content
        dummyPaint.textSize = viewModel.uiState.value.inlineTextSize
        val textWidth = dummyPaint.measureText(content.fullText.trimEnd())
        return when {
            preferencesManager.suggestionPresentationTypeState.value == SuggestionPresentationType.INLINE -> ""
            preferencesManager.suggestionPresentationTypeState.value == SuggestionPresentationType.BUBBLE -> content.fullText.trimEnd()
            textWidth > viewModel.uiState.value.chatEntryWidth -> content.fullText.trimEnd()
            else -> ""
        }
    }


    fun update() {
        val uiState = viewModel.uiState.value
        if (uiState.isRunning) {
            uiState.rect?.let { chatEntryRect ->
                // Update positioning
                //Log.v("CoWA", "Overlay update: mainParams.y=${mainParams.y}")
                mainParams.y = chatEntryRect.top
                mainParams.height = chatEntryRect.bottom - chatEntryRect.top

                // Update background and positioning based on status
                val showBubbleBackground = uiState.showBubbleBackground
                viewModel.updateBackgroundVisibility(showBubbleBackground)

                val inlineText = getInlineText()
                val bubbleText = getBubbleText()
                val inlineShowsLogo = inlineText.isNotBlank() && showBubbleBackground
                val trailingShowsLogo = bubbleText.isNotBlank() && !inlineShowsLogo
                val inlineTextWidth = dummyPaint.measureText(inlineText).toInt()
                val trailingTextWidth = dummyPaint.measureText(bubbleText).toInt()

                if (showBubbleBackground) {
                    mainParams.width =
                        min(inlineTextWidth + DP8 * 3 + if (inlineShowsLogo) overlayLogoExtraWidth else 0, uiState.chatEntryWidth + DP8 * 2)
                    mainParams.x = chatEntryRect.right - mainParams.width
                } else {
                    mainParams.width = min(inlineTextWidth + DP8, uiState.chatEntryWidth)
                    mainParams.x = chatEntryRect.left

                }

                trailingParams.y = chatEntryRect.bottom

                // Show/hide overlays based on content and preferences
                if (inlineText.isBlank()) {
                    removeInlineOverlay()
                } else {
                    showInlineOverlay()
                }

                if (bubbleText.isBlank()) {
                    removeTrailingOverlay()
                } else {
                    trailingParams.width = trailingTextWidth + DP20 + DP8 + if (trailingShowsLogo) overlayLogoExtraWidth else 0
                    showTrailingOverlay()
                }

                // Update view layouts if shown
                if (inlineComposeView.isShown) {
                    windowManager.updateViewLayout(inlineComposeView, mainParams)
                }
                if (trailingComposeView.isShown) {
                    windowManager.updateViewLayout(trailingComposeView, trailingParams)
                }
                Log.v("CoWA", "Overlay updated: y=${chatEntryRect.bottom},")


            }
        }
    }

    fun removeOverlays() {
        removeInlineOverlay()
        removeTrailingOverlay()
    }

    fun removeInlineOverlay() {
        if (inlineComposeView.isShown) {
            windowManager.removeView(inlineComposeView)
        }
    }

    fun removeTrailingOverlay() {
        if (trailingComposeView.isShown) {
            windowManager.removeView(trailingComposeView)
        }
    }

    fun showInlineOverlay() {
        if (!inlineComposeView.isShown) {
            windowManager.addView(inlineComposeView, mainParams)
        }
    }

    fun showTrailingOverlay() {
        if (!trailingComposeView.isShown) {
            windowManager.addView(trailingComposeView, trailingParams)
        }
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/OverlayContent.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/OverlayContent.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.ui

import app.coreply.coreplyapp.utils.TokenizerUtil

/**
 * Represents the type of content displayed in the overlay
 */
enum class OverlayContentType {
    SUGGESTION,
    ERROR
}

/**
 * Represents content to be displayed in the overlay
 * with pre-tokenized text and type information
 */
sealed class OverlayContent {
    abstract val fullText: String
    abstract val tokens: List<String>
    abstract val type: OverlayContentType

    /**
     * Get the first token for short insertion
     */
    fun getFirstToken(): String {
        return if (tokens.isNotEmpty()) {
            var firstToken = tokens[0]
            if (firstToken.isBlank() && tokens.size > 1) {
                firstToken += tokens[1]
            }
            firstToken
        } else {
            ""
        }
    }

    /**
     * Suggestion content with pre-tokenized text
     */
    data class Suggestion(
        override val fullText: String,
        override val tokens: List<String>
    ) : OverlayContent() {
        override val type = OverlayContentType.SUGGESTION

        companion object {
            /**
             * Create a suggestion with automatic tokenization
             */
            fun create(text: String): Suggestion {
                return Suggestion(
                    fullText = text,
                    tokens = TokenizerUtil.tokenizeText(text.trimEnd())
                )
            }
        }
    }

    /**
     * Error content (typically not tokenized as it's displayed as-is)
     */
    data class Error(
        override val fullText: String,
        val errorCode: String? = null
    ) : OverlayContent() {
        override val tokens = listOf(fullText)
        override val type = OverlayContentType.ERROR
    }

    companion object {
        /**
         * Empty content when there's nothing to display
         */
        val Empty = Suggestion("", emptyList())
    }
}



```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/LifeCycleThings.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/LifeCycleThings.kt" 
package app.coreply.coreplyapp.ui.compose

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner

class LifeCycleThings: SavedStateRegistryOwner {

    private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
    private var savedStateRegistryController: SavedStateRegistryController =
        SavedStateRegistryController.create(this)

    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry

    override val lifecycle: Lifecycle
        get() = lifecycleRegistry

    init {
        lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
        savedStateRegistryController.performAttach()
        savedStateRegistryController.performRestore(null)
        lifecycleRegistry.currentState = Lifecycle.State.CREATED
        lifecycleRegistry.currentState = Lifecycle.State.STARTED
        lifecycleRegistry.currentState = Lifecycle.State.RESUMED
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/ModernSettingsScreen.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/ModernSettingsScreen.kt" 
package app.coreply.coreplyapp.ui.compose

import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import app.coreply.coreplyapp.AppSelectorActivity
import app.coreply.coreplyapp.R
import app.coreply.coreplyapp.WelcomeActivity
import app.coreply.coreplyapp.data.SuggestionPresentationType
import app.coreply.coreplyapp.ui.viewmodel.SettingsViewModel
import app.coreply.coreplyapp.utils.GlobalPref

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModernSettingsScreen(
    viewModel: SettingsViewModel = viewModel()
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    var expandMenu by remember { mutableStateOf(false) }
    val uiState = viewModel.uiState

    val suggestionPresentationTypeStrings =
        listOf("Bubble below text field only", "Inline only", "Bubble and inline")

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                viewModel.updateMasterSwitchState(context)
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {

        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "Coreply Service",
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(bottom = 8.dp)
            )


            // Master Switch Section
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(modifier = Modifier.weight(1f)) {
                    Text(
                        text = "Enable Coreply",
                        style = MaterialTheme.typography.bodyLarge
                    )
                    Text(
                        text = "Toggle the main Coreply accessibility service",
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }

                Switch(
                    checked = uiState.masterSwitchEnabled,
                    onCheckedChange = { enabled ->
                        if (enabled) {
                            viewModel.setMasterSwitchEnabled(true)
                            val intent = Intent(context, WelcomeActivity::class.java)
                            intent.putExtra(
                                "page",
                                GlobalPref.getFirstRunActivityPageNumber(context)
                            )
                            context.startActivity(intent)
                        } else {
                            viewModel.setMasterSwitchEnabled(false)
                        }
                    }
                )
            }



            ExposedDropdownMenuBox(
                expanded = expandMenu,
                onExpandedChange = { expandMenu = it },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp)
            ) {
                OutlinedTextField(
                    value = suggestionPresentationTypeStrings[uiState.suggestionPresentationType.ordinal],
                    readOnly = true,
                    onValueChange = {},
                    label = { Text("Suggestion mode") },
                    trailingIcon = {
                        ExposedDropdownMenuDefaults.TrailingIcon(
                            expanded = expandMenu
                        )
                    },
                    modifier = Modifier
                        .fillMaxWidth()
                        .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true)

                )
                ExposedDropdownMenu(
                    expanded = expandMenu,
                    onDismissRequest = { expandMenu = false },
                ) {
                    suggestionPresentationTypeStrings.forEachIndexed { index, selectionOption ->
                        DropdownMenuItem(
                            text = { Text(selectionOption) },
                            onClick = {
                                viewModel.updateSuggestionPresentationType(
                                    SuggestionPresentationType.fromInt(index)
                                )
                                expandMenu = false
                            },
                            leadingIcon = {
                                if (uiState.suggestionPresentationType.ordinal == index) {
                                    Icon(
                                        painter = painterResource(R.drawable.check_24px),
                                        contentDescription = null
                                    )
                                }
                            },
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }


            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { viewModel.updateShowErrors(!uiState.showErrors) }
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(modifier = Modifier.weight(1f)) {
                    Text(
                        text = "Show errors",
                        style = MaterialTheme.typography.bodyLarge
                    )
                    Text(
                        text = "Display error messages in the overlay",
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }

                Checkbox(
                    checked = uiState.showErrors,
                    onCheckedChange = { viewModel.updateShowErrors(it) }
                )
            }


            // Select Apps Button

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable {
                        val intent = Intent(context, AppSelectorActivity::class.java)
                        context.startActivity(intent)
                    }
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically,

                ) {
                Column(modifier = Modifier.weight(1f)) {
                    Text(
                        text = "Select Apps",
                        style = MaterialTheme.typography.bodyLarge,
                        fontWeight = FontWeight.Medium
                    )
                    Text(
                        text = "Choose apps enabling Coreply",
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }

            }

        }
        CustomApiSettingsSection(viewModel)

        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "Customizations",
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(bottom = 16.dp)
            )
            // Custom Debounce Slider
            Column(modifier = Modifier.padding(vertical = 8.dp)) {
                Text(
                    text = "Debounce: ${uiState.customDebounceMs}ms",
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
                Slider(
                    value = uiState.customDebounceMs.toFloat(),
                    onValueChange = { viewModel.updateCustomDebounceMs(it.toInt()) },
                    valueRange = 150f..1200f,
                    steps = 20,
                    modifier = Modifier.fillMaxWidth()
                )
                Text(
                    text = "Delay before fetching suggestions",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            // Typing Regex Enabled Checkbox
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { viewModel.updateTypingRegexEnabled(!uiState.typingRegexEnabled) }
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(modifier = Modifier.weight(1f)) {
                    Text(
                        text = "Enable Regex Filter",
                        style = MaterialTheme.typography.bodyLarge
                    )
                    Text(
                        text = "Only fetch suggestions after regex match",
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }

                Checkbox(
                    checked = uiState.typingRegexEnabled,
                    onCheckedChange = { viewModel.updateTypingRegexEnabled(it) }
                )
            }

            // Typing Regex Pattern Text Field
            OutlinedTextField(
                value = uiState.typingRegexPattern,
                onValueChange = { viewModel.updateTypingRegexPattern(it) },
                label = { Text("Typing Regex Pattern") },
                supportingText = { Text("Regex to limit suggestions after matching text") },
                minLines = 1,
                maxLines = 3,
                enabled = uiState.typingRegexEnabled,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp)
            )

        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomApiSettingsSection(viewModel: SettingsViewModel) {
    val uiState = viewModel.uiState
    var showApiKey by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Custom API Settings",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        SettingsInfoCard(
            text = "Suggestion quality depends on the AI model and API configuration you set up. Relevant on-screen text needed for suggestions will be sent to that API. You are responsible for API billing and should review that provider's privacy policy and terms before using it."
        )

        // Config Type Radio Buttons
        Row(modifier = Modifier.padding(bottom = 12.dp)) {
            Row(
                modifier = Modifier
                    .clickable { viewModel.updateConfigType("simple") },
                verticalAlignment = Alignment.CenterVertically
            ) {
                androidx.compose.material3.RadioButton(
                    selected = uiState.configType == "simple",
                    onClick = { viewModel.updateConfigType("simple") }
                )
                Text(
                    text = "Simple",
                    style = MaterialTheme.typography.bodyLarge,
                )
            }

            Row(
                modifier = Modifier
                    .clickable { viewModel.updateConfigType("advanced") }
                    .padding(start = 16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                androidx.compose.material3.RadioButton(
                    selected = uiState.configType == "advanced",
                    onClick = { viewModel.updateConfigType("advanced") }
                )
                Text(
                    text = "Advanced",
                    style = MaterialTheme.typography.bodyLarge,
                )
            }
        }

        // API URL
        OutlinedTextField(

            value = uiState.customApiUrl,
            onValueChange = viewModel::updateCustomApiUrl,
            label = { Text("${if(uiState.configType == "simple") "Base" else "Full"} URL") },
            supportingText = { Text(if (uiState.configType == "simple") "OpenAI-compatible API endpoint" else "Full URL of the request") },
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 12.dp)

        )

        // API Key
        OutlinedTextField(
            value = uiState.customApiKey, onValueChange = viewModel::updateCustomApiKey,
            label = { Text("API Key") },
            supportingText = { Text("Your API authentication key") },
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 12.dp),
            trailingIcon = {
                IconButton(onClick = {
                    showApiKey = !showApiKey
                }) {
                    Icon(
                        painter = painterResource(if (showApiKey) R.drawable.visibility_off_24px else R.drawable.visibility_24px),
                        contentDescription = if (showApiKey) "Hide API Key" else "Show API Key"
                    )
                }
            },
            visualTransformation = if (showApiKey) VisualTransformation.None else PasswordVisualTransformation()
        )

        // Conditional rendering based on config type
        if (uiState.configType == "simple") {
            // Simple mode - show individual fields
            // Model Name
            OutlinedTextField(
                value = uiState.customModelName,
                onValueChange = viewModel::updateCustomModelName,
                label = { Text("Model Name") },
                supportingText = { Text("Model name of the LLM") },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 12.dp)
            )

            // System Prompt
            OutlinedTextField(
                value = uiState.customSystemPrompt,
                onValueChange = viewModel::updateCustomSystemPrompt,
                label = { Text("System Prompt") },
                supportingText = { Text("Instructions for the AI assistant") },
                minLines = 3,
                maxLines = 6,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 16.dp)
            )

            // Temperature Slider
            Column(modifier = Modifier.padding(bottom = 12.dp)) {
                Text(
                    text = "Temperature: ${String.format("%.1f", uiState.temperature)}",
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
                Slider(
                    value = uiState.temperature,
                    onValueChange = viewModel::updateTemperature,
                    valueRange = 0f..1f,
                    steps = 9,

                    )
                Text(
                    text = "Controls randomness in responses",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            // Top-P Slider
            Column {
                Text(
                    text = "Top-P: ${String.format("%.1f", uiState.topP)}",
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
                Slider(
                    value = uiState.topP,
                    onValueChange = viewModel::updateTopP,
                    valueRange = 0f..1f,
                    steps = 9
                )
                Text(
                    text = "Controls diversity of token selection",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        } else {
            // Advanced mode - show JSON text box
            OutlinedTextField(
                value = uiState.advancedConfigBody,
                onValueChange = viewModel::updateAdvancedConfigBody,
                label = { Text("Request Body") },
                supportingText = { Text("The request body to the API, usually a JSON") },
                minLines = 15,
                maxLines = 150,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 12.dp)
            )
            // Suggestion Content template
            OutlinedTextField(
                value = uiState.suggestionContentTemplate,
                onValueChange = viewModel::updateSuggestionContentTemplate,
                label = { Text("Suggestion Content") },
                supportingText = { Text("Mustache template for the final suggestion. Use {{assistantMessage}} for the model response.") },
                minLines = 1,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 12.dp)
            )
        }
    }
}

@Composable
private fun SettingsInfoCard(text: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(bottom = 16.dp),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.secondaryContainer
        )
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSecondaryContainer,
            modifier = Modifier.padding(16.dp)
        )
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/OverlayComposables.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/compose/OverlayComposables.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.ui.compose

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.Image
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.coreply.coreplyapp.R

private val OverlayLogoSize = 12.dp
private val OverlayLogoSpacing = 2.dp
private const val OverlayLogoCropScale = 2f / 1f

@Composable
private fun CoreplyOverlayLogo() {
    Image(
        painter = painterResource(id = R.mipmap.ic_launcher_foreground),
        contentDescription = null,
        modifier = Modifier
            .size(OverlayLogoSize)
            .graphicsLayer(
                scaleX = OverlayLogoCropScale,
                scaleY = OverlayLogoCropScale,
                clip = true
            ),
        contentScale = ContentScale.Crop
    )
}

/**
 * Main inline suggestion overlay that appears over the text input field
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun InlineSuggestionOverlay(
    text: String,
    textSize: Float,
    showBackground: Boolean,
    onClick: () -> Unit,
    onLongClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(
        modifier = modifier
            .wrapContentWidth(
                if (showBackground) {
                    Alignment.End
                } else {
                    Alignment.Start
                }
            )
            .wrapContentHeight(
                if (showBackground) {
                    Alignment.CenterVertically
                } else {
                    Alignment.Bottom
                }
            ) // Adjust height based on background visibility
            .combinedClickable(
                onClick = onClick,
                onLongClick = onLongClick,
                onClickLabel = "Insert first word",
                onLongClickLabel = "Insert full suggestion"
            )
            .then(
                if (showBackground) {
                    Modifier
                        .background(
                            color = MaterialTheme.colorScheme.secondaryContainer,
                            shape = RoundedCornerShape(50.dp)
                        )
                        .padding(horizontal = 8.dp)
                } else {
                    Modifier.background(Color.Transparent)
                }
            ),
        contentAlignment = if (showBackground) {
            Alignment.Center
        } else {
            Alignment.BottomStart
        }
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Text(
                text = text,
                fontSize = textSize.sp,
                color = if (showBackground)
                    MaterialTheme.colorScheme.onSecondaryContainer
                else
                    Color(0xEE999999), // A color that fits both light and dark backgrounds
                style = Typography().bodyMedium,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
            if (showBackground) {
                Spacer(modifier = Modifier.width(OverlayLogoSpacing))
                CoreplyOverlayLogo()
            }
        }

    }
}

/**
 * Trailing suggestion overlay that appears below the text input field
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TrailingSuggestionOverlay(
    text: String,
    onClick: () -> Unit,
    onLongClick: () -> Unit,
    modifier: Modifier = Modifier,
    isError: Boolean = false,
    showLogo: Boolean = true
) {
    Surface(
        modifier = modifier
            .wrapContentWidth(Alignment.Start)
            .fillMaxHeight()
            .combinedClickable(
                onClick = onClick,
                onLongClick = onLongClick,
                onClickLabel = "Insert first word",
                onLongClickLabel = "Insert full suggestion"
            ),
        shape = RoundedCornerShape(12.dp),
        color = if (isError)
            MaterialTheme.colorScheme.errorContainer
        else
            MaterialTheme.colorScheme.secondaryContainer,
    ) {
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .wrapContentWidth(Alignment.Start)
                .padding(horizontal = 10.dp),
            contentAlignment = Alignment.CenterStart
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) {

                Text(
                    text = text,
                    fontSize = 13.sp,
                    color = if (isError)
                        MaterialTheme.colorScheme.onErrorContainer
                    else
                        MaterialTheme.colorScheme.onSecondaryContainer,
                    style = Typography().bodyMedium,
                    textAlign = TextAlign.Start,
                    maxLines = 1,
                    modifier = Modifier.wrapContentWidth(Alignment.Start)
                )
                if (showLogo) {
                    Spacer(modifier = Modifier.width(OverlayLogoSpacing))
                    CoreplyOverlayLogo()

                }
            }
        }
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/AppSelectorViewModel.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/AppSelectorViewModel.kt" 
package app.coreply.coreplyapp.ui.viewmodel

import android.app.Application
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.coreply.coreplyapp.applistener.SupportedApps
import app.coreply.coreplyapp.data.PreferencesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

data class AppInfo(
    val packageName: String,
    val appName: String,
    val appIcon: Drawable?,
    val isSupported: Boolean = false,
)

data class AppSelectorUiState(
    val isLoading: Boolean = true,
    val supportedApps: List<AppInfo> = emptyList(),
    val otherApps: List<AppInfo> = emptyList(),
    val selectedApps: Set<String> = emptySet(),
    val error: String? = null
)

class AppSelectorViewModel(application: Application) : AndroidViewModel(application) {

    private val preferencesManager = PreferencesManager.getInstance(application)
    private val packageManager = application.packageManager

    var uiState by mutableStateOf(AppSelectorUiState())
        private set

    init {
        loadApps()
        loadSelectedApps()
    }

    private fun loadSelectedApps() {
        viewModelScope.launch {
            preferencesManager.loadPreferences()
            uiState = uiState.copy(selectedApps = preferencesManager.selectedAppsState.value)
        }
    }

    private fun loadApps() {
        viewModelScope.launch {
            uiState = uiState.copy(isLoading = true, error = null)

            try {
                val apps = withContext(Dispatchers.IO) {
                    getInstalledApps()
                }

                val (supportedApps, otherApps) = apps.partition { it.isSupported }

                uiState = uiState.copy(
                    isLoading = false,
                    supportedApps = supportedApps,
                    otherApps = otherApps
                )
            } catch (e: Exception) {
                uiState = uiState.copy(
                    isLoading = false,
                    error = "Failed to load apps: ${e.message}"
                )
            }
        }
    }

    private suspend fun getInstalledApps(): List<AppInfo> = withContext(Dispatchers.IO) {
        packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
            .filter { app ->
                // Filter out system apps that users can't interact with
                (app.flags and ApplicationInfo.FLAG_SYSTEM) == 0 ||
                        packageManager.getLaunchIntentForPackage(app.packageName) != null
            }
            .map { app ->
                val appName = packageManager.getApplicationLabel(app).toString()
                val appIcon = try {
                    packageManager.getApplicationIcon(app.packageName)
                } catch (e: Exception) {
                    null
                }

                // Check if app is officially supported
                val supportedApp =
                    SupportedApps.supportedApps.find { it.pkgName == app.packageName }

                AppInfo(
                    packageName = app.packageName,
                    appName = appName,
                    appIcon = appIcon,
                    isSupported = supportedApp != null,
                )
            }
            .sortedWith(compareBy<AppInfo> { !uiState.selectedApps.contains(it.packageName) }.thenBy { it.appName })
    }

    fun toggleAppSelection(packageName: String) {
        val currentSelectedApps = uiState.selectedApps.toMutableSet()
        if (currentSelectedApps.contains(packageName)) {
            currentSelectedApps.remove(packageName)
        } else {
            currentSelectedApps.add(packageName)
        }

        uiState = uiState.copy(selectedApps = currentSelectedApps)

        // Persist to preferences
        viewModelScope.launch {
            preferencesManager.updateSelectedApps(currentSelectedApps)
        }
    }

    fun retryLoadApps() {
        loadApps()
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/OverlayViewModel.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/OverlayViewModel.kt" 
/**
 * coreply
 *
 * Copyright (C) 2024 coreply
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package app.coreply.coreplyapp.ui.viewmodel

import android.accessibilityservice.InputMethod
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo
import androidx.lifecycle.ViewModel
import app.coreply.coreplyapp.applistener.AppSupportStatus
import app.coreply.coreplyapp.applistener.SupportedAppProperty
import app.coreply.coreplyapp.suggestions.SuggestionStorage
import app.coreply.coreplyapp.suggestions.TypingInfo
import app.coreply.coreplyapp.ui.OverlayContent
import app.coreply.coreplyapp.ui.OverlayContentType
import app.coreply.coreplyapp.utils.ChatContents
import app.coreply.coreplyapp.utils.ChatMessage
import app.coreply.coreplyapp.utils.SuggestionUpdateListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.max

enum class RefreshType {
    NORMAL,
    CHAR_LOCATION,
    TEXT_SIZE
}

data class OverlayUiState(
    val inlineTextSize: Float = 48f,
    val showBubbleBackground: Boolean = false,
    val isRunning: Boolean = false,
    val content: OverlayContent = OverlayContent.Empty,
    val rect: Rect? = null,
    val chatEntryWidth: Int = 0,
    var currentInput: AccessibilityNodeInfo? = null,
    var currentMessageListNode: AccessibilityNodeInfo? = null,
    var currentApp: SupportedAppProperty? = null,
    var currentChatContents: ChatContents = ChatContents(),
    var currentStatus: AppSupportStatus = AppSupportStatus.UNKNOWN,
    var currentTyping: String = "-",
    var messageListProcessor: (AccessibilityNodeInfo) -> MutableList<ChatMessage> = { mutableListOf() },
    var currentInputMethod: InputMethod? = null
)

class OverlayViewModel() : ViewModel(), SuggestionUpdateListener {

    private var _uiState = MutableStateFlow(OverlayUiState())
    val uiState: StateFlow<OverlayUiState> = _uiState.asStateFlow()
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
    private var userInputFlow: MutableSharedFlow<TypingInfo>? = null
    private var suggestionStorage: SuggestionStorage? = null

    private var chatContentsInitializedInSession: Boolean = false

    fun updateTextSize(textSize: Float) {
        _uiState.update { state -> state.copy(inlineTextSize = textSize) }
    }

    fun updateBackgroundVisibility(showBackground: Boolean) {
        _uiState.update { state -> state.copy(showBubbleBackground = showBackground) }
    }

    fun updateContent(
        content: OverlayContent,
    ) {
        // For errors, always show as trailing bubble
        if (content.type == OverlayContentType.ERROR) {
            _uiState.update { state ->
                state.copy(
                    content = content,
                    showBubbleBackground = false
                )
            }
            return
        }
        _uiState.update { state ->
            state.copy(
                content = content,
                showBubbleBackground = _uiState.value.currentStatus == AppSupportStatus.HINT_TEXT
            )
        }
    }

    fun updateRect(rect: Rect) {
        _uiState.update { state ->
            state.copy(
                rect = rect,
                chatEntryWidth = rect.right - rect.left
            )
        }
    }

    fun enable(
        currentApp: SupportedAppProperty,
        currentInput: AccessibilityNodeInfo,
        currentMessageListNode: AccessibilityNodeInfo,
        currentInputMethod: InputMethod?
    ) {
        _uiState.update { state ->
            state.copy(
                isRunning = true,
                currentApp = currentApp,
                currentInput = currentInput,
                currentMessageListNode = currentMessageListNode,
                messageListProcessor = currentApp.messageListProcessor,
                currentInputMethod = currentInputMethod
            )
        }
    }

    fun updateInputMethod(inputMethod: InputMethod?) {
        _uiState.update { state ->
            state.copy(
                currentInputMethod = null
            )
        }
        // TODO: Better way should be implemented
        _uiState.update { state ->
            state.copy(
                currentInputMethod = inputMethod
            )
        }
    }

    fun supplyExtras(
        userInputFlow: MutableSharedFlow<TypingInfo>,
        suggestionStorage: SuggestionStorage
    ) {
        this.userInputFlow = userInputFlow
        this.suggestionStorage = suggestionStorage
    }

    fun disable() {
        suggestionStorage?.clearSuggestion()
        _uiState.update { state ->
            state.copy(
                currentTyping = "-",
                isRunning = false,
                content = OverlayContent.Empty,
            )
        }
    }


    fun refresh(
        refreshType: RefreshType,
        refreshText: Boolean,
        defaultTextSizeInPx: Float = 0.0f
    ): Boolean {
        synchronized(lock = this) {
            try {
                return when (refreshType) {
                    RefreshType.NORMAL -> refreshInputNode(refreshText)
                    RefreshType.CHAR_LOCATION -> refreshInputNodeWithCharLocation(
                        refreshText
                    )

                    RefreshType.TEXT_SIZE -> {
                        refreshInputNodeWithTextSize(defaultTextSizeInPx)
                        true
                    }
                }
            } catch (e: IllegalStateException) {
                e.printStackTrace()
//                reset()
                return false
            }

        }

    }

    fun refreshInputNode(refreshText: Boolean = false): Boolean {
        var refreshResult = _uiState.value.currentInput?.refresh() ?: false
        Log.v("OverlayViewModel", "refreshInputNode: refresh result = $refreshResult for app ${_uiState.value.currentApp?.pkgName}")
        if ((_uiState.value.currentInput?.packageName?.contains("telegram")?:false || _uiState.value.currentInput?.packageName?.contains("perplexity")?:false) && !(_uiState.value.currentInput?.className?.contains("EditText")?:true)) {
            // Special handling for Telegram as its EditText sometimes turns into a FrameLayout
            refreshResult = false
        }
        if (!refreshResult) {
            reset()
        } else if (refreshText) {
            refreshText()
        }
        return refreshResult
    }

    fun refreshInputNodeWithCharLocation(refreshText: Boolean = true): Boolean {
        val rect = Rect()
        var status: AppSupportStatus
        // Use EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY to get the cursor position
        val arguments = Bundle()
        arguments.putInt(
            AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX,
            0
        )
        arguments.putInt(
            AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH,
            _uiState.value.currentInput?.text?.length ?: 0
        )

        val refreshResult = _uiState.value.currentInput?.refreshWithExtraData(
            AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY,
            arguments
        ) ?: false
        if (!refreshResult) {
            reset()
        } else {
            val rectArray: Array<RectF?>? = if (Build.VERSION.SDK_INT >= 33) {
                uiState.value.currentInput?.extras?.getParcelableArray(
                    AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY,
                    RectF::class.java
                )
            } else {
                @Suppress("DEPRECATION")
                uiState.value.currentInput?.extras?.getParcelableArray(
                    AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY,
                )?.mapNotNull { it as? RectF }?.toTypedArray()
            }

            uiState.value.currentInput?.getBoundsInScreen(rect)
            // For loop in reverse order to get the last cursor position
            if (rectArray != null && rectArray.any { it != null }) {
                status = AppSupportStatus.TYPING
                var rtl = false
                for (rectF in rectArray) {
                    if (rectF != null) {
                        // Check if is RTL by comparing the distance to left and right edges
                        val distanceToLeft = abs(rectF.left - rect.left)
                        val distanceToRight = abs(rectF.right - rect.right)
                        if (distanceToLeft > distanceToRight) {
                            rtl = true
                        }
                        break
                    }
                }
                for (i in rectArray.indices.reversed()) {
                    val rectF = rectArray[i]
                    if (rectF != null) {
                        if (rtl) {
                            // RTL, align to left edge
                            rect.right = rectF.left.toInt()
                        } else {
                            // LTR, align to right edge
                            rect.left = rectF.right.toInt()
                        }
                        rect.top = rectF.top.toInt()
                        rect.bottom = rectF.bottom.toInt()
                        break
                    }
                }
            } else {
                rect.left += (rect.width() * 0.25).toInt()
                rect.right -= (rect.width() * 0.25).toInt()
                status = AppSupportStatus.HINT_TEXT
            }

            if (uiState.value.currentApp?.pkgName == "com.openai.chatgpt") {
                if (uiState.value.currentInput?.text?.isNotEmpty() == true){
                    // Special handling for ChatGPT app as it provides incorrect cursor position when text is not empty
                    val child = uiState.value.currentInput?.getChild(0)
                    val childRect = Rect()
                    val inputRect = Rect()

                    child?.getBoundsInScreen(childRect)
                    uiState.value.currentInput?.getBoundsInScreen(inputRect)
                    val offsetX = childRect.left - inputRect.left
                    val offsetY = childRect.top - inputRect.top
                    rect.left += offsetX
                    rect.top += offsetY
                    rect.bottom += offsetY
                    rect.right = max(rect.right - offsetX * 3, rect.left+1)
                } else{
                    rect.right -= (rect.width() * 0.25).toInt()
                }

            }
            updateStatus(status)
            updateRect(rect)
            if (refreshText) {
                refreshText()
            }
        }
        return refreshResult
    }

    fun refreshInputNodeWithTextSize(
        defaultTextSizeInPx: Float
    ) {
        if (Build.VERSION.SDK_INT >= 30) {
            val refreshResult = (_uiState.value.currentInput?.refreshWithExtraData(
                AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY,
                Bundle()
            ) ?: false)
                    && _uiState.value.currentInput?.extraRenderingInfo != null
            if (!refreshResult && _uiState.value.currentApp?.pkgName != "com.openai.chatgpt" && _uiState.value.currentApp?.pkgName != "ai.perplexity.app.android") {
                // Seems like cannot refresh rendering info doesn't necessarily mean it needs a reset.
                reset()
            } else {
                updateTextSize(
                    _uiState.value.currentInput?.extraRenderingInfo?.textSizeInPx
                        ?: defaultTextSizeInPx
                )
            }
        } else {
            updateTextSize(defaultTextSizeInPx)
        }

    }

    // Returns true if clear current suggestions is needed
    fun refreshMessageListNode() {
        synchronized(lock = this) {
            try {
                val refreshResult = _uiState.value.currentMessageListNode?.refresh() ?: false
                if (!refreshResult) {
                    reset()
                }
                _uiState.value.currentMessageListNode?.let {
                    val chatMessages = _uiState.value.messageListProcessor(it)
                    val clearSuggestions: Boolean =
                        _uiState.value.currentChatContents.combineChatContents(chatMessages)

                    // On first initialization of chat contents in this session, trigger suggestion update
                    if (!chatContentsInitializedInSession) {
                        chatContentsInitializedInSession = true
                        onEditTextUpdate(_uiState.value.currentTyping)
                    }

                    if (clearSuggestions) {
                        suggestionStorage?.clearSuggestion()
                        if (uiState.value.currentTyping == "") {
                            onEditTextUpdate("")
                        }
                    }
                }
            } catch (e: IllegalStateException) {
            }

        }
    }

    fun updateStatus(newStatus: AppSupportStatus, refreshText: Boolean = true) {
        _uiState.update { state -> state.copy(currentStatus = newStatus) }
        if (refreshText) {
            refreshText()
        }
    }

    fun refreshText() {
        var actualMessage =
            _uiState.value.currentInput?.text?.toString()?.replace("Compose Message", "") ?: ""
        if (_uiState.value.currentStatus == AppSupportStatus.HINT_TEXT || _uiState.value.currentInput?.isShowingHintText ?: true) {
            actualMessage = ""
        }
        if (actualMessage != _uiState.value.currentTyping) {
            _uiState.update { state -> state.copy(currentTyping = actualMessage) }
            onEditTextUpdate(actualMessage)

        }
    }

    fun reset() {
        _uiState.value.currentInput?.recycle()
        _uiState.value.currentMessageListNode?.recycle()
        _uiState.value.currentChatContents.clear()
        _uiState.value.currentInput = null
        _uiState.value.currentMessageListNode = null
        updateInputMethod(null)
        suggestionStorage?.clearSuggestion()
        chatContentsInitializedInSession = false
    }


    fun toTypingInfo(): TypingInfo {
        return TypingInfo(
            pastMessages = _uiState.value.currentChatContents,
            currentTyping = _uiState.value.currentTyping,
            pkgName = _uiState.value.currentApp?.pkgName ?: ""
        )
    }

    override fun onSuggestionUpdated(
    ) {
        suggestionStorage?.let {
            if (_uiState.value.isRunning) {
                val suggestionText =
                    it.getSuggestion(_uiState.value.currentTyping)
                if (suggestionText != null) {
                    updateContent(OverlayContent.Suggestion.create(suggestionText))
                }
            } else {
                it.clearSuggestion()
            }
        }

    }

    override fun onSuggestionError(
        typingInfo: TypingInfo,
        errorMessage: String
    ) {
        if (_uiState.value.isRunning) {
            updateContent(OverlayContent.Error(errorMessage))
        }
    }

    fun onEditTextUpdate(newText: String) {
        // Only process suggestions and emit after chat contents have been initialized in this session
        if (!chatContentsInitializedInSession) {
            return
        }

        suggestionStorage?.let {
            if (it.getSuggestion(newText) != null) {
                val suggestionText = it.getSuggestion(newText)!!
                updateContent(OverlayContent.Suggestion.create(suggestionText))
            } else {
                updateContent(OverlayContent.Empty)
                coroutineScope.launch { userInputFlow?.emit(toTypingInfo()) }
            }
        }

    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/SettingsViewModel.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/ui/viewmodel/SettingsViewModel.kt" 
package app.coreply.coreplyapp.ui.viewmodel

import android.app.Application
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.coreply.coreplyapp.data.PreferencesManager
import app.coreply.coreplyapp.data.SuggestionPresentationType
import app.coreply.coreplyapp.utils.GlobalPref
import kotlinx.coroutines.launch

data class SettingsUiState(
    val masterSwitchEnabled: Boolean = false,
    val apiType: String = "custom",
    val customApiUrl: String = "https://api.openai.com/v1/",
    val customApiKey: String = "",
    val customModelName: String = "gpt-4.1-mini",
    val customSystemPrompt: String = "",
    val temperature: Float = 0.3f,
    val suggestionPresentationType: SuggestionPresentationType = SuggestionPresentationType.BOTH,
    val showErrors: Boolean = false,
    val topP: Float = 0.5f,
    val selectedApps: Set<String> = emptySet(),
    val configType: String = "simple",
    val advancedConfigBody: String = "{}",
    val typingRegexPattern: String = "^.*[\\s.!?,;:]{{contextString}}quot;,
    val typingRegexEnabled: Boolean = false,
    val customDebounceMs: Int = 350,
    val suggestionContentTemplate: String = "{{assistantMessage}}"
)

class SettingsViewModel(application: Application) : AndroidViewModel(application) {
    
    private val preferencesManager = PreferencesManager.getInstance(application)
    
    var uiState by mutableStateOf(SettingsUiState())
        private set

    init {
        // Load preferences from datastore on app launch
        viewModelScope.launch {
            preferencesManager.loadPreferences()
            updateUiStateFromPreferences()
        }
    }
    
    private fun updateUiStateFromPreferences() {
        uiState = SettingsUiState(
            masterSwitchEnabled = preferencesManager.masterSwitchState.value && GlobalPref.isAccessibilityEnabled(getApplication()),
            apiType = preferencesManager.apiTypeState.value,
            customApiUrl = preferencesManager.customApiUrlState.value,
            customApiKey = preferencesManager.customApiKeyState.value,
            customModelName = preferencesManager.customModelNameState.value,
            customSystemPrompt = preferencesManager.customSystemPromptState.value,
            temperature = preferencesManager.temperatureState.value,
            selectedApps = preferencesManager.selectedAppsState.value,
            topP = preferencesManager.topPState.value,
            suggestionPresentationType = preferencesManager.suggestionPresentationTypeState.value,
            showErrors = preferencesManager.showErrorsState.value,
            configType = preferencesManager.configTypeState.value,
            advancedConfigBody = preferencesManager.advancedConfigBodyState.value,
            typingRegexPattern = preferencesManager.typingRegexPatternState.value,
            typingRegexEnabled = preferencesManager.typingRegexEnabledState.value,
            customDebounceMs = preferencesManager.customDebounceState.value,
            suggestionContentTemplate = preferencesManager.suggestionContentTemplateState.value
        )
    }
    
    fun updateMasterSwitchState(context: Context) {
        val isEnabled = GlobalPref.isAccessibilityEnabled(context)
        uiState = uiState.copy(masterSwitchEnabled = isEnabled && preferencesManager.masterSwitchState.value)
    }

    fun setMasterSwitchEnabled(enabled: Boolean) {
        uiState = uiState.copy(masterSwitchEnabled = enabled)
        viewModelScope.launch {
            preferencesManager.updateMasterSwitch(enabled)
        }
    }

    fun updateApiType(type: String) {
        uiState = uiState.copy(apiType = type)
        viewModelScope.launch { 
            preferencesManager.updateApiType(type)
        }
    }
    
    fun updateCustomApiUrl(url: String) {
        uiState = uiState.copy(customApiUrl = url)
        viewModelScope.launch { 
            preferencesManager.updateCustomApiUrl(url)
        }
    }
    
    fun updateCustomApiKey(key: String) {
        uiState = uiState.copy(customApiKey = key)
        viewModelScope.launch { 
            preferencesManager.updateCustomApiKey(key)
        }
    }
    
    fun updateCustomModelName(model: String) {
        uiState = uiState.copy(customModelName = model)
        viewModelScope.launch { 
            preferencesManager.updateCustomModelName(model)
        }
    }
    
    fun updateCustomSystemPrompt(prompt: String) {
        uiState = uiState.copy(customSystemPrompt = prompt)
        viewModelScope.launch { 
            preferencesManager.updateCustomSystemPrompt(prompt)
        }
    }
    
    fun updateTemperature(temperature: Float) {
        uiState = uiState.copy(temperature = temperature)
        viewModelScope.launch { 
            preferencesManager.updateTemperature(temperature)
        }
    }
    
    fun updateTopP(topP: Float) {
        uiState = uiState.copy(topP = topP)
        viewModelScope.launch { 
            preferencesManager.updateTopP(topP)
        }
    }

    fun updateSuggestionPresentationType(type: SuggestionPresentationType) {
        uiState = uiState.copy(suggestionPresentationType = type)
        viewModelScope.launch {
            preferencesManager.updateSuggestionPresentationType(type)
        }
    }

    fun updateShowErrors(show: Boolean) {
        uiState = uiState.copy(showErrors = show)
        viewModelScope.launch {
            preferencesManager.updateShowErrors(show)
        }
    }

    fun updateSelectedApps(apps: Set<String>) {
        uiState = uiState.copy(selectedApps = apps)
        viewModelScope.launch {
            preferencesManager.updateSelectedApps(apps)
        }
    }

    fun updateConfigType(type: String) {
        uiState = uiState.copy(configType = type)
        viewModelScope.launch {
            preferencesManager.updateConfigType(type)
        }
    }

    fun updateAdvancedConfigBody(json: String) {
        uiState = uiState.copy(advancedConfigBody = json)
        viewModelScope.launch {
            preferencesManager.updateAdvancedConfigBody(json)
        }
    }

    fun updateTypingRegexPattern(pattern: String) {
        uiState = uiState.copy(typingRegexPattern = pattern)
        viewModelScope.launch {
            preferencesManager.updateTypingRegexPattern(pattern)
        }
    }

    fun updateTypingRegexEnabled(enabled: Boolean) {
        uiState = uiState.copy(typingRegexEnabled = enabled)
        viewModelScope.launch {
            preferencesManager.updateTypingRegexEnabled(enabled)
        }
    }

    fun updateCustomDebounceMs(debounceMs: Int) {
        uiState = uiState.copy(customDebounceMs = debounceMs)
        viewModelScope.launch {
            preferencesManager.updateCustomDebounceMs(debounceMs)
        }
    }

    fun updateSuggestionContentTemplate(template: String) {
        uiState = uiState.copy(suggestionContentTemplate = template)
        viewModelScope.launch {
            preferencesManager.updateSuggestionContentTemplate(template)
        }
    }
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/ChatContents.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/ChatContents.kt" 
package app.coreply.coreplyapp.utils

import android.util.Log
import app.coreply.coreplyapp.suggestions.TypingInfo


class ChatContents {
    // Contains a list of ChatMessage objects
    var chatContents: MutableList<ChatMessage> = mutableListOf()

    // Add a ChatMessage object to the list
    fun addChatMessage(chatMessage: ChatMessage) {
        chatContents.add(chatMessage)
    }

    fun clear(){
        chatContents.clear()
    }

    // compare the chatContents list with another ChatContents list and combine them if they have ChatMessage objects in common
    // Returns a boolean, true if needs new suggestions
    fun combineChatContents(other: MutableList<ChatMessage>): Boolean {
        if (chatContents.isEmpty() || other.isEmpty()) {
            chatContents = other
            return other.isNotEmpty()
        } else if (chatContents == other) (
            return false
        )
        else {
            // Append new messages to the chatContents list
            if (other[0] in chatContents) {
                val clearCurrentSuggestions = other.last().sender == "Me" && other.size > 1 && chatContents.last() == other[other.size-2]
                // If the second last message in the new messages list is the same as the last message in the old messages list, that means the user has sent a new message
                for (i in other) {
                    if (i !in chatContents) {
                        chatContents.add(i)
                    }
                }
                return clearCurrentSuggestions && chatContents.size>1
            } else if (chatContents[0] in other) {
                // Insert new messages to the top of chatContents list
                for (i in chatContents) {
                    if (i !in other) {
                        other.add(i)
                    }
                }
                chatContents = other
                return false
            } else {
                chatContents = other
                return false
            }
        }
    }

    fun getOpenAIFormat(): MutableList<com.aallam.openai.api.chat.ChatMessage> {
        Log.v("ChatContents", chatContents.toString())
        return chatContents.map {
            com.aallam.openai.api.chat.ChatMessage(
                role = it.getRole(),
                content = it.message
            )
        }.toMutableList()
    }

    /**
     * Return a list of turn objects, where each turn contains consecutive messages from the same sender.
     * Each turn object has fields:
     * - "sent": true if sender is "Me", otherwise false
     * - "received": opposite of sent
     * - "sender": sender name
     * - "messages": list of message maps, each with "sent", "received", "sender", and "content" fields
     */
    fun getMessageMapList(): MutableList<Map<String, Any?>> {
        val turns = mutableListOf<Map<String, Any?>>()
        if (chatContents.isEmpty()) return turns
        
        var currentTurnMessages = mutableListOf<Map<String, Any?>>()
        var currentSender = chatContents[0].sender
        
        for (message in chatContents) {
            if (message.sender == currentSender) {
                // Same sender, add to current turn
                val sent = message.sender == "Me"
                currentTurnMessages.add(
                    mapOf(
                        "sent" to sent,
                        "received" to !sent,
                        "sender" to message.sender.toTemplateMap(),
                        "content" to message.message.toTemplateMap()
                    )
                )
            } else {
                // Different sender, save current turn and start a new one
                val sent = currentSender == "Me"
                turns.add(
                    mapOf<String, Any?>(
                        "sent" to sent,
                        "received" to !sent,
                        "sender" to currentSender.toTemplateMap(),
                        "messages" to currentTurnMessages
                    )
                )
                currentTurnMessages = mutableListOf()
                currentSender = message.sender
                val sent2 = message.sender == "Me"
                currentTurnMessages.add(
                    mapOf(
                        "sent" to sent2,
                        "received" to !sent2,
                        "sender" to message.sender.toTemplateMap(),
                        "content" to message.message.toTemplateMap()
                    )
                )
            }
        }
        
        // Add the last turn
        if (currentTurnMessages.isNotEmpty()) {
            val sent = currentSender == "Me"
            turns.add(
                mapOf<String, Any?>(
                    "sent" to sent,
                    "received" to !sent,
                    "sender" to currentSender.toTemplateMap(),
                    "messages" to currentTurnMessages
                )
            )
        }
        
        return turns
    }

    fun getCoreplyFormat(typingInfo: TypingInfo): MutableList<com.aallam.openai.api.chat.ChatMessage> {
        var msgBlock: String = ">>"
        val msgList: MutableList<com.aallam.openai.api.chat.ChatMessage> = mutableListOf()
        for (i in 0..chatContents.size - 1) {
            msgBlock += chatContents[i].message + "\n>>"
            if (i == chatContents.size - 1) {
                if (chatContents[i].sender == "Me") {
                    if (!typingInfo.currentTyping.isBlank()) {
                        msgBlock = msgBlock.substring(0, msgBlock.length - 2)
                        msgBlock += "// Next line is a message starting with: ${typingInfo.currentTyping}\n>>"
                    }
                    msgBlock += typingInfo.currentTypingTrimmed
                    msgList.add(
                        com.aallam.openai.api.chat.ChatMessage(
                            role = com.aallam.openai.api.chat.ChatRole.Assistant,
                            content = msgBlock
                        )
                    )
                } else {
                    msgList.add(
                        com.aallam.openai.api.chat.ChatMessage(
                            role = com.aallam.openai.api.chat.ChatRole.User,
                            content = msgBlock
                        )
                    )
                    msgList.add(
                        com.aallam.openai.api.chat.ChatMessage(
                            role = com.aallam.openai.api.chat.ChatRole.Assistant,
                            content = if (!typingInfo.currentTyping.isBlank()) "// Next line is a message starting with: '${typingInfo.currentTyping}'\n>>${typingInfo.currentTypingTrimmed}" else ">>"
                        )
                    )
                }
                msgBlock = ">>"
            } else {
                if (chatContents[i].sender != chatContents[i + 1].sender) {
                    msgList.add(
                        com.aallam.openai.api.chat.ChatMessage(
                            role = chatContents[i].getRole(),
                            content = msgBlock
                        )
                    )
                    msgBlock = ">>"
                }
            }
        }
        return msgList
    }

    fun getCoreply2Format(): String{
        var msgBlock: String = ""
        for (msg in chatContents.takeLast(20)) {
            msgBlock += msg.toCoreply2String()
        }
        return msgBlock
    }

    fun getFIMFormat(): String{
        var msgBlock: String = ""
        for (msg in chatContents.takeLast(20)) {
            msgBlock += msg.toFIMString()
        }
        return msgBlock
    }

    override fun toString(): String {
        var str = ""
        for (i in chatContents) {
            str += i.toString() + "\n"
        }
        return str
    }

}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/ChatMessage.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/ChatMessage.kt" 
package app.coreply.coreplyapp.utils

import com.aallam.openai.api.chat.ChatRole


class ChatMessage {
    // Contains properties: sender, message, timestr and override the toString method
    var sender: String = ""
    var message: String = ""
    var timestr: String = ""

    // Constructor for ChatMessage
    constructor(sender: String, message: String, timestr: String) {
        this.sender = sender
        this.message = message
        this.timestr = timestr
    }

    fun getRole(): ChatRole {
        return if (sender == "Me") ChatRole.Assistant else ChatRole.User
    }

    override fun toString(): String {
        return "$sender:$message"
    }

    fun toCoreply2String(): String {
        var str = ""
        if (sender == "Me") {
            str += "Message I sent:\n"
        } else if (sender == "Others") {
            str += "Message I received:\n"
        } else {
            str += "On screen content, unknown sender:\n"
        }
        str += message + "\n"
        return str
    }

    fun toFIMString(): String {
        var str = ""
        if (sender == "Me") {
            str += "send_message(\""
        } else {
            str += "mock_received(\""
        }
        str += message + "\")\n"
        return str
    }

    override fun equals(other: Any?): Boolean {
        return (other is ChatMessage) && (other.sender == sender) && (other.message == message) && (other.timestr == timestr)
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/GlobalPref.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/GlobalPref.kt" 
package app.coreply.coreplyapp.utils

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.provider.Settings
import android.view.accessibility.AccessibilityManager

/**
 * Created on 1/13/17.
 */
object GlobalPref {
    fun isAccessibilityEnabled(context: Context?, activityName: String): Boolean {
        val manager =
            context!!.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
        val infos =
            manager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
        for (info in infos) {
            if (info.settingsActivityName != null && info.settingsActivityName == activityName) return true
        }
        return false
    }

    fun isAccessibilityEnabled(context: Context?): Boolean {
        return isAccessibilityEnabled(context, "app.coreply.coreplyapp.SettingsActivity")
    }

    fun getFirstRunActivityPageNumber(context: Context?, activityName: String): Int {
        if (!isAccessibilityEnabled(context, activityName)) {
            return 2 //page=2 means enable accessibility page
        }
        return 4
    }
    fun getFirstRunActivityPageNumber(context: Context?): Int {
        return getFirstRunActivityPageNumber(context, "app.coreply.coreplyapp.SettingsActivity")
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/JsonEscapeExtension.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/JsonEscapeExtension.kt" 
package app.coreply.coreplyapp.utils

import org.json.JSONObject

/**
 * Extension function to JSON escape a string.
 * Uses JSONObject.quote() which is well-tested, then strips the outer quotes.
 */
fun String.jsonEscape(): String {
    // Use JSONObject.quote() which is well-tested, then strip the outer quotes
    val quoted = JSONObject.quote(this)
    return quoted.substring(1, quoted.length - 1)
}

/**
 * Convert a string to a template map with multiple escape variants.
 * Returns null if the string is empty.
 *
 * The map contains:
 * - "raw": the original unescaped string
 * - "jsonEscaped": JSON-escaped string (for embedding in JSON)
 * - "regexLiteral": regex-escaped string (for use in regex patterns)
 * - "regexLiteralEscaped": first regex-escaped, then JSON-escaped
 */
fun String.toTemplateMap(): Map<String, String>? {
    if (isEmpty()) {
        return null
    }
    val regexLiteral = Regex.escape(this)
    return mapOf(
        "raw" to this,
        "jsonEscaped" to this.jsonEscape(),
        "regexLiteral" to regexLiteral,
        "regexLiteralEscaped" to regexLiteral.jsonEscape()
    )
}

```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/PixelCalculator.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/PixelCalculator.kt" 
package app.coreply.coreplyapp.utils

import android.content.Context
import android.content.ContextWrapper
import android.util.TypedValue

/**
 * Created on 10/15/16.
 */

class PixelCalculator(context: Context?) : ContextWrapper(context) {
    fun dpToPx(dp: Int): Int {
        val scale = resources.displayMetrics.density
        return (dp * scale + 0.5f).toInt()
    }
    fun pxToSp(px: Float): Float {
        return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            // For API 34+ (UPSIDE_DOWN_CAKE) use the recommended method
            TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, px, resources.displayMetrics)
        } else {
            // For older API versions, use the traditional calculation
            px / resources.displayMetrics.scaledDensity
        }
    }

    fun spToPx(sp: Float): Float {
        return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            // For API 34+ (UPSIDE_DOWN_CAKE) use the recommended method
            TypedValue.convertDimensionToPixels(TypedValue.COMPLEX_UNIT_SP, sp, resources.displayMetrics)
        } else {
            // For older API versions, use the traditional calculation
            sp * resources.displayMetrics.scaledDensity
        }
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/PreferenceHelper.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/PreferenceHelper.kt" 
package app.coreply.coreplyapp.utils

import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager

/**
 * Legacy preference helper - for backward compatibility only.
 * Use PreferencesManager for new preference management.
 */
object PreferenceHelper {
    lateinit var preferences: SharedPreferences
    
    fun init(context: Context) {
        preferences = PreferenceManager.getDefaultSharedPreferences(context)
    }

    private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
        val editor = this.edit()
        operation(editor)
        editor.apply()
    }

    /**
     * puts a value for the given [key].
     */
    operator fun SharedPreferences.set(key: String, value: Any?)
            = when (value) {
        is String? -> preferences.edit { it.putString(key, value) }
        is Int -> preferences.edit { it.putInt(key, value) }
        is Boolean -> preferences.edit { it.putBoolean(key, value) }
        is Float -> preferences.edit { it.putFloat(key, value) }
        is Long -> preferences.edit { it.putLong(key, value) }
        else -> throw UnsupportedOperationException("Not yet implemented")
    }

    /**
     * finds a preference based on the given [key].
     * [T] is the type of value
     * @param defaultValue optional defaultValue - will take a default defaultValue if it is not specified
     */
    inline operator fun <reified T : Any> get(key: String, defaultValue: T? = null): T
            = when (T::class) {
        String::class -> preferences.getString(key, defaultValue as? String ?: "") as T
        Int::class -> preferences.getInt(key, defaultValue as? Int ?: -1) as T
        Boolean::class -> preferences.getBoolean(key, defaultValue as? Boolean ?: false) as T
        Float::class -> preferences.getFloat(key, defaultValue as? Float ?: -1f) as T
        Long::class -> preferences.getLong(key, defaultValue as? Long ?: -1) as T
        else -> throw UnsupportedOperationException("Not yet implemented")
    }
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/SuggestionUpdateListener.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/SuggestionUpdateListener.kt" 
package app.coreply.coreplyapp.utils

import app.coreply.coreplyapp.suggestions.TypingInfo

interface SuggestionUpdateListener {
    fun onSuggestionUpdated()
    fun onSuggestionError(typingInfo: TypingInfo, errorMessage: String)
}
```

## /coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/TokenizerUtil.kt

```kt path="/coreply-android/app/src/main/java/app/coreply/coreplyapp/utils/TokenizerUtil.kt" 
package app.coreply.coreplyapp.utils

import android.icu.text.BreakIterator
import java.util.Locale

/**
 * Utility for tokenizing text consistently across the app
 */
object TokenizerUtil {
    val PUNCTUATIONS = listOf(
        "!", "\"", ")", ",", ".", ":",
        ";", "?", "]", "~", ",", "。", ":", ";", "?", ")", "】", "!", "、", "」",
    )

    /**
     * Tokenize text using BreakIterator and merge trailing punctuation
     */
    fun tokenizeText(input: String): List<String> {
        if (input.isBlank()) return emptyList()

        val breakIterator = BreakIterator.getWordInstance(Locale.ROOT)
        breakIterator.setText(input)
        val tokens = mutableListOf<String>()
        var start = breakIterator.first()
        var end = breakIterator.next()

        while (end != BreakIterator.DONE) {
            val word = input.substring(start, end)
            if (word.isNotEmpty()) {
                tokens.add(word)
            }
            start = end
            end = breakIterator.next()
        }

        // Merge trailing punctuation with previous token
        if (tokens.isNotEmpty()) {
            val lastToken = tokens.last()
            if (tokens.size >= 2 && lastToken.length == 1 && PUNCTUATIONS.contains(lastToken)) {
                tokens.removeAt(tokens.size - 1)
                tokens[tokens.size - 1] = tokens[tokens.size - 1] + lastToken
            }
        }

        return tokens
    }
}



```

## /coreply-android/app/src/main/res/drawable/accessibility_disable_permission.png

Binary file available at https://raw.githubusercontent.com/coreply/coreply/refs/heads/main/coreply-android/app/src/main/res/drawable/accessibility_disable_permission.png

## /coreply-android/app/src/main/res/drawable/accessibility_grant_permission.png

Binary file available at https://raw.githubusercontent.com/coreply/coreply/refs/heads/main/coreply-android/app/src/main/res/drawable/accessibility_grant_permission.png

## /coreply-android/app/src/main/res/drawable/allow_overlay.png

Binary file available at https://raw.githubusercontent.com/coreply/coreply/refs/heads/main/coreply-android/app/src/main/res/drawable/allow_overlay.png

## /coreply-android/app/src/main/res/drawable/arrow_back_24px.xml

```xml path="/coreply-android/app/src/main/res/drawable/arrow_back_24px.xml" 
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960"
    android:tint="?attr/colorControlNormal"
    android:autoMirrored="true">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>

```

## /coreply-android/app/src/main/res/drawable/bubble_backgroud.xml

```xml path="/coreply-android/app/src/main/res/drawable/bubble_backgroud.xml" 
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
    xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="#FFFFFF"/>
    <size android:height="8dp" />
    <corners android:radius="10dp"/>
    <stroke android:width="0.5dp" android:color="#DDDDDD"/>

   </shape>
```

## /coreply-android/app/src/main/res/drawable/check_24px.xml

```xml path="/coreply-android/app/src/main/res/drawable/check_24px.xml" 
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960"
    android:tint="?attr/colorControlNormal">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z"/>
</vector>

```

## /coreply-android/app/src/main/res/drawable/refresh_24px.xml

```xml path="/coreply-android/app/src/main/res/drawable/refresh_24px.xml" 
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960"
    android:tint="?attr/colorControlNormal">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z"/>
</vector>

```

## /coreply-android/app/src/main/res/drawable/visibility_24px.xml

```xml path="/coreply-android/app/src/main/res/drawable/visibility_24px.xml" 
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960"
    android:tint="?attr/colorControlNormal">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z"/>
</vector>

```

## /coreply-android/app/src/main/res/drawable/visibility_off_24px.xml

```xml path="/coreply-android/app/src/main/res/drawable/visibility_off_24px.xml" 
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960"
    android:tint="?attr/colorControlNormal">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M644,532L586,474Q595,427 559,386Q523,345 466,354L408,296Q425,288 442.5,284Q460,280 480,280Q555,280 607.5,332.5Q660,385 660,460Q660,480 656,497.5Q652,515 644,532ZM772,658L714,602Q752,573 781.5,538.5Q811,504 832,460Q782,359 688.5,299.5Q595,240 480,240Q451,240 423,244Q395,248 368,256L306,194Q347,177 390,168.5Q433,160 480,160Q631,160 749,243.5Q867,327 920,460Q897,519 859.5,569.5Q822,620 772,658ZM792,904L624,738Q589,749 553.5,754.5Q518,760 480,760Q329,760 211,676.5Q93,593 40,460Q61,407 93,361.5Q125,316 166,280L56,168L112,112L848,848L792,904ZM222,336Q193,362 169,393Q145,424 128,460Q178,561 271.5,620.5Q365,680 480,680Q500,680 519,677.5Q538,675 558,672L522,634Q511,637 501,638.5Q491,640 480,640Q405,640 352.5,587.5Q300,535 300,460Q300,449 301.5,439Q303,429 306,418L222,336ZM541,429L541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429ZM390,504Q390,504 390,504Q390,504 390,504L390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Z"/>
</vector>

```

## /coreply-android/app/src/main/res/layout/activity_accessibility_disable_permission.xml

```xml path="/coreply-android/app/src/main/res/layout/activity_accessibility_disable_permission.xml" 
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="app.coreply.coreplyapp.WelcomeActivity"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:gravity="bottom"
            android:layout_weight="1.5"
            android:orientation="vertical">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="@style/TextAppearance.AppCompat.Display2"
                android:paddingStart="24dp"
                android:paddingEnd="24dp"
                android:text="@string/permission_disable_title"
                android:layout_marginBottom="32sp"
                android:layout_marginTop="16dp"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/permission_disable_description"
                android:paddingStart="24dp"
                android:paddingEnd="24dp"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>

        </LinearLayout>
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:scaleType="fitCenter"
            app:srcCompat="@drawable/accessibility_disable_permission"
            android:padding="16dp"
            android:layout_weight="1"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000000"
            android:alpha="0.12"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@string/next"
            style="@style/Widget.AppCompat.Button.Borderless.Colored"
            android:layout_gravity="center"
            android:onClick="openAccessibilitySettings" />

    </LinearLayout>



</androidx.coordinatorlayout.widget.CoordinatorLayout>

```

## /coreply-android/app/src/main/res/layout/activity_accessibility_permission.xml

```xml path="/coreply-android/app/src/main/res/layout/activity_accessibility_permission.xml" 
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="app.coreply.coreplyapp.WelcomeActivity"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1.5"
            android:gravity="bottom"
            android:orientation="vertical">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="@style/TextAppearance.AppCompat.Display2"
                android:paddingStart="24dp"
                android:paddingEnd="24dp"
                android:text="@string/permission_accessibility_title"
                android:layout_marginBottom="32sp"
                android:layout_marginTop="16dp"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/permission_accessibility_description"
                android:paddingStart="24dp"
                android:paddingEnd="24dp"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>

        </LinearLayout>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:scaleType="fitCenter"
            android:padding="16dp"
            app:srcCompat="@drawable/accessibility_grant_permission"
            android:layout_weight="1"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000000"
            android:alpha="0.12"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center">
<!--            <Button-->
<!--                android:layout_width="wrap_content"-->
<!--                android:layout_height="56dp"-->
<!--                android:text="@string/vid_tut"-->
<!--                style="@style/Widget.AppCompat.Button.Borderless"-->
<!--                android:textColor="#FFFFFF"-->
<!--                android:layout_gravity="right"-->
<!--                android:onClick="openVideoTutorial" />-->

            <Button
                android:layout_width="match_parent"
                android:layout_height="56dp"
                android:text="@string/next"
                style="@style/Widget.AppCompat.Button.Borderless.Colored"
                android:layout_gravity="center"
                android:onClick="openAccessibilitySettings" />
        </LinearLayout>

    </LinearLayout>



</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

## /coreply-android/app/src/main/res/layout/overlay_main.xml

```xml path="/coreply-android/app/src/main/res/layout/overlay_main.xml" 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="72dp"
    android:layout_height="match_parent"
    android:minWidth="72dp"
    android:theme="@style/AppTheme"
    android:id="@+id/overlay_main_layout"
    android:background="@android:color/transparent"
    android:gravity="center_vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto">


            <TextView
                android:id="@+id/suggestionBtn"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:textSize="18sp"
                android:ellipsize="end"
                android:singleLine="true"
                android:textColor="#BBBBBB"
                android:text="Button" />


</LinearLayout>

```

## /coreply-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

```xml path="/coreply-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" 
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  <background android:drawable="@mipmap/ic_launcher_background"/>
  <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
  <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>
```

## /coreply-android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file available at https://raw.githubusercontent.com/coreply/coreply/refs/heads/main/coreply-android/app/src/main/res/mipmap-hdpi/ic_launcher.png

## /docs/static/insta.gif

Binary file available at https://raw.githubusercontent.com/coreply/coreply/refs/heads/main/docs/static/insta.gif


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!