mirror of
https://github.com/souravanand001/ai-assistant-android.git
synced 2026-10-02 20:28:46 +00:00
code refactor started with step 1
This commit is contained in:
parent
a888bead9a
commit
5a6d90176d
26 changed files with 2451 additions and 1679 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ out/
|
|||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
.gradle_home/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(""),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8)
|
|||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
41
app/src/main/java/com/app/assistant/viewmodel/UIEvent.kt
Normal file
41
app/src/main/java/com/app/assistant/viewmodel/UIEvent.kt
Normal 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()
|
||||
}
|
||||
13
gradle/gradle-daemon-jvm.properties
Normal file
13
gradle/gradle-daemon-jvm.properties
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue