Refactor step 9 removed context direct use

This commit is contained in:
souravanand001 2026-06-01 22:04:51 +05:30
parent 8c6e583773
commit 80a82620e3
12 changed files with 179 additions and 140 deletions

View file

@ -0,0 +1,15 @@
package com.app.assistant.model
sealed class DeviceAction {
data class MakeCall(val phoneNumber: String) : DeviceAction()
data class NavigateTo(val location: String) : DeviceAction()
data class PlaySong(val videoId: String) : DeviceAction()
data class SearchSong(val query: String) : DeviceAction()
data class SetAlarm(
val message: String,
val hour: Int,
val minutes: Int,
val repeatDays: List<Int>,
val isReminder: Boolean
) : DeviceAction()
}

View file

@ -1,32 +1,25 @@
package com.app.assistant.usecase
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.core.content.ContextCompat
import com.app.assistant.model.DeviceAction
import com.app.assistant.repository.ContactsRepository
import com.app.assistant.viewmodel.UIEvent
import java.net.URI
class CallContactUseCase(
private val context: Context,
private val permissionChecker: PermissionChecker,
private val contactsRepository: ContactsRepository
) {
suspend fun execute(
prompt: String,
onPermissionRequest: suspend (Array<String>) -> Unit,
onIntentTriggered: suspend (Intent) -> Unit,
onIntentTriggered: suspend (DeviceAction) -> Unit,
onSuccess: suspend (name: String, dialUri: URI) -> Unit,
onFailure: suspend (errorMsg: String) -> Unit
) {
val requiredPermissions = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE
"android.permission.READ_CONTACTS",
"android.permission.CALL_PHONE"
).filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
!permissionChecker.hasPermission(it)
}
if (requiredPermissions.isNotEmpty()) {
@ -37,14 +30,11 @@ class CallContactUseCase(
val bestMatch = contactsRepository.searchContact(prompt)
if (bestMatch != null) {
try {
val callIntent = Intent(Intent.ACTION_CALL).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.parse("tel:${bestMatch.phoneNumber}")
}
onIntentTriggered(callIntent)
val callAction = DeviceAction.MakeCall(bestMatch.phoneNumber)
onIntentTriggered(callAction)
onSuccess(bestMatch.name, URI("tel:${bestMatch.phoneNumber.replace(" ", "")}"))
} catch (e: Exception) {
Log.e("CallContactUseCase", "Error making call:", e)
System.err.println("Error making call: ${e.message}")
onFailure("Sorry, failed to make call. Please try again.")
}
} else {

View file

@ -1,10 +1,5 @@
package com.app.assistant.usecase
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.app.assistant.repository.WeatherRepository
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@ -14,7 +9,7 @@ import java.util.Calendar
import java.util.Locale
class GetWeatherUseCase(
private val context: Context,
private val permissionChecker: PermissionChecker,
private val weatherRepository: WeatherRepository,
private val getAiResponseUseCase: GetAiResponseUseCase
) {
@ -50,19 +45,16 @@ class GetWeatherUseCase(
onFailure("I can not find such location, please try again.")
}
} else {
val isPermissionGranted = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val isPermissionGranted = permissionChecker.hasPermission("android.permission.ACCESS_COARSE_LOCATION")
if (!isPermissionGranted) {
onPermissionRequest(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION))
onPermissionRequest(arrayOf("android.permission.ACCESS_COARSE_LOCATION"))
} else {
onLocationRequest()
}
}
} catch (e: Exception) {
Log.e("GetWeatherUseCase", "Error fetching weather", e)
System.err.println("Error fetching weather: ${e.message}")
onFailure("Something went wrong, please try again.")
}
}

View file

@ -1,16 +1,12 @@
package com.app.assistant.usecase
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.app.assistant.R
import com.app.assistant.model.DeviceAction
import java.net.URI
class NavigateUseCase(private val context: Context) {
class NavigateUseCase(private val resourceProvider: ResourceProvider) {
suspend fun execute(
prompt: String,
onIntentTriggered: suspend (Intent) -> Unit,
onIntentTriggered: suspend (DeviceAction) -> Unit,
onSuccess: suspend (location: String, navigationUri: URI) -> Unit,
onFailure: suspend (errorMsg: String) -> Unit
) {
@ -18,24 +14,18 @@ class NavigateUseCase(private val context: Context) {
val sanitizedPrompt = prompt.replace("\\p{Punct}+".toRegex(), "")
val location = extractLocation(sanitizedPrompt)
if (location != null) {
val gmmIntentUri = Uri.parse("google.navigation:q=$location")
val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage("com.google.android.apps.maps")
}
onIntentTriggered(mapIntent)
val encodedURI = java.net.URLEncoder.encode(
location,
java.nio.charset.StandardCharsets.UTF_8.toString()
)
onIntentTriggered(DeviceAction.NavigateTo(location))
onSuccess(location, URI("google.navigation:q=$encodedURI"))
} else {
onFailure(context.getString(R.string.location_not_found_fallback))
onFailure(resourceProvider.getString("location_not_found"))
}
} catch (e: Exception) {
Log.d("NavigateUseCase", e.message.toString())
onFailure(context.getString(R.string.generic_error_fallback))
System.err.println("Error navigating: ${e.message}")
onFailure(resourceProvider.getString("generic_error"))
}
}

View file

@ -0,0 +1,5 @@
package com.app.assistant.usecase
interface PermissionChecker {
fun hasPermission(permission: String): Boolean
}

View file

@ -1,11 +1,7 @@
package com.app.assistant.usecase
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.app.assistant.BuildConfig
import com.app.assistant.R
import com.app.assistant.model.DeviceAction
import com.app.assistant.repository.SettingsRepository
import kotlinx.coroutines.CompletableDeferred
import kotlinx.serialization.Serializable
@ -52,7 +48,7 @@ data class YouTubeSearchResponse(
)
class PlaySongUseCase(
private val context: Context,
private val resourceProvider: ResourceProvider,
private val settingsRepository: SettingsRepository,
private val client: OkHttpClient
) {
@ -60,7 +56,7 @@ class PlaySongUseCase(
suspend fun execute(
prompt: String,
onIntentTriggered: suspend (Intent) -> Unit,
onIntentTriggered: suspend (DeviceAction) -> Unit,
onSuccess: suspend (songName: String, videoId: String, thumbnailUrl: String, videoUri: URI) -> Unit,
onMissingApiKey: suspend (searchQuery: String) -> Unit,
onFailure: suspend (errorMsg: String) -> Unit
@ -68,7 +64,7 @@ class PlaySongUseCase(
try {
val sanitizedPrompt = prompt.replace("\\p{Punct}+".toRegex(), "")
if (!sanitizedPrompt.lowercase().contains("play ")) {
onFailure(context.getString(R.string.song_not_found_fallback))
onFailure(resourceProvider.getString("song_not_found"))
return
}
val searchQuery = sanitizedPrompt.lowercase().substringAfter("play").trim()
@ -76,20 +72,12 @@ class PlaySongUseCase(
val (videoId, thumbnailUrl) = youtubeApiCall(searchQuery)
if (videoId.isEmpty()) {
onFailure(context.getString(R.string.song_not_found_fallback))
onFailure(resourceProvider.getString("song_not_found"))
} else if (videoId == "Missing API Key") {
val intent = Intent(Intent.ACTION_SEARCH).apply {
setPackage("com.google.android.youtube")
putExtra("query", searchQuery)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
onIntentTriggered(intent)
onIntentTriggered(DeviceAction.SearchSong(searchQuery))
onMissingApiKey(searchQuery)
} else {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.youtube.com/watch?v=$videoId")).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
onIntentTriggered(intent)
onIntentTriggered(DeviceAction.PlaySong(videoId))
onSuccess(
searchQuery,
videoId,
@ -98,8 +86,8 @@ class PlaySongUseCase(
)
}
} catch (e: Exception) {
Log.d("PlaySongUseCase", e.message.toString())
onFailure(context.getString(R.string.generic_error_fallback))
System.err.println("Error playing song: ${e.message}")
onFailure(resourceProvider.getString("generic_error"))
}
}
@ -161,7 +149,7 @@ class PlaySongUseCase(
}
}
} catch (e: Exception) {
Log.e("PlaySongUseCase", "Error parsing YouTube JSON", e)
System.err.println("Error parsing YouTube JSON: ${e.message}")
}
return Pair("", "")
}

View file

@ -0,0 +1,5 @@
package com.app.assistant.usecase
interface ResourceProvider {
fun getString(key: String): String
}

View file

@ -1,19 +1,15 @@
package com.app.assistant.usecase
import android.content.Context
import android.content.Intent
import android.provider.AlarmClock
import android.util.Log
import com.app.assistant.R
import com.app.assistant.model.DeviceAction
import java.util.Calendar
import java.util.Locale
class SetAlarmUseCase(private val context: Context) {
class SetAlarmUseCase(private val resourceProvider: ResourceProvider) {
suspend fun execute(
prompt: String,
dayOverride: String? = null,
onPromptForTime: suspend (dayMatch: String?) -> Unit,
onSuccess: suspend (intent: Intent) -> Unit,
onSuccess: suspend (action: DeviceAction) -> Unit,
onFailure: suspend (errorMsg: String) -> Unit
) {
try {
@ -26,21 +22,21 @@ class SetAlarmUseCase(private val context: Context) {
if (timeMatch == null && relativeTimeMatch == null) {
onPromptForTime(dayMatch)
} else {
val intent = setAlarmFromPrompt(dayMatch, timeMatch, relativeTimeMatch, context.getString(R.string.new_alarm_message))
onSuccess(intent)
val action = calculateAlarmAction(dayMatch, timeMatch, relativeTimeMatch, resourceProvider.getString("new_alarm_message"))
onSuccess(action)
}
} catch (e: Exception) {
Log.e("SetAlarmUseCase", "Error setting alarm", e)
onFailure(context.getString(R.string.generic_error_fallback))
System.err.println("Error setting alarm: ${e.message}")
onFailure(resourceProvider.getString("generic_error"))
}
}
private fun setAlarmFromPrompt(
private fun calculateAlarmAction(
dayMatch: String?,
timeMatch: MatchResult?,
relativeTimeMatch: MatchResult?,
message: String
): Intent {
): DeviceAction {
val calendar = Calendar.getInstance()
if (relativeTimeMatch != null) {
@ -130,17 +126,13 @@ class SetAlarmUseCase(private val context: Context) {
}
}
return Intent(AlarmClock.ACTION_SET_ALARM).apply {
putExtra(AlarmClock.EXTRA_MESSAGE, message)
putExtra(AlarmClock.EXTRA_HOUR, calendar.get(Calendar.HOUR_OF_DAY))
putExtra(AlarmClock.EXTRA_MINUTES, calendar.get(Calendar.MINUTE))
putExtra(AlarmClock.EXTRA_SKIP_UI, true)
if (repeatDays.isNotEmpty()) {
putExtra(AlarmClock.EXTRA_DAYS, ArrayList(repeatDays))
}
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return DeviceAction.SetAlarm(
message = message,
hour = calendar.get(Calendar.HOUR_OF_DAY),
minutes = calendar.get(Calendar.MINUTE),
repeatDays = repeatDays,
isReminder = false
)
}
companion object {

View file

@ -1,27 +1,23 @@
package com.app.assistant.usecase
import android.content.Context
import android.content.Intent
import android.provider.AlarmClock
import android.util.Log
import com.app.assistant.R
import com.app.assistant.model.DeviceAction
import java.util.Calendar
import java.util.Locale
class SetReminderUseCase(private val context: Context) {
class SetReminderUseCase(private val resourceProvider: ResourceProvider) {
suspend fun execute(
prompt: String,
dayOverride: String? = null,
contextOverride: String? = null,
onPromptForTime: suspend (dayMatch: String?, context: String) -> Unit,
onSuccess: suspend (intent: Intent) -> Unit,
onSuccess: suspend (action: DeviceAction) -> Unit,
onFailure: suspend (errorMsg: String) -> Unit
) {
try {
val sanitizedPrompt = prompt.replace(PunctuationRegex, "")
val contextMatch = ContextRegex.find(sanitizedPrompt)
val context = contextOverride ?: contextMatch?.groupValues?.get(1)?.trim() ?: this.context.getString(R.string.reminder_default_context)
val context = contextOverride ?: contextMatch?.groupValues?.get(1)?.trim() ?: resourceProvider.getString("reminder_default_context")
val timeMatch = TimeRegex.find(sanitizedPrompt)
val relativeTimeMatch = RelativeTimeRegex.find(sanitizedPrompt)
@ -30,21 +26,21 @@ class SetReminderUseCase(private val context: Context) {
if (timeMatch == null && relativeTimeMatch == null) {
onPromptForTime(dayMatch, context)
} else {
val intent = setReminderFromPrompt(dayMatch, timeMatch, relativeTimeMatch, context)
onSuccess(intent)
val action = calculateReminderAction(dayMatch, timeMatch, relativeTimeMatch, context)
onSuccess(action)
}
} catch (e: Exception) {
Log.e("SetReminderUseCase", "Error setting reminder", e)
onFailure(this.context.getString(R.string.generic_error_fallback))
System.err.println("Error setting reminder: ${e.message}")
onFailure(resourceProvider.getString("generic_error"))
}
}
private fun setReminderFromPrompt(
private fun calculateReminderAction(
dayMatch: String?,
timeMatch: MatchResult?,
relativeTimeMatch: MatchResult?,
context: String
): Intent {
): DeviceAction {
val calendar = Calendar.getInstance()
if (relativeTimeMatch != null) {
@ -134,18 +130,13 @@ class SetReminderUseCase(private val context: Context) {
}
}
return Intent(AlarmClock.ACTION_SET_ALARM).apply {
putExtra(AlarmClock.EXTRA_MESSAGE, context)
putExtra(AlarmClock.EXTRA_HOUR, calendar.get(Calendar.HOUR_OF_DAY))
putExtra(AlarmClock.EXTRA_MINUTES, calendar.get(Calendar.MINUTE))
putExtra(AlarmClock.EXTRA_SKIP_UI, true)
putExtra(AlarmClock.EXTRA_RINGTONE, AlarmClock.VALUE_RINGTONE_SILENT)
if (repeatDays.isNotEmpty()) {
putExtra(AlarmClock.EXTRA_DAYS, ArrayList(repeatDays))
}
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return DeviceAction.SetAlarm(
message = context,
hour = calendar.get(Calendar.HOUR_OF_DAY),
minutes = calendar.get(Calendar.MINUTE),
repeatDays = repeatDays,
isReminder = true
)
}
companion object {

View file

@ -0,0 +1,68 @@
package com.app.assistant.viewmodel
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.AlarmClock
import androidx.core.content.ContextCompat
import com.app.assistant.R
import com.app.assistant.model.DeviceAction
import com.app.assistant.usecase.PermissionChecker
import com.app.assistant.usecase.ResourceProvider
class AndroidPermissionChecker(private val context: Context) : PermissionChecker {
override fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
class AndroidResourceProvider(private val context: Context) : ResourceProvider {
override fun getString(key: String): String {
return when (key) {
"song_not_found" -> context.getString(R.string.song_not_found_fallback)
"generic_error" -> context.getString(R.string.generic_error_fallback)
"location_not_found" -> context.getString(R.string.location_not_found_fallback)
"new_alarm_message" -> context.getString(R.string.new_alarm_message)
"reminder_default_context" -> context.getString(R.string.reminder_default_context)
else -> ""
}
}
}
fun DeviceAction.toIntent(): Intent {
return when (this) {
is DeviceAction.MakeCall -> Intent(Intent.ACTION_CALL).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.parse("tel:$phoneNumber")
}
is DeviceAction.NavigateTo -> {
val gmmIntentUri = Uri.parse("google.navigation:q=$location")
Intent(Intent.ACTION_VIEW, gmmIntentUri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage("com.google.android.apps.maps")
}
}
is DeviceAction.PlaySong -> Intent(Intent.ACTION_VIEW, Uri.parse("https://www.youtube.com/watch?v=$videoId")).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
is DeviceAction.SearchSong -> Intent(Intent.ACTION_SEARCH).apply {
setPackage("com.google.android.youtube")
putExtra("query", query)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
is DeviceAction.SetAlarm -> Intent(AlarmClock.ACTION_SET_ALARM).apply {
putExtra(AlarmClock.EXTRA_MESSAGE, message)
putExtra(AlarmClock.EXTRA_HOUR, hour)
putExtra(AlarmClock.EXTRA_MINUTES, minutes)
putExtra(AlarmClock.EXTRA_SKIP_UI, true)
if (isReminder) {
putExtra(AlarmClock.EXTRA_RINGTONE, AlarmClock.VALUE_RINGTONE_SILENT)
}
if (repeatDays.isNotEmpty()) {
putExtra(AlarmClock.EXTRA_DAYS, ArrayList(repeatDays))
}
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}

View file

@ -26,8 +26,8 @@ fun MainViewModel.callContact(
_uiEvent.emit(UIEvent.RequestPermissions(permissions, 102))
processResponse(getRandomResponse(ResponseStrings.permissionContactsCall(getApplication())), loadingItemId, speak, Category.OTHER)
},
onIntentTriggered = { intent ->
_uiEvent.emit(UIEvent.StartIntent(intent))
onIntentTriggered = { action ->
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
},
onSuccess = { name, dialUri ->
processResponse(
@ -57,8 +57,8 @@ fun MainViewModel.playSong(
viewModelScope.launch {
playSongUseCase.execute(
prompt = prompt,
onIntentTriggered = { intent ->
_uiEvent.emit(UIEvent.StartIntent(intent))
onIntentTriggered = { action ->
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
},
onSuccess = { songName, videoId, thumbnailUrl, videoUri ->
processResponse(
@ -91,8 +91,8 @@ fun MainViewModel.navigate(
viewModelScope.launch {
navigateUseCase.execute(
prompt = prompt,
onIntentTriggered = { intent ->
_uiEvent.emit(UIEvent.StartIntent(intent))
onIntentTriggered = { action ->
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
},
onSuccess = { location, navigationUri ->
processResponse(
@ -219,8 +219,8 @@ fun MainViewModel.setAlarm(
Category.OTHER
)
},
onSuccess = { intent ->
_uiEvent.emit(UIEvent.StartIntent(intent))
onSuccess = { action ->
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
processResponse(
getRandomResponse(ResponseStrings.alarmSetSuccess(getApplication())),
loadingItemId,
@ -249,9 +249,9 @@ fun MainViewModel.handleAlarmLockState(
onPromptForTime = {
processResponse(getRandomResponse(ResponseStrings.invalidTime(getApplication())), loadingItemId, speak, Category.OTHER)
},
onSuccess = { intent ->
onSuccess = { action ->
lockState = LockState.None
_uiEvent.emit(UIEvent.StartIntent(intent))
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
processResponse(getRandomResponse(ResponseStrings.alarmSetSuccess(getApplication())), loadingItemId, speak, Category.ALARM)
},
onFailure = { errorMsg ->
@ -276,8 +276,8 @@ fun MainViewModel.setReminder(
lockState = LockState.LockReminder(day = dayMatch, context = context)
processResponse(getRandomResponse(ResponseStrings.promptForTime(getApplication())), loadingItemId, speak, Category.OTHER)
},
onSuccess = { intent ->
_uiEvent.emit(UIEvent.StartIntent(intent))
onSuccess = { action ->
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
processResponse(getRandomResponse(ResponseStrings.reminderSetSuccess(getApplication())), loadingItemId, speak, category)
},
onFailure = { errorMsg ->
@ -302,9 +302,9 @@ fun MainViewModel.handleReminderLockState(
onPromptForTime = { _, _ ->
processResponse(getRandomResponse(ResponseStrings.invalidTime(getApplication())), loadingItemId, speak, Category.OTHER)
},
onSuccess = { intent ->
onSuccess = { action ->
lockState = LockState.None
_uiEvent.emit(UIEvent.StartIntent(intent))
_uiEvent.emit(UIEvent.StartIntent(action.toIntent()))
processResponse(getRandomResponse(ResponseStrings.reminderSetSuccess(getApplication())), loadingItemId, speak, Category.ALARM)
},
onFailure = { errorMsg ->

View file

@ -32,13 +32,16 @@ class MainViewModelFactory(
val weatherRepo = WeatherRepository(application, okHttpClient)
val permissionChecker = AndroidPermissionChecker(application)
val resourceProvider = AndroidResourceProvider(application)
val getAiResponseUseCase = GetAiResponseUseCase(settingsRepo, okHttpClient)
val callContactUseCase = CallContactUseCase(application, contactsRepo)
val playSongUseCase = PlaySongUseCase(application, settingsRepo, okHttpClient)
val navigateUseCase = NavigateUseCase(application)
val getWeatherUseCase = GetWeatherUseCase(application, weatherRepo, getAiResponseUseCase)
val setAlarmUseCase = SetAlarmUseCase(application)
val setReminderUseCase = SetReminderUseCase(application)
val callContactUseCase = CallContactUseCase(permissionChecker, contactsRepo)
val playSongUseCase = PlaySongUseCase(resourceProvider, settingsRepo, okHttpClient)
val navigateUseCase = NavigateUseCase(resourceProvider)
val getWeatherUseCase = GetWeatherUseCase(permissionChecker, weatherRepo, getAiResponseUseCase)
val setAlarmUseCase = SetAlarmUseCase(resourceProvider)
val setReminderUseCase = SetReminderUseCase(resourceProvider)
val processChatCommandUseCase = ProcessChatCommandUseCase(getAiResponseUseCase)
@Suppress("UNCHECKED_CAST")