```
├── .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



[](https://discord.gg/zCsQKmTFTk)
[](https://t.me/coreplyappgroup)

**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
[](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.