code refactor started with step 1

This commit is contained in:
souravanand001 2026-05-30 13:57:06 +05:30
parent a888bead9a
commit 5a6d90176d
26 changed files with 2451 additions and 1679 deletions

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ out/
# Gradle files
.gradle/
.gradle_home/
build/
# Local configuration file (sdk path, etc)

View file

@ -30,7 +30,7 @@ class AssistActivity : ComponentActivity() {
finish()
}
private fun moveToMainActivity(){
private fun moveToMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION
intent.putExtra("speak", true)
@ -47,8 +47,9 @@ class AssistActivity : ComponentActivity() {
} else {
this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
)
}
}
}
}

View file

@ -1,33 +1,406 @@
package com.app.assistant
import android.Manifest
import android.app.KeyguardManager
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.LocationManager
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.app.assistant.ui.screen.SetupUI
import com.app.assistant.viewmodel.MainViewModel
import com.app.assistant.viewmodel.MainViewModelFactory
import com.app.assistant.viewmodel.UIEvent
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.gms.location.LocationSettingsResponse
import com.google.android.gms.location.Priority
import com.google.android.gms.location.SettingsClient
import com.google.android.gms.tasks.Task
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Locale
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels{
private val viewModel: MainViewModel by viewModels {
MainViewModelFactory(application, intent.getBooleanExtra("speak", false))
}
private lateinit var textToSpeech: TextToSpeech
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
allowOnLockScreen()
enableEdgeToEdge()
initializeTextToSpeech()
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiEvent.collect { event ->
when (event) {
is UIEvent.RequestPermissions -> {
ActivityCompat.requestPermissions(this@MainActivity, event.permissions, event.requestCode)
}
is UIEvent.StartIntent -> {
try {
startActivity(event.intent)
} catch (e: Exception) {
Log.e("MainActivity", "Error starting intent", e)
}
}
is UIEvent.ShowToast -> {
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_SHORT).show()
}
is UIEvent.SpeakText -> {
speakResponse(event.text)
}
is UIEvent.StopSpeaking -> {
if (::textToSpeech.isInitialized) {
textToSpeech.stop()
}
}
is UIEvent.StartSpeechRecognition -> {
startSpeechRecognition()
}
is UIEvent.StopSpeechRecognition -> {
// no-op
}
is UIEvent.GetLocationForWeather -> {
getCurrentLocationForWeather(event)
}
is UIEvent.ResolveLocationSettings -> {
try {
event.exception.startResolutionForResult(this@MainActivity, 104)
} catch (sendEx: IntentSender.SendIntentException) {
Log.e("MainActivity", "Error showing location settings dialog: ${sendEx.message}")
}
}
}
}
}
}
setContent {
SetupUI(viewModel)
}
}
private fun initializeTextToSpeech() {
textToSpeech =
TextToSpeech(application) { status ->
if (status == TextToSpeech.SUCCESS) {
Log.d("TextToSpeech", "Initialization Success")
} else {
Log.d("TextToSpeech", "Initialization Failed")
}
}
}
private fun speakResponse(plaintext: String) {
if (!::textToSpeech.isInitialized) return
textToSpeech.speak(plaintext, TextToSpeech.QUEUE_FLUSH, null, "left_for_now_id")
textToSpeech.setOnUtteranceProgressListener(
object : UtteranceProgressListener() {
override fun onDone(utteranceId: String?) {
viewModel.setSpeaking(false)
}
@Deprecated("Deprecated in Java")
override fun onError(utteranceId: String?) {
viewModel.setSpeaking(false)
}
override fun onStart(utteranceId: String?) {
viewModel.setSpeaking(true)
}
},
)
}
private fun startSpeechRecognition() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 101)
return
}
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.mode = AudioManager.MODE_IN_CALL
audioManager.isBluetoothScoOn = true
audioManager.startBluetoothSco()
val originalRingerMode = audioManager.ringerMode
if (::textToSpeech.isInitialized && textToSpeech.isSpeaking) {
textToSpeech.stop()
}
val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this)
val speechRecognizerIntent =
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
if (viewModel.getIsTranslationEnabled()) {
val localeCode = getLocaleCode(viewModel.getActiveLanguageCode())
putExtra(RecognizerIntent.EXTRA_LANGUAGE, localeCode)
} else {
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
}
}
speechRecognizer.setRecognitionListener(
object : RecognitionListener {
override fun onReadyForSpeech(bundle: Bundle?) {
if (originalRingerMode == AudioManager.RINGER_MODE_NORMAL) {
audioManager.ringerMode = AudioManager.RINGER_MODE_VIBRATE
}
}
override fun onBeginningOfSpeech() {
viewModel.setListening(true)
}
override fun onRmsChanged(v: Float) {}
override fun onBufferReceived(bytes: ByteArray?) {}
override fun onEndOfSpeech() {
viewModel.setListening(false)
if (originalRingerMode == AudioManager.RINGER_MODE_NORMAL) {
lifecycleScope.launch {
delay(800)
audioManager.ringerMode = originalRingerMode
}
}
}
override fun onError(errorCode: Int) {
viewModel.setListening(false)
if (originalRingerMode == AudioManager.RINGER_MODE_NORMAL) {
lifecycleScope.launch {
delay(800)
audioManager.ringerMode = originalRingerMode
}
}
}
override fun onResults(bundle: Bundle?) {
viewModel.setListening(false)
val recognizedText = bundle?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.get(0) ?: ""
viewModel.onSpeechRecognized(recognizedText)
}
override fun onPartialResults(bundle: Bundle) {
val recognizedText = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.get(0) ?: ""
viewModel.onSpeechPartialResult(recognizedText)
}
override fun onEvent(
i: Int,
bundle: Bundle?,
) {}
},
)
speechRecognizer.startListening(speechRecognizerIntent)
registerComponentCallbacks(
object : ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
speechRecognizer.destroy()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {}
override fun onLowMemory() {}
},
)
}
private fun getCurrentLocationForWeather(event: UIEvent.GetLocationForWeather) {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
val gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
if (!gpsEnabled) {
promptEnableLocation()
viewModel.onLocationFailed(event.loadingItemId, event.speak, "GPS_OFF")
} else {
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
val locationRequest =
LocationRequest
.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
.setWaitForAccurateLocation(true)
.setMaxUpdates(1)
.build()
val locationCallback =
object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
val location = locationResult.lastLocation
if (location != null) {
viewModel.onLocationReceived(
location.latitude,
location.longitude,
event.itemId,
event.loadingItemId,
event.speak,
event.categoryName,
event.prompt,
)
} else {
viewModel.onLocationFailed(event.loadingItemId, event.speak, "UNAVAILABLE")
}
fusedLocationClient.removeLocationUpdates(this)
}
override fun onLocationAvailability(locationAvailability: LocationAvailability) {
if (!locationAvailability.isLocationAvailable) {
viewModel.onLocationFailed(event.loadingItemId, event.speak, "SUGGEST_CITY")
}
}
}
val permissionGranted =
ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (permissionGranted) {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
} else {
viewModel.onLocationFailed(event.loadingItemId, event.speak, "PERMISSION")
}
}
}
private fun promptEnableLocation() {
val locationRequest =
LocationRequest
.Builder(100, 10000)
.setMinUpdateIntervalMillis(5000)
.build()
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
val settingsClient: SettingsClient = LocationServices.getSettingsClient(this)
val task: Task<LocationSettingsResponse> = settingsClient.checkLocationSettings(builder.build())
task.addOnFailureListener { exception ->
if (exception is ResolvableApiException) {
try {
exception.startResolutionForResult(this@MainActivity, 104)
} catch (sendEx: IntentSender.SendIntentException) {
Log.d("Location", "Error showing location settings dialog: ${sendEx.message}")
}
}
}
}
private fun getLocaleCode(languageCode: String): String {
val languageCountryMapping =
mapOf(
"af" to "ZA",
"sq" to "AL",
"ar" to "SA",
"be" to "BY",
"bg" to "BG",
"bn" to "BD",
"ca" to "ES",
"zh" to "CN",
"hr" to "HR",
"cs" to "CZ",
"da" to "DK",
"nl" to "NL",
"en" to "US",
"eo" to "EO",
"et" to "EE",
"fi" to "FI",
"fr" to "FR",
"gl" to "ES",
"ka" to "GE",
"de" to "DE",
"el" to "GR",
"gu" to "IN",
"ht" to "HT",
"he" to "IL",
"hi" to "IN",
"hu" to "HU",
"is" to "IS",
"id" to "ID",
"ga" to "IE",
"it" to "IT",
"ja" to "JP",
"kn" to "IN",
"ko" to "KR",
"lt" to "LT",
"lv" to "LV",
"mk" to "MK",
"mr" to "IN",
"ms" to "MY",
"mt" to "MT",
"no" to "NO",
"fa" to "IR",
"pl" to "PL",
"pt" to "BR",
"ro" to "RO",
"ru" to "RU",
"sk" to "SK",
"sl" to "SI",
"es" to "ES",
"sv" to "SE",
"sw" to "KE",
"tl" to "PH",
"ta" to "IN",
"te" to "IN",
"th" to "TH",
"tr" to "TR",
"uk" to "UA",
"ur" to "PK",
"vi" to "VN",
"cy" to "GB",
)
val countryCode = languageCountryMapping[languageCode] ?: Locale.getDefault().country
return "$languageCode-$countryCode"
}
private fun allowOnLockScreen() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { // Android 8.1+
setShowWhenLocked(true)
@ -35,7 +408,7 @@ class MainActivity : ComponentActivity() {
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
)
}
@ -46,13 +419,16 @@ class MainActivity : ComponentActivity() {
keyguardManager.requestDismissKeyguard(this, null)
}
}
}catch (ex: Exception){
} catch (ex: Exception) {
ex.message?.let { Log.d("Exception Occurred", it) }
}
}
override fun onDestroy() {
super.onDestroy()
if (::textToSpeech.isInitialized) {
textToSpeech.shutdown()
}
viewModel.shutdownResources()
}
}
}

View file

@ -1,4 +1,4 @@
package com.app.assistant.Classifier
package com.app.assistant.classifier
import android.content.Context
import android.os.SystemClock
@ -11,7 +11,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor
class TextClassifierHelper(
var currentModel: String = MOBILEBERT,
val context: Context,
val listener: TextResultsListener
val listener: TextResultsListener,
) {
private lateinit var textClassifier: TextClassifier
private lateinit var executor: ScheduledThreadPoolExecutor
@ -21,29 +21,40 @@ class TextClassifierHelper(
}
fun initClassifier() {
val baseOptionsBuilder = BaseOptions.builder()
.setModelAssetPath(currentModel)
val baseOptionsBuilder =
BaseOptions
.builder()
.setModelAssetPath(currentModel)
try {
val baseOptions = baseOptionsBuilder.build()
val optionsBuilder = TextClassifier.TextClassifierOptions.builder()
.setBaseOptions(baseOptions)
val optionsBuilder =
TextClassifier.TextClassifierOptions
.builder()
.setBaseOptions(baseOptions)
val options = optionsBuilder.build()
textClassifier = TextClassifier.createFromOptions(context, options)
} catch (e: IllegalStateException) {
listener.onError(
"Text classifier failed to initialize. See error logs for " +
"details"
"details",
)
Log.e(
TAG, "Text classifier failed to load the task with error: " + e
.message
TAG,
"Text classifier failed to load the task with error: " +
e
.message,
)
}
}
// Run text classification using MediaPipe Text Classifier API
fun classify(text: String, itemId: Long, loadingItemId: Long, speak: Boolean) {
fun classify(
text: String,
itemId: Long,
loadingItemId: Long,
speak: Boolean,
) {
executor = ScheduledThreadPoolExecutor(1)
executor.execute {
@ -61,11 +72,19 @@ class TextClassifierHelper(
interface TextResultsListener {
fun onError(error: String)
fun onResult(results: TextClassifierResult, inferenceTime: Long, inputText: String, itemId: Long, loadingItemId: Long, speak: Boolean)
fun onResult(
results: TextClassifierResult,
inferenceTime: Long,
inputText: String,
itemId: Long,
loadingItemId: Long,
speak: Boolean,
)
}
companion object {
const val TAG = "TextClassifierHelper"
const val MOBILEBERT = "model.tflite"
}
}
}

View file

@ -6,9 +6,9 @@ import android.database.sqlite.SQLiteOpenHelper
import com.app.assistant.model.Conversation
import kotlin.reflect.full.memberProperties
class DynamicConversationDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
class DynamicConversationDbHelper(
context: Context,
) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "conversation.db"
private const val DATABASE_VERSION = 1
@ -16,7 +16,7 @@ class DynamicConversationDbHelper(context: Context) : SQLiteOpenHelper(context,
private const val COLUMN_IV = "iv"
// Groups Table
private const val TABLE_GROUPS = "groups"
private const val TABLE_GROUPS = "`groups`"
private const val COLUMN_GROUP_ID = "group_id"
private const val COLUMN_TITLE = "title"
private const val COLUMN_CREATED_AT = "created_at"
@ -26,20 +26,25 @@ class DynamicConversationDbHelper(context: Context) : SQLiteOpenHelper(context,
}
override fun onCreate(db: SQLiteDatabase?) {
val createGroupsTableQuery = """
val createGroupsTableQuery =
"""
CREATE TABLE $TABLE_GROUPS (
$COLUMN_GROUP_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_TITLE TEXT,
$COLUMN_CREATED_AT INTEGER
)
""".trimIndent()
""".trimIndent()
db?.execSQL(createGroupsTableQuery)
val createMessageTableQuery = buildCreateTableQuery()
db?.execSQL(createMessageTableQuery)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
override fun onUpgrade(
db: SQLiteDatabase?,
oldVersion: Int,
newVersion: Int,
) {
db?.execSQL("DROP TABLE IF EXISTS $TABLE_MESSAGES")
db?.execSQL("DROP TABLE IF EXISTS $TABLE_GROUPS")
onCreate(db)
@ -47,31 +52,31 @@ class DynamicConversationDbHelper(context: Context) : SQLiteOpenHelper(context,
private fun buildCreateTableQuery(): String {
val fields = Conversation::class.memberProperties
val columns = fields.filterNot { it.name == "isLoading" }.joinToString(", ") { field ->
val fieldType = mapFieldTypeToSQLiteType(field.returnType.toString())
if (field.name == "id") {
"${field.name} INTEGER PRIMARY KEY"
} else {
"${field.name} $fieldType"
val columns =
fields.filterNot { it.name == "isLoading" }.joinToString(", ") { field ->
val fieldType = mapFieldTypeToSQLiteType(field.returnType.toString())
if (field.name == "id") {
"${field.name} INTEGER PRIMARY KEY"
} else {
"${field.name} $fieldType"
}
}
}
return """
CREATE TABLE $TABLE_MESSAGES (
$columns,
iv TEXT,
$COLUMN_GROUP_ID INTEGER,
FOREIGN KEY ($COLUMN_GROUP_ID) REFERENCES $TABLE_GROUPS($COLUMN_GROUP_ID)
)
""".trimIndent()
CREATE TABLE $TABLE_MESSAGES (
$columns,
iv TEXT,
$COLUMN_GROUP_ID INTEGER,
FOREIGN KEY ($COLUMN_GROUP_ID) REFERENCES $TABLE_GROUPS($COLUMN_GROUP_ID)
)
""".trimIndent()
}
private fun mapFieldTypeToSQLiteType(kotlinType: String): String {
return when (kotlinType) {
private fun mapFieldTypeToSQLiteType(kotlinType: String): String =
when (kotlinType) {
"kotlin.String" -> "TEXT"
"kotlin.Int", "kotlin.Long", "kotlin.Boolean" -> "INTEGER"
"java.net.URI" -> "TEXT"
else -> "TEXT"
}
}
}
}

View file

@ -17,8 +17,9 @@ import java.util.Locale
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
class DynamicConversationRepository(private val context: Context) {
class DynamicConversationRepository(
private val context: Context,
) {
companion object {
private const val TABLE_GROUPS = "groups"
private const val COLUMN_GROUP_ID = "group_id"
@ -34,10 +35,11 @@ class DynamicConversationRepository(private val context: Context) {
private suspend fun startNewChat(msg: String): Long {
withContext(Dispatchers.IO) {
val db = dbHelper.writableDatabase
val contentValues = ContentValues().apply {
put(COLUMN_TITLE, generateChatTitle(msg))
put(COLUMN_CREATED_AT, System.nanoTime())
}
val contentValues =
ContentValues().apply {
put(COLUMN_TITLE, generateChatTitle(msg))
put(COLUMN_CREATED_AT, System.nanoTime())
}
currentGroupId = db.insert(TABLE_GROUPS, null, contentValues)
}
return currentGroupId
@ -58,14 +60,15 @@ class DynamicConversationRepository(private val context: Context) {
val db = dbHelper.writableDatabase
val (encryptedMap, iv) = encryptConversation(conversation)
val values = ContentValues().apply {
encryptedMap.forEach { (key, value) -> put(key, value) }
put("iv", iv)
put(COLUMN_GROUP_ID, currentGroupId)
}
try{
db.insert(TABLE_MESSAGES, null, values)
}catch (e: Exception){
val values =
ContentValues().apply {
encryptedMap.forEach { (key, value) -> put(key, value) }
put("iv", iv)
put(COLUMN_GROUP_ID, currentGroupId)
}
try {
db.insert(TABLE_MESSAGES, null, values)
} catch (e: Exception) {
e.message?.let { Log.d("Test", it) }
}
}
@ -77,24 +80,28 @@ class DynamicConversationRepository(private val context: Context) {
db.delete(
TABLE_MESSAGES,
"id = ?",
arrayOf(id.toString())
arrayOf(id.toString()),
)
}
}
suspend fun updateMessage(oldConversation: Conversation, newConversation: Conversation) {
suspend fun updateMessage(
oldConversation: Conversation,
newConversation: Conversation,
) {
withContext(Dispatchers.IO) {
val db = dbHelper.writableDatabase
val (encryptedMap, iv) = encryptConversation(newConversation)
val values = ContentValues().apply {
encryptedMap.forEach { (key, value) -> put(key, value) }
put("iv", iv)
}
val values =
ContentValues().apply {
encryptedMap.forEach { (key, value) -> put(key, value) }
put("iv", iv)
}
db.update(
TABLE_MESSAGES,
values,
"id = ?",
arrayOf(oldConversation.id.toString())
arrayOf(oldConversation.id.toString()),
)
}
}
@ -113,15 +120,16 @@ class DynamicConversationRepository(private val context: Context) {
val groupList = mutableListOf<Group>()
withContext(Dispatchers.IO) {
val db = dbHelper.readableDatabase
val cursor: Cursor = db.query(
TABLE_GROUPS,
arrayOf(COLUMN_GROUP_ID, COLUMN_TITLE),
null,
null,
null,
null,
"$COLUMN_GROUP_ID DESC" // Sort by latest created
)
val cursor: Cursor =
db.query(
TABLE_GROUPS,
arrayOf(COLUMN_GROUP_ID, COLUMN_TITLE),
null,
null,
null,
null,
"$COLUMN_GROUP_ID DESC", // Sort by latest created
)
while (cursor.moveToNext()) {
val groupId = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_GROUP_ID))
@ -133,20 +141,20 @@ class DynamicConversationRepository(private val context: Context) {
return groupList
}
suspend fun loadMessagesForGroup(groupId: Long): MutableList<Conversation> {
val tempList = mutableListOf<Conversation>()
withContext(Dispatchers.IO) {
val db = dbHelper.readableDatabase
val cursor: Cursor = db.query(
TABLE_MESSAGES,
null,
"$COLUMN_GROUP_ID = ?",
arrayOf(groupId.toString()),
null,
null,
COLUMN_MESSAGE_ID // Sort by latest created
)
val cursor: Cursor =
db.query(
TABLE_MESSAGES,
null,
"$COLUMN_GROUP_ID = ?",
arrayOf(groupId.toString()),
null,
null,
COLUMN_MESSAGE_ID, // Sort by latest created
)
while (cursor.moveToNext()) {
val conversation = cursorToConversation(cursor)
@ -165,21 +173,31 @@ class DynamicConversationRepository(private val context: Context) {
properties.filter { it.name != "isLoading" }.forEach { property ->
val columnName = property.name
val columnIndex = cursor.getColumnIndexOrThrow(columnName)
val value = when (property.returnType.toString()) {
"kotlin.String" -> cursor.getString(columnIndex)//?.let { EncryptionUtil.decrypt(it, iv) }
"kotlin.Int" -> cursor.getInt(columnIndex)
"kotlin.Long" -> cursor.getLong(columnIndex)
"kotlin.Boolean" -> cursor.getString(columnIndex).toBooleanStrictOrNull() ?: false
"java.net.URI" -> URI(cursor.getString(columnIndex))
else -> null
}
val value =
when (property.returnType.toString()) {
"kotlin.String" -> cursor.getString(columnIndex)
// ?.let { EncryptionUtil.decrypt(it, iv) }
"kotlin.Int" -> cursor.getInt(columnIndex)
"kotlin.Long" -> cursor.getLong(columnIndex)
"kotlin.Boolean" -> cursor.getString(columnIndex).toBooleanStrictOrNull() ?: false
"java.net.URI" -> URI(cursor.getString(columnIndex))
else -> null
}
fieldMap[columnName] = value
}
return Conversation::class.constructors.first().callBy(
Conversation::class.primaryConstructor!!.parameters
.filter { it.name != "isLoading" } // Ignore "isLoading"
.associateWith { parameter -> fieldMap[parameter.name] }
Conversation::class
.primaryConstructor!!
.parameters
.filter { it.name != "isLoading" } // Ignore "isLoading"
.associateWith { parameter -> fieldMap[parameter.name] },
)
}
@ -191,11 +209,11 @@ class DynamicConversationRepository(private val context: Context) {
properties.forEach { property ->
val key = property.name
val value = property.getter.call(conversation)?.toString()
if (value != null && key!="isLoading") {
//encryptedMap[key] = EncryptionUtil.encrypt(value, iv).first
if (value != null && key != "isLoading") {
// encryptedMap[key] = EncryptionUtil.encrypt(value, iv).first
encryptedMap[key] = value
}
}
return Pair(encryptedMap, iv)
}
}
}

View file

@ -27,7 +27,10 @@ object EncryptionUtil {
}
// Encrypt a string and return encrypted text and IV
fun encrypt(input: String, ivString: String): Pair<String, String> {
fun encrypt(
input: String,
ivString: String,
): Pair<String, String> {
val iv = Base64.decode(ivString, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
@ -39,7 +42,10 @@ object EncryptionUtil {
}
// Decrypt an encrypted string using IV
fun decrypt(encryptedInput: String, ivString: String): String {
fun decrypt(
encryptedInput: String,
ivString: String,
): String {
val iv = Base64.decode(ivString, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
@ -58,12 +64,10 @@ object EncryptionUtil {
}
// Save and restore secret key as Base64 (for persistence)
fun getSecretKeyAsString(): String {
return Base64.encodeToString(secretKey.encoded, Base64.DEFAULT)
}
fun getSecretKeyAsString(): String = Base64.encodeToString(secretKey.encoded, Base64.DEFAULT)
fun restoreSecretKeyFromString(keyString: String): SecretKey {
val decodedKey = Base64.decode(keyString, Base64.DEFAULT)
return SecretKeySpec(decodedKey, 0, decodedKey.size, ALGORITHM)
}
}
}

View file

@ -10,9 +10,8 @@ import kotlinx.coroutines.launch
class SyncStateList(
private val repository: DynamicConversationRepository, // Repository to handle DB operations
private val stateList: SnapshotStateList<Conversation> = mutableStateListOf()
private val stateList: SnapshotStateList<Conversation> = mutableStateListOf(),
) : MutableList<Conversation> by stateList {
override fun add(element: Conversation): Boolean {
val result = stateList.add(element)
if (result && !element.isLoading) {
@ -31,7 +30,10 @@ class SyncStateList(
return result
}
override fun set(index: Int, element: Conversation): Conversation {
override fun set(
index: Int,
element: Conversation,
): Conversation {
val oldElement = stateList[index]
stateList[index] = element
CoroutineScope(Dispatchers.IO).launch {

View file

@ -1,3 +1,7 @@
package com.app.assistant.model
data class Contact(val id: String ,val name: String, val phoneNumber: String)
data class Contact(
val id: String,
val name: String,
val phoneNumber: String,
)

View file

@ -2,4 +2,13 @@ package com.app.assistant.model
import com.app.assistant.util.Category
import java.net.URI
data class Conversation(var id: Long = System.currentTimeMillis() * 1_000_000 + (System.nanoTime() % 1_000_000), var englishText: String, var translatedText: String, val isMe: Boolean, val isLoading: Boolean = false, var category: String = Category.OTHER.name, val contentURL: String ="", val navigationURI: URI=URI("") )
data class Conversation(
var id: Long = System.currentTimeMillis() * 1_000_000 + (System.nanoTime() % 1_000_000),
var englishText: String,
var translatedText: String,
val isMe: Boolean,
val isLoading: Boolean = false,
var category: String = Category.OTHER.name,
val contentURL: String = "",
val navigationURI: URI = URI(""),
)

View file

@ -1,3 +1,6 @@
package com.app.assistant.model
data class Group(val groupId: Long, val title: String)
data class Group(
val groupId: Long,
val title: String,
)

View file

@ -0,0 +1,113 @@
package com.app.assistant.repository
import android.annotation.SuppressLint
import android.content.Context
import android.provider.ContactsContract
import com.app.assistant.model.Contact
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.max
class ContactsRepository(
private val context: Context,
) {
@SuppressLint("Range")
suspend fun searchContact(prompt: String): Contact? {
val contentResolver = context.contentResolver
val contactList = mutableListOf<Contact>()
withContext(Dispatchers.IO) {
val cursor =
contentResolver.query(
ContactsContract.Data.CONTENT_URI,
arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
),
"${ContactsContract.Data.MIMETYPE} =? AND ${ContactsContract.Data.CONTACT_ID} IS NOT NULL",
arrayOf(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE),
null,
)
cursor?.use { contactCursor ->
while (contactCursor.moveToNext()) {
val contactId = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID))
val name = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME))
val phoneNumber = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactList.add(Contact(contactId, name, phoneNumber))
}
}
}
val contactName = cleanAndExtractName(prompt)
return findBestMatch(contactName, contactList)
}
private fun cleanAndExtractName(command: String): String {
val stopWords = setOf("can", "you", "make", "call", "dial", "number", "to", "please", "the", "a", "on")
val words = command.lowercase().trim().split(" ")
return words.filter { !stopWords.contains(it) }.joinToString(" ")
}
private fun findBestMatch(
inputString: String,
nameList: List<Contact>,
similarityThreshold: Double = 0.7,
): Contact? {
if (inputString.isEmpty()) {
return null
}
val inputLower = inputString.lowercase().trim()
var exactMatch: Contact? = null
var bestSubstringMatch: Contact? = null
var bestFuzzyMatch: Contact? = null
var bestFuzzyScore = 0.0
for (contact in nameList) {
val nameLower = contact.name.lowercase().trim()
// Check for exact match
if (inputLower == nameLower) {
return contact
}
// Check for exact match within tokens
val tokens = nameLower.split(" ")
for (token in tokens) {
when {
inputLower == token -> {
exactMatch = contact
}
token.contains(inputLower) -> {
bestSubstringMatch = contact
}
else -> {
val similarityScore = stringSimilarity(inputLower, token)
if (similarityScore > bestFuzzyScore) {
bestFuzzyScore = similarityScore
bestFuzzyMatch = contact
}
}
}
}
}
return when {
exactMatch != null -> exactMatch
bestSubstringMatch != null -> bestSubstringMatch
bestFuzzyScore >= similarityThreshold -> bestFuzzyMatch
else -> null
}
}
private fun stringSimilarity(
a: String,
b: String,
): Double {
val matcher = a.commonPrefixWith(b).length.toDouble() / max(a.length, b.length).toDouble()
return matcher
}
}

View file

@ -0,0 +1,53 @@
package com.app.assistant.repository
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
class SettingsRepository(
context: Context,
) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
private val securedPreferences: SharedPreferences =
EncryptedSharedPreferences.create(
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context.applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
fun getIsTranslationEnabled(): Boolean = sharedPreferences.getBoolean("is_translation_enabled", false)
fun setTranslationEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean("is_translation_enabled", enabled).apply()
}
fun getActiveLanguageCode(): String = sharedPreferences.getString("active_language_code", "en") ?: "en"
fun setActiveLanguageCode(languageCode: String) {
sharedPreferences.edit().putString("active_language_code", languageCode).apply()
}
fun saveKeys(
youtubeApiKey: String,
chatApiKey: String,
) {
with(securedPreferences.edit()) {
if (youtubeApiKey.isNotBlank()) {
putString("youtube_api_key", youtubeApiKey)
}
if (chatApiKey.isNotBlank()) {
putString("chat_api_key", chatApiKey)
}
apply()
}
}
fun getYoutubeApiKey(): String? = securedPreferences.getString("youtube_api_key", null)
fun getChatApiKey(): String? = securedPreferences.getString("chat_api_key", null)
}

View file

@ -0,0 +1,203 @@
package com.app.assistant.repository
import android.content.Context
import android.location.Geocoder
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.util.Locale
class WeatherRepository(
private val context: Context,
) {
private val client = OkHttpClient()
fun extractPlaceName(inputText: String): String {
// Pattern to match place names after "in" or "for", excluding trailing words like "today", "tomorrow", etc.
val pattern =
Regex(
"\\b(?:in|for)\\s+([A-Za-z]+(?:\\s+[A-Za-z]+)*)" +
"(?=\\b(?:\\s+(today|now|tomorrow|report|forecast|currently|evening|morning|afternoon)\\b|\\s*$))",
)
// Search for a match in the input text
val match = pattern.find(inputText)
// Check if the captured group does not match excluded words (e.g., "tomorrow") and return it
val placeName =
match
?.groups
?.get(1)
?.value
?.trim()
return if (placeName != null &&
!placeName.lowercase().matches(Regex("today|now|tomorrow|report|forecast|currently|evening|morning|afternoon"))
) {
placeName
} else {
""
}
}
suspend fun getCoordinatesFromCity(cityName: String): Pair<Double, Double>? {
val deferredResult = CompletableDeferred<Pair<Double, Double>?>()
val urlBuilder =
"https://geocoding-api.open-meteo.com/v1/search"
.toHttpUrlOrNull()
?.newBuilder()
?.addQueryParameter("name", cityName)
?.build()
val request =
Request
.Builder()
.url(urlBuilder.toString())
.build()
client.newCall(request).enqueue(
object : Callback {
override fun onFailure(
call: Call,
e: IOException,
) {
Log.e("WeatherRepository", "Request failed: $e")
deferredResult.completeExceptionally(e)
}
override fun onResponse(
call: Call,
response: Response,
) {
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
deferredResult.complete(extractCoordinates(body, cityName))
} else {
Log.e("WeatherRepository", "Request unsuccessful: ${response.code}")
deferredResult.cancel()
}
}
},
)
return deferredResult.await()
}
private fun extractCoordinates(
body: String,
cityName: String,
): Pair<Double, Double>? {
if (body.isEmpty()) return null
val jsonObject = JSONObject(body)
var latitude: Double? = null
var longitude: Double? = null
try {
val results = jsonObject.getJSONArray("results")
for (i in 0 until results.length()) {
val result = results.getJSONObject(i)
if (result.getString("name").equals(cityName, ignoreCase = true)) {
latitude = result.getDouble("latitude")
longitude = result.getDouble("longitude")
break
}
}
if ((latitude == null || longitude == null) && results.length() > 0) {
val firstResult = results.getJSONObject(0)
latitude = firstResult.getDouble("latitude")
longitude = firstResult.getDouble("longitude")
}
} catch (e: Exception) {
e.message?.let { Log.d("extractCoordinates Excepetion", it) }
}
return if (latitude != null && longitude != null) Pair(latitude, longitude) else null
}
fun getCityNameFromLocation(
latitude: Double,
longitude: Double,
): String? {
val geocoder = Geocoder(context, Locale.getDefault())
return try {
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
if (addresses?.isNotEmpty() == true) {
addresses[0].locality // This gives the city name
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
suspend fun getWeatherData(
latitude: Double,
longitude: Double,
): JSONObject? {
val deferredResult = CompletableDeferred<JSONObject?>()
val urlBuilder =
"https://api.open-meteo.com/v1/forecast"
.toHttpUrlOrNull()
?.newBuilder()
?.addQueryParameter("latitude", latitude.toString())
?.addQueryParameter("longitude", longitude.toString())
?.addQueryParameter("daily", "temperature_2m_max,temperature_2m_min,precipitation_sum,windspeed_10m_max")
?.addQueryParameter("hourly", "temperature_2m,precipitation,windspeed_10m")
?.addQueryParameter("timezone", "auto")
?.addQueryParameter("forecast_days", "10")
?.build()
val request =
Request
.Builder()
.url(urlBuilder.toString())
.build()
client.newCall(request).enqueue(
object : Callback {
override fun onFailure(
call: Call,
e: IOException,
) {
Log.e("WeatherRepository", "Weather data request failed: $e")
deferredResult.complete(null)
}
override fun onResponse(
call: Call,
response: Response,
) {
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
try {
deferredResult.complete(JSONObject(body))
} catch (e: JSONException) {
Log.e("WeatherRepository", "Error parsing JSON: $e")
deferredResult.complete(null)
}
} else {
Log.e("WeatherRepository", "Weather data request unsuccessful: ${response.code}")
deferredResult.complete(null)
}
}
},
)
return deferredResult.await()
}
}

View file

@ -10,13 +10,16 @@ import com.google.mlkit.nl.translate.Translator
import com.google.mlkit.nl.translate.TranslatorOptions
class TranslatorManager {
private var toEnglishTranslator: Translator? = null
private var fromEnglishTranslator: Translator? = null
private val modelManager = RemoteModelManager.getInstance()
fun setupTranslators(selectedLanguageCode: String, onResult: (Boolean) -> Unit) {
modelManager.getDownloadedModels(TranslateRemoteModel::class.java)
fun setupTranslators(
selectedLanguageCode: String,
onResult: (Boolean) -> Unit,
) {
modelManager
.getDownloadedModels(TranslateRemoteModel::class.java)
.addOnSuccessListener { downloadedModels ->
val downloadedLanguages = downloadedModels.map { it.language }.toSet()
@ -29,14 +32,14 @@ class TranslatorManager {
if (!success) {
onResult(false)
return@downloadLanguageModel
}else{
} else {
// Download English model if not already downloaded
if (!isEnglishModelDownloaded) {
downloadLanguageModel(TranslateLanguage.ENGLISH) { successEnglish ->
if (!successEnglish) {
onResult(false)
return@downloadLanguageModel
}else{
} else {
onResult(true)
}
}
@ -53,8 +56,7 @@ class TranslatorManager {
// Set up the translators for the selected language and English
initializeTranslators(selectedLanguageCode)
}
.addOnFailureListener { e ->
}.addOnFailureListener { e ->
Log.e("Translator", "Failed to get downloaded models: ${e.message}")
onResult(false)
}
@ -63,57 +65,69 @@ class TranslatorManager {
// Helper function to initialize translators for the selected language
private fun initializeTranslators(selectedLanguageCode: String) {
// Set up translator for selected language -> English
val toEnglishOptions = TranslatorOptions.Builder()
.setSourceLanguage(selectedLanguageCode)
.setTargetLanguage(TranslateLanguage.ENGLISH)
.build()
val toEnglishOptions =
TranslatorOptions
.Builder()
.setSourceLanguage(selectedLanguageCode)
.setTargetLanguage(TranslateLanguage.ENGLISH)
.build()
toEnglishTranslator = Translation.getClient(toEnglishOptions)
// Set up translator for English -> selected language
val fromEnglishOptions = TranslatorOptions.Builder()
.setSourceLanguage(TranslateLanguage.ENGLISH)
.setTargetLanguage(selectedLanguageCode)
.build()
val fromEnglishOptions =
TranslatorOptions
.Builder()
.setSourceLanguage(TranslateLanguage.ENGLISH)
.setTargetLanguage(selectedLanguageCode)
.build()
fromEnglishTranslator = Translation.getClient(fromEnglishOptions)
}
// Use this method to translate from selected language to English
fun translateToEnglish(text: String, callback: (translatedText: String?) -> Unit) {
toEnglishTranslator?.translate(text)
fun translateToEnglish(
text: String,
callback: (translatedText: String?) -> Unit,
) {
toEnglishTranslator
?.translate(text)
?.addOnSuccessListener { translatedText ->
callback(translatedText)
}
?.addOnFailureListener { e ->
}?.addOnFailureListener { e ->
Log.e("Translator", "Translation to English failed: ${e.message}")
callback(null)
}
}
// Use this method to translate from English to the selected language
fun translateFromEnglish(text: String, callback: (translatedText: String?) -> Unit) {
fromEnglishTranslator?.translate(text)
fun translateFromEnglish(
text: String,
callback: (translatedText: String?) -> Unit,
) {
fromEnglishTranslator
?.translate(text)
?.addOnSuccessListener { translatedText ->
callback(translatedText)
}
?.addOnFailureListener { e ->
}?.addOnFailureListener { e ->
Log.e("Translator", "Translation from English failed: ${e.message}")
callback(null)
}
}
private fun downloadLanguageModel(languageCode: String, onDownloadComplete: (Boolean) -> Unit) {
private fun downloadLanguageModel(
languageCode: String,
onDownloadComplete: (Boolean) -> Unit,
) {
val languageModel = TranslateRemoteModel.Builder(languageCode).build()
val conditions = DownloadConditions.Builder().build()
modelManager.download(languageModel, conditions)
modelManager
.download(languageModel, conditions)
.addOnSuccessListener {
Log.d("Translator", "Language model for $languageCode downloaded successfully.")
onDownloadComplete(true)
}
.addOnFailureListener { e ->
}.addOnFailureListener { e ->
Log.e("Translator", "Failed to download language model for $languageCode: ${e.message}")
onDownloadComplete(false)
}
@ -122,17 +136,17 @@ class TranslatorManager {
// Delete the previously downloaded language model
private fun deletePreviousLanguageModel(languageCode: String) {
val languageModel = TranslateRemoteModel.Builder(languageCode).build()
modelManager.deleteDownloadedModel(languageModel)
modelManager
.deleteDownloadedModel(languageModel)
.addOnSuccessListener {
Log.d("Translator", "Language model for $languageCode deleted successfully.")
}
.addOnFailureListener { e ->
}.addOnFailureListener { e ->
Log.e("Translator", "Failed to delete language model for $languageCode: ${e.message}")
}
}
fun closeTranslator(){
fun closeTranslator() {
toEnglishTranslator?.close()
fromEnglishTranslator?.close()
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Pink40 = Color(0xFF7D5260)

View file

@ -11,17 +11,18 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
private val DarkColorScheme =
darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)
private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
@ -30,28 +31,35 @@ private val LightColorScheme = lightColorScheme(
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
*/
)
@Composable
fun AssistantTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
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 -> DarkColorScheme
else -> LightColorScheme
}
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> {
DarkColorScheme
}
else -> {
LightColorScheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
}

View file

@ -7,14 +7,16 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
@ -30,5 +32,5 @@ val Typography = Typography(
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
*/
)

View file

@ -1,5 +1,12 @@
package com.app.assistant.util
enum class Category {
SONGS, CALL, ALARM, REMINDER, OTHER, NAVIGATION, SETTINGS, WEATHER
}
SONGS,
CALL,
ALARM,
REMINDER,
OTHER,
NAVIGATION,
SETTINGS,
WEATHER,
}

View file

@ -1,6 +1,10 @@
package com.app.assistant.util
object Constants {
const val MAIN_CONTEXT = "You are my smart assistant who answers just like normal human response, not too long. Don't refer yourself as an AI."
val CATEGORY_CONTEXT = "You are a assistant who can classify commands based on previous conversation, from these categories. "+ enumValues<Category>().joinToString(",") { it.name } +". Just say the category name."
}
const val MAIN_CONTEXT =
"You are my smart assistant who answers just like normal human response, not too long. Don't refer yourself as an AI."
val CATEGORY_CONTEXT =
"You are a assistant who can classify commands based on previous conversation, from these categories. " +
enumValues<Category>().joinToString(",") { it.name } +
". Just say the category name."
}

View file

@ -4,14 +4,19 @@ sealed class LockState {
object None : LockState() // Default state when no task is locked
data class LockAlarm(
var day: String? = null, // Already processed day (e.g., "tomorrow")
var time: String? = null // Time to be provided
// Already processed day (e.g., "tomorrow")
var day: String? = null,
// Time to be provided
var time: String? = null,
) : LockState()
data class LockReminder(
var day: String? = null, // Already processed day (e.g., "tomorrow")
var time: String? = null, // Time to be provided
var context: String? = null // Message for reminder
// Already processed day (e.g., "tomorrow")
var day: String? = null,
// Time to be provided
var time: String? = null,
// Message for reminder
var context: String? = null,
) : LockState()
object LockNavigation : LockState() // No additional data needed

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,21 @@ package com.app.assistant.viewmodel
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.app.assistant.repository.ContactsRepository
import com.app.assistant.repository.SettingsRepository
import com.app.assistant.repository.WeatherRepository
class MainViewModelFactory(
private val application: Application,
private val speak: Boolean
private val speak: Boolean,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
val settingsRepo = SettingsRepository(application)
val contactsRepo = ContactsRepository(application)
val weatherRepo = WeatherRepository(application)
@Suppress("UNCHECKED_CAST")
return MainViewModel(application, speak) as T
return MainViewModel(application, speak, settingsRepo, contactsRepo, weatherRepo) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}

View file

@ -0,0 +1,41 @@
package com.app.assistant.viewmodel
import android.content.Intent
import com.google.android.gms.common.api.ResolvableApiException
sealed class UIEvent {
data class RequestPermissions(
val permissions: Array<String>,
val requestCode: Int,
) : UIEvent()
data class StartIntent(
val intent: Intent,
) : UIEvent()
data class ShowToast(
val message: String,
) : UIEvent()
data class SpeakText(
val text: String,
) : UIEvent()
object StopSpeaking : UIEvent()
object StartSpeechRecognition : UIEvent()
object StopSpeechRecognition : UIEvent()
data class GetLocationForWeather(
val itemId: Long,
val loadingItemId: Long,
val speak: Boolean,
val categoryName: String,
val prompt: String,
) : UIEvent()
data class ResolveLocationSettings(
val exception: ResolvableApiException,
) : UIEvent()
}

View file

@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21