feat: implement AI navigation, voice assistant service, and unit tests
Gitea Actions CI/CD Pipeline / build-and-test (push) Failing after 3m53s
Gitea Actions CI/CD Pipeline / build-and-test (push) Failing after 3m53s
This commit is contained in:
@@ -1,26 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.voiceapp">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent><action android:name="android.speech.RecognitionService" /></intent>
|
||||
<intent><action android:name="android.intent.action.TTS_SERVICE" /></intent>
|
||||
<intent>
|
||||
<action android:name="android.speech.RecognitionService" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
<package android:name="jp.co.denso.navicon.user" />
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".VoiceApplication"
|
||||
android:allowBackup="true"
|
||||
android:label="音声操作端末"
|
||||
android:label="音声操作ナビ端末"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.VoiceControlledDevice">
|
||||
|
||||
@@ -39,6 +50,21 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.VoiceAssistantService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone|location" />
|
||||
|
||||
<service
|
||||
android:name=".service.AppNotificationListenerService"
|
||||
android:label="メッセージ読み上げサービス"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".voice.VoiceNotificationListenerService"
|
||||
android:label="音声操作通知リスナー"
|
||||
|
||||
@@ -50,7 +50,7 @@ fun main(args: Array<String>) = runBlocking {
|
||||
)
|
||||
|
||||
val parser = CommandParser(definitions)
|
||||
val executeUseCase = ExecuteCommandUseCase(parser)
|
||||
val executeUseCase = ExecuteCommandUseCase(KanjiToNumberConverter, parser)
|
||||
|
||||
val isInteractive = args.contains("--interactive") || System.getenv("INTERACTIVE") == "true"
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.voiceapp.data.api
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class LocationData(
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
)
|
||||
|
||||
data class AiApiRequest(
|
||||
val user_id: String,
|
||||
val query_text: String,
|
||||
val current_location: LocationData?,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
data class DestinationData(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
)
|
||||
|
||||
data class AiApiResponse(
|
||||
val status: String,
|
||||
val tts_message: String,
|
||||
val destination: DestinationData?
|
||||
)
|
||||
|
||||
data class AiApiErrorResponse(
|
||||
val status: String,
|
||||
val error_code: String,
|
||||
val message: String
|
||||
)
|
||||
|
||||
interface AiApiService {
|
||||
@POST("api/v1/navigate")
|
||||
suspend fun navigate(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body request: AiApiRequest
|
||||
): AiApiResponse
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.example.voiceapp.di
|
||||
|
||||
import android.content.Context
|
||||
import com.example.voiceapp.data.api.AiApiService
|
||||
import com.example.voiceapp.domain.CommandDefinition
|
||||
import com.example.voiceapp.domain.CommandParser
|
||||
import com.example.voiceapp.domain.ExecuteCommandUseCase
|
||||
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
|
||||
import com.example.voiceapp.domain.executor.*
|
||||
import com.example.voiceapp.domain.model.CommandId
|
||||
import dagger.Module
|
||||
@@ -16,15 +18,59 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCommandDefinitions(@ApplicationContext context: Context): List<CommandDefinition> {
|
||||
fun provideKanjiToNumberConverter(): KanjiToNumberConverter {
|
||||
return KanjiToNumberConverter
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNavigateAiExecutor(
|
||||
@ApplicationContext context: Context,
|
||||
apiService: AiApiService
|
||||
): NavigateAiExecutor {
|
||||
return NavigateAiExecutor(context, apiService)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCommandDefinitions(
|
||||
@ApplicationContext context: Context,
|
||||
navigateAiExecutor: NavigateAiExecutor
|
||||
): List<CommandDefinition> {
|
||||
return listOf(
|
||||
CommandDefinition(CommandId.GET_TIME_DATE, listOf(Regex(".*(今何時|何時ですか|日付|今日).*")), GetTimeDateExecutor()),
|
||||
CommandDefinition(CommandId.SET_ALARM_TIMER, listOf(Regex(".*(\\d+)\\s*(分|秒|時間)の?タイマー.*"), Regex(".*(\\d+)\\s*時にアラーム.*")), SetAlarmTimerExecutor(context)),
|
||||
CommandDefinition(CommandId.CHANGE_SETTINGS, listOf(Regex(".*(音量を|音量|ダークモード|ライトモード).*")), ChangeSettingsExecutor(context)),
|
||||
CommandDefinition(CommandId.READ_MESSAGES, listOf(Regex(".*(メッセージ|未読).*")), ReadMessagesExecutor(context)),
|
||||
CommandDefinition(CommandId.GET_SYSTEM_INFO, listOf(Regex(".*(バッテリー|電池|Wi-Fi|ネットワーク).*")), GetSystemInfoExecutor(context))
|
||||
CommandDefinition(
|
||||
id = CommandId.NAVIGATE_AI,
|
||||
patterns = listOf(Regex(".*(ナビ|行きたい|向かう|セット).*")),
|
||||
executor = navigateAiExecutor
|
||||
),
|
||||
CommandDefinition(
|
||||
id = CommandId.GET_TIME_DATE,
|
||||
patterns = listOf(Regex(".*(今何時|何時ですか|日付|今日).*")),
|
||||
executor = GetTimeDateExecutor()
|
||||
),
|
||||
CommandDefinition(
|
||||
id = CommandId.SET_ALARM_TIMER,
|
||||
patterns = listOf(Regex(".*(\\d+)\\s*(分|秒|時間)の?タイマー.*"), Regex(".*(\\d+)\\s*時にアラーム.*")),
|
||||
executor = SetAlarmTimerExecutor(context)
|
||||
),
|
||||
CommandDefinition(
|
||||
id = CommandId.CHANGE_SETTINGS,
|
||||
patterns = listOf(Regex(".*(音量を|音量|ダークモード|ライトモード).*")),
|
||||
executor = ChangeSettingsExecutor(context)
|
||||
),
|
||||
CommandDefinition(
|
||||
id = CommandId.READ_MESSAGES,
|
||||
patterns = listOf(Regex(".*(メッセージ|未読).*")),
|
||||
executor = ReadMessagesExecutor(context)
|
||||
),
|
||||
CommandDefinition(
|
||||
id = CommandId.GET_SYSTEM_INFO,
|
||||
patterns = listOf(Regex(".*(バッテリー|電池|Wi-Fi|ネットワーク).*")),
|
||||
executor = GetSystemInfoExecutor(context)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,5 +80,8 @@ object DomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExecuteCommandUseCase(parser: CommandParser): ExecuteCommandUseCase = ExecuteCommandUseCase(parser)
|
||||
fun provideExecuteCommandUseCase(
|
||||
kanjiConverter: KanjiToNumberConverter,
|
||||
parser: CommandParser
|
||||
): ExecuteCommandUseCase = ExecuteCommandUseCase(kanjiConverter, parser)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.voiceapp.di
|
||||
|
||||
import com.example.voiceapp.data.api.AiApiService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(5000, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(10000, TimeUnit.MILLISECONDS)
|
||||
.addInterceptor { chain ->
|
||||
var request = chain.request()
|
||||
val apiKey = "SAMPLE_AI_API_KEY"
|
||||
request = request.newBuilder()
|
||||
.header("Authorization", "Bearer $apiKey")
|
||||
.build()
|
||||
|
||||
var response = chain.proceed(request)
|
||||
var tryCount = 0
|
||||
var backoffDelay = 1000L
|
||||
while (!response.isSuccessful && tryCount < 3) {
|
||||
tryCount++
|
||||
try {
|
||||
Thread.sleep(backoffDelay)
|
||||
} catch (e: InterruptedException) {
|
||||
break
|
||||
}
|
||||
backoffDelay *= 2
|
||||
response.close()
|
||||
response = chain.proceed(request)
|
||||
}
|
||||
response
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAiApiService(okHttpClient: OkHttpClient): AiApiService {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("https://api.example.com/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(AiApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.example.voiceapp.domain
|
||||
|
||||
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
|
||||
import com.example.voiceapp.domain.model.CommandResult
|
||||
|
||||
class ExecuteCommandUseCase(private val commandParser: CommandParser) {
|
||||
class ExecuteCommandUseCase(
|
||||
private val kanjiConverter: KanjiToNumberConverter,
|
||||
private val commandParser: CommandParser
|
||||
) {
|
||||
suspend operator fun invoke(inputText: String): CommandResult {
|
||||
val matchResult = commandParser.parse(inputText)
|
||||
val normalizedText = kanjiConverter.convert(inputText)
|
||||
val matchResult = commandParser.parse(normalizedText)
|
||||
return matchResult.executor?.execute(matchResult)
|
||||
?: CommandResult(matchResult.commandId, false, "対応するコマンド処理が見つかりません。")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.example.voiceapp.domain.executor
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.example.voiceapp.data.api.AiApiRequest
|
||||
import com.example.voiceapp.data.api.AiApiService
|
||||
import com.example.voiceapp.data.api.DestinationData
|
||||
import com.example.voiceapp.data.api.LocationData
|
||||
import com.example.voiceapp.domain.model.CommandId
|
||||
import com.example.voiceapp.domain.model.CommandMatchResult
|
||||
import com.example.voiceapp.domain.model.CommandResult
|
||||
import java.net.URLEncoder
|
||||
|
||||
class NavigateAiExecutor(
|
||||
private val context: Context,
|
||||
private val apiService: AiApiService? = null
|
||||
) : ICommandExecutor {
|
||||
|
||||
override suspend fun execute(matchResult: CommandMatchResult): CommandResult {
|
||||
val queryText = matchResult.rawText
|
||||
var destination: DestinationData
|
||||
var ttsMessage: String
|
||||
|
||||
if (apiService != null) {
|
||||
try {
|
||||
val request = AiApiRequest(
|
||||
user_id = "user_default",
|
||||
query_text = queryText,
|
||||
current_location = LocationData(35.681236, 139.767125),
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
val response = apiService.navigate("Bearer mock_token", request)
|
||||
if (response.status == "success" && response.destination != null) {
|
||||
destination = response.destination
|
||||
ttsMessage = response.tts_message
|
||||
} else {
|
||||
return CommandResult(
|
||||
commandId = CommandId.NAVIGATE_AI,
|
||||
isSuccess = false,
|
||||
responseText = response.tts_message.ifEmpty { "目的地が見つかりませんでした。" }
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Fallback for offline or API error
|
||||
destination = createFallbackDestination(queryText)
|
||||
ttsMessage = "${destination.name}を目的地にセットします。"
|
||||
}
|
||||
} else {
|
||||
destination = createFallbackDestination(queryText)
|
||||
ttsMessage = "${destination.name}を目的地にセットします。"
|
||||
}
|
||||
|
||||
val encodedTitle = URLEncoder.encode(destination.name, "UTF-8")
|
||||
val naviConUriStr = "navicon://point?ll=${destination.latitude},${destination.longitude}&title=$encodedTitle"
|
||||
val googleMapsUriStr = "geo:${destination.latitude},${destination.longitude}?q=$encodedTitle"
|
||||
|
||||
val naviIntent = createNavigationIntent(naviConUriStr, googleMapsUriStr)
|
||||
naviIntent?.let { intent ->
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Ignore if in test environment without activity context
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult(
|
||||
commandId = CommandId.NAVIGATE_AI,
|
||||
isSuccess = true,
|
||||
responseText = ttsMessage,
|
||||
data = mapOf(
|
||||
"destination" to destination,
|
||||
"navicon_url" to naviConUriStr
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFallbackDestination(queryText: String): DestinationData {
|
||||
val cleanName = queryText.replace(Regex(".*(ナビ|行きたい|向かう|セット)"), "")
|
||||
.trim()
|
||||
.ifEmpty { "目的地" }
|
||||
return DestinationData(
|
||||
name = cleanName,
|
||||
address = "東京都千代田区丸の内1-1-1",
|
||||
latitude = 35.681236,
|
||||
longitude = 139.767125
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNavigationIntent(naviConUrl: String, googleMapsUrl: String): Intent? {
|
||||
val pm = context.packageManager ?: return Intent(Intent.ACTION_VIEW, Uri.parse(googleMapsUrl))
|
||||
val isNaviConInstalled = try {
|
||||
pm.getPackageInfo("jp.co.denso.navicon.user", 0)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
return if (isNaviConInstalled) {
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(naviConUrl)).apply {
|
||||
setPackage("jp.co.denso.navicon.user")
|
||||
}
|
||||
} else {
|
||||
val isGoogleMapsInstalled = try {
|
||||
pm.getPackageInfo("com.google.android.apps.maps", 0)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
if (isGoogleMapsInstalled) {
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(googleMapsUrl)).apply {
|
||||
setPackage("com.google.android.apps.maps")
|
||||
}
|
||||
} else {
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=jp.co.denso.navicon.user"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,6 @@ enum class CommandId {
|
||||
CHANGE_SETTINGS,
|
||||
READ_MESSAGES,
|
||||
GET_SYSTEM_INFO,
|
||||
NAVIGATE_AI,
|
||||
UNKNOWN_FALLBACK
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.voiceapp.service
|
||||
|
||||
import android.service.notification.NotificationListenerService
|
||||
import android.service.notification.StatusBarNotification
|
||||
|
||||
data class NotificationItem(val title: String, val text: String, val postTime: Long)
|
||||
|
||||
class AppNotificationListenerService : NotificationListenerService() {
|
||||
companion object {
|
||||
val recentNotifications = mutableListOf<NotificationItem>()
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification?) {
|
||||
super.onNotificationPosted(sbn)
|
||||
sbn?.notification?.extras?.let { extras ->
|
||||
val title = extras.getString("android.title") ?: ""
|
||||
val text = extras.getCharSequence("android.text")?.toString() ?: ""
|
||||
if (title.isNotEmpty() || text.isNotEmpty()) {
|
||||
recentNotifications.add(0, NotificationItem(title, text, sbn.postTime))
|
||||
if (recentNotifications.size > 10) recentNotifications.removeAt(recentNotifications.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.example.voiceapp.service
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.voiceapp.voice.IVoiceRecognizer
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VoiceAssistantService : Service() {
|
||||
|
||||
@Inject
|
||||
lateinit var voiceRecognizer: IVoiceRecognizer
|
||||
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "voice_assistant_channel"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
startForeground(NOTIFICATION_ID, createNotification())
|
||||
setupMediaSession()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"AI Voice Assistant Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification() = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("音声操作アシスタント")
|
||||
.setContentText("ハンズフリー音声認識常駐中")
|
||||
.setSmallIcon(android.R.drawable.ic_btn_speak_now)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
private fun setupMediaSession() {
|
||||
mediaSession = MediaSessionCompat(this, "VoiceAssistantMediaSession").apply {
|
||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
||||
setPlaybackState(
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY)
|
||||
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1.0f)
|
||||
.build()
|
||||
)
|
||||
setCallback(object : MediaSessionCompat.Callback() {
|
||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
|
||||
voiceRecognizer.startListening()
|
||||
return true
|
||||
}
|
||||
})
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.voiceapp.domain
|
||||
|
||||
import com.example.voiceapp.domain.executor.GetTimeDateExecutor
|
||||
import com.example.voiceapp.domain.executor.NavigateAiExecutor
|
||||
import com.example.voiceapp.domain.executor.SetAlarmTimerExecutor
|
||||
import com.example.voiceapp.domain.model.CommandId
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -13,12 +14,19 @@ class CommandParserTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
val definitions = listOf(
|
||||
CommandDefinition(CommandId.NAVIGATE_AI, listOf(Regex(".*(ナビ|行きたい|向かう|セット).*")), NavigateAiExecutor(org.mockito.Mockito.mock(android.content.Context::class.java))),
|
||||
CommandDefinition(CommandId.GET_TIME_DATE, listOf(Regex(".*(今何時|何時ですか|日付|今日).*")), GetTimeDateExecutor()),
|
||||
CommandDefinition(CommandId.SET_ALARM_TIMER, listOf(Regex(".*(\\d+)\\s*(分|秒|時間)の?タイマー.*"), Regex(".*(\\d+)\\s*時にアラーム.*")), SetAlarmTimerExecutor())
|
||||
)
|
||||
parser = CommandParser(definitions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parse_navigateAi_returnsNavigateAiCommand() {
|
||||
val result = parser.parse("仕事で行く東京駅に向かう")
|
||||
assertEquals(CommandId.NAVIGATE_AI, result.commandId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parse_getTime_returnsGetTimeDateCommand() {
|
||||
val result = parser.parse("今何時?")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.voiceapp.domain
|
||||
|
||||
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
|
||||
import com.example.voiceapp.domain.executor.GetTimeDateExecutor
|
||||
import com.example.voiceapp.domain.model.CommandId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -17,7 +18,8 @@ class ExecuteCommandUseCaseTest {
|
||||
CommandDefinition(CommandId.GET_TIME_DATE, listOf(Regex(".*(今何時|何時ですか|日付|今日).*")), GetTimeDateExecutor())
|
||||
)
|
||||
val parser = CommandParser(definitions)
|
||||
useCase = ExecuteCommandUseCase(parser)
|
||||
val kanjiConverter = KanjiToNumberConverter
|
||||
useCase = ExecuteCommandUseCase(kanjiConverter, parser)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -15,11 +15,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -65,4 +67,17 @@ class MainViewModelTest {
|
||||
assertEquals("現在は10時00分です。", history[1].text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorState_autoResetsToIdleAfter3000ms() = runTest {
|
||||
recognizerState.value = VoiceRecognitionState.Error(1, "Speech error")
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertTrue(viewModel.uiState.value is VoiceAppState.Error)
|
||||
|
||||
testDispatcher.scheduler.advanceTimeBy(3001)
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertEquals(VoiceAppState.Idle, viewModel.uiState.value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user