diff --git a/client-test/src/main/kotlin/com/speechly/clienttest/MainActivity.kt b/client-test/src/main/kotlin/com/speechly/clienttest/MainActivity.kt index 0b4b27a..927201c 100644 --- a/client-test/src/main/kotlin/com/speechly/clienttest/MainActivity.kt +++ b/client-test/src/main/kotlin/com/speechly/clienttest/MainActivity.kt @@ -1,6 +1,5 @@ package com.speechly.clienttest -import android.app.Activity import android.os.Bundle import android.view.MotionEvent import android.view.View @@ -34,7 +33,11 @@ val repoList = listOf( class MainActivity : AppCompatActivity() { - private var speechlyClient: Client? = null + private var speechlyClient: Client = Client.fromActivity( + activity = getActivity(), + appId = UUID.randomUUID() + ) + private var button: SpeechlyButton? = null private var textView: TextView? = null private var recyclerView: RecyclerView? = null @@ -48,7 +51,7 @@ class MainActivity : AppCompatActivity() { MotionEvent.ACTION_DOWN -> { textView?.visibility = View.VISIBLE textView?.text = "" - speechlyClient!!.startContext() + speechlyClient?.startContext() } MotionEvent.ACTION_UP -> { speechlyClient!!.stopContext() @@ -62,24 +65,21 @@ class MainActivity : AppCompatActivity() { } } - fun getActivity(): Activity { + fun getActivity(): AppCompatActivity { return this } override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - this.button = findViewById(R.id.speechly) + this.button = findViewById(R.id.speechly) this.recyclerView = findViewById(R.id.recycler_view) this.textView = findViewById(R.id.textView) textView?.visibility = View.INVISIBLE GlobalScope.launch(Dispatchers.Default) { - speechlyClient = Client.fromActivity( - activity = getActivity(), - appId = UUID.fromString(UUID.randomUUID().toString()) - ) speechlyClient?.onSegmentChange { segment: Segment -> val transcript: String = segment.words.values.map{it.value}.joinToString(" ") @@ -89,7 +89,7 @@ class MainActivity : AppCompatActivity() { if (segment.intent != null) { when(segment.intent?.intent) { "filter" -> { - lunguageFilter = segment.getEntityByType("language")?.value + languageFilter = segment.getEntityByType("language")?.value updateList() } "sort" -> { @@ -97,7 +97,7 @@ class MainActivity : AppCompatActivity() { updateList() } "reset" -> { - lunguageFilter = null + languageFilter = null updateList() } } @@ -118,11 +118,11 @@ class MainActivity : AppCompatActivity() { fun updateList() { val list = repoList.filter { repo: Repo -> - lunguageFilter == null || repo.language == lunguageFilter + languageFilter == null || repo.language == languageFilter }.sortedWith(Comparator { r1: Repo, r2: Repo -> - when(sortField) { - "NAME" -> if (r1.name > r2.name) 1 else -1 - "LANGUAGE" -> if (r1.language > r2.language) 1 else -1 + when (sortField) { + "NAME" -> if (r1.name > r2.name) 1 else -1 + "LANGUAGE" -> if (r1.language > r2.language) 1 else -1 "FOLLOWERS" -> r2.followers - r1.followers "STARS" -> r2.stars - r1.stars "FORKS" -> r2.forks - r1.forks diff --git a/client-test/src/main/res/layout/activity_main.xml b/client-test/src/main/res/layout/activity_main.xml index 7b54e25..73511f7 100644 --- a/client-test/src/main/res/layout/activity_main.xml +++ b/client-test/src/main/res/layout/activity_main.xml @@ -5,8 +5,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> - - - - \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index 2458adb..cec04ab 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -45,11 +45,11 @@ android { } dependencies { - def activity_version = "1.2.0-rc01" + def activity_version = "1.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' @@ -64,6 +64,6 @@ dependencies { androidTestImplementation "com.android.support.test:runner:1.0.2" androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2" - + implementation 'androidx.fragment:fragment:1.3.0' implementation "androidx.activity:activity-ktx:$activity_version" } diff --git a/client/src/main/kotlin/com/speechly/client/device/DeviceIdProvider.kt b/client/src/main/kotlin/com/speechly/client/device/DeviceIdProvider.kt index 9e7f0a3..8ec4aad 100644 --- a/client/src/main/kotlin/com/speechly/client/device/DeviceIdProvider.kt +++ b/client/src/main/kotlin/com/speechly/client/device/DeviceIdProvider.kt @@ -39,9 +39,11 @@ class RandomIdProvider : DeviceIdProvider { * By default it uses a random id provider for generating a new id in case of a cache miss. */ class CachingIdProvider( - private val cacheService: CacheService, private val baseProvider: DeviceIdProvider = RandomIdProvider() ) : DeviceIdProvider { + + var cacheService: CacheService? = null + private val cacheKey = "speechly-device-id" override fun getDeviceId(): DeviceId { @@ -49,7 +51,7 @@ class CachingIdProvider( } private fun loadFromCache(): DeviceId? { - val cached = this.cacheService.loadString(this.cacheKey) ?: return null + val cached = this.cacheService?.loadString(this.cacheKey) ?: return null return try { UUID.fromString(cached) @@ -63,7 +65,7 @@ class CachingIdProvider( // `storeString` returns false if the write operation has failed. // Current we choose to ignore failed writes and instead re-generate the id on the next call. - this.cacheService.storeString(this.cacheKey, id.toString()) + this.cacheService?.storeString(this.cacheKey, id.toString()) return id } diff --git a/client/src/main/kotlin/com/speechly/client/identity/IdentityService.kt b/client/src/main/kotlin/com/speechly/client/identity/IdentityService.kt index ef73505..0c514c5 100644 --- a/client/src/main/kotlin/com/speechly/client/identity/IdentityService.kt +++ b/client/src/main/kotlin/com/speechly/client/identity/IdentityService.kt @@ -70,30 +70,31 @@ class BasicIdentityService( * @param cacheService an implementation of PersistentCache that is used for storing cached tokens */ class CachingIdentityService( - private val baseService: IdentityService, - private val cacheService: CacheService + private val baseService: IdentityService ) : IdentityService { + + // cacheService an implementation of PersistentCache that is used for storing cached tokens + var cacheService: CacheService? = null + companion object { /** * Creates a new identity service using default gRPC client implementation. * - * @param cacheService an implementation of PersistentCache that is used for storing cached tokens * @param target the address of the API endpoint to connect to, e.g. "api.speechly.com" * @param secure whether to use secured (TLS) or plaintext connection */ fun forTarget( - cacheService: CacheService, target: String = "api.speechly.com", secure: Boolean = true ): CachingIdentityService { return CachingIdentityService( - BasicIdentityService.forTarget(target, secure), - cacheService + BasicIdentityService.forTarget(target, secure) ) } } override suspend fun authenticate(appId: UUID, deviceId: UUID): AuthToken { + println("\n\n\n***** appId ${appId} deviceId ${deviceId} ***** \n\n\n") // Try to load the token from the cache. val token = this.loadToken(appId, deviceId) @@ -112,7 +113,7 @@ class CachingIdentityService( } private fun loadToken(appId: UUID, deviceId: UUID): AuthToken? { - val cacheValue = this.cacheService.loadString(this.makeCacheKey(appId, deviceId)) ?: return null + val cacheValue = this.cacheService?.loadString(this.makeCacheKey(appId, deviceId)) ?: return null return try { AuthToken.fromJWT(cacheValue) @@ -124,7 +125,7 @@ class CachingIdentityService( private suspend fun reloadToken(appId: UUID, deviceId: UUID): AuthToken { val token = this.baseService.authenticate(appId, deviceId) - this.cacheService.storeString(this.makeCacheKey(token.appId, token.deviceId), token.tokenString) + this.cacheService?.storeString(this.makeCacheKey(token.appId, token.deviceId), token.tokenString) return token } diff --git a/client/src/main/kotlin/com/speechly/client/speech/AudioRecorder.kt b/client/src/main/kotlin/com/speechly/client/speech/AudioRecorder.kt index bfd330d..74c5372 100644 --- a/client/src/main/kotlin/com/speechly/client/speech/AudioRecorder.kt +++ b/client/src/main/kotlin/com/speechly/client/speech/AudioRecorder.kt @@ -1,10 +1,13 @@ package com.speechly.client.speech import android.Manifest -import android.media.AudioRecord +import android.content.pm.PackageManager import android.media.AudioFormat +import android.media.AudioRecord import android.media.MediaRecorder -import android.content.pm.PackageManager +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat.requestPermissions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -13,24 +16,23 @@ import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -val REQUEST_AUDIO = 1 - -class AudioRecorder(var activity: android.app.Activity, val sampleRate: Int) { +class AudioRecorder(var activity: AppCompatActivity, val sampleRate: Int) { private var recording = false private var recorder: AudioRecord? = null + val channelMask = AudioFormat.CHANNEL_IN_MONO + val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, AudioFormat.ENCODING_PCM_16BIT) * 4 - private var permissionGranted: Boolean = - activity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED - - private var bufferSize: Int? = null - - init { - val channelMask = AudioFormat.CHANNEL_IN_MONO - - bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, AudioFormat.ENCODING_PCM_16BIT) * 4 + val requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + buildRecorder() + } else { + throw Exception("Need access to microphone") + } + } - if (permissionGranted) { + fun buildRecorder() { + if (activity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { recorder = AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) .setAudioFormat(AudioFormat.Builder() @@ -41,7 +43,7 @@ class AudioRecorder(var activity: android.app.Activity, val sampleRate: Int) { .setBufferSizeInBytes(bufferSize!!) .build() } else { - activity.requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_AUDIO) + requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) } } diff --git a/client/src/main/kotlin/com/speechly/client/speech/Client.kt b/client/src/main/kotlin/com/speechly/client/speech/Client.kt index a2cea8a..336a81e 100644 --- a/client/src/main/kotlin/com/speechly/client/speech/Client.kt +++ b/client/src/main/kotlin/com/speechly/client/speech/Client.kt @@ -1,5 +1,9 @@ package com.speechly.client.speech +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent import com.speechly.client.cache.SharedPreferencesCache import com.speechly.client.device.CachingIdProvider import com.speechly.client.device.DeviceIdProvider @@ -48,21 +52,32 @@ class Client ( companion object { fun fromActivity( - activity: android.app.Activity, + activity: AppCompatActivity, appId: UUID, language: StreamConfig.LanguageCode = StreamConfig.LanguageCode.EN_US, target: String = "api.speechly.com", secure: Boolean = true ): Client { - val cache = SharedPreferencesCache.fromContext(activity.getApplicationContext()) + val cachingIdProvider = CachingIdProvider() + val cachingIdentityService = CachingIdentityService.forTarget(target, secure) + val audioRecorder = AudioRecorder(activity, 16000) + activity.lifecycle.addObserver(object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun connectListener() { + val cache = SharedPreferencesCache.fromContext(activity.getApplicationContext()) + cachingIdProvider.cacheService = cache + cachingIdentityService.cacheService = cache + audioRecorder.buildRecorder() + } + }) return Client( appId, language, - CachingIdProvider(cache), - CachingIdentityService.forTarget(cache, target, secure), + cachingIdProvider, + cachingIdentityService, GrpcSluClient.forTarget(target, secure), - AudioRecorder(activity, 16000) + audioRecorder ) } } @@ -147,8 +162,7 @@ class Client ( streams.add(stream) var segment: Segment? = null - var contextId: String? = null - GlobalScope.launch(Dispatchers.IO) { + GlobalScope.launch(Dispatchers.Default) { try { stream.responseFlow?.collect { response: SLUResponse -> if (segment == null) {