diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd892c4c..4c1b78b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { defaultConfig { applicationId = "com.joeloewi.croissant" - versionCode = 44 + versionCode = 45 versionName = "1.2.1" targetSdk = 34 @@ -151,8 +151,6 @@ dependencies { implementation(libs.androidx.profileinstaller) - implementation(libs.tts) - //open source license activity implementation(libs.gms.play.services.oss.licenses) -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e9523993..2d7fa733 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,9 @@ + + + Unit, onShowDefaultScreen: () -> Unit ) { + val context = LocalContext.current LaunchedEffect(Unit) { withContext(Dispatchers.IO) { snapshotFlow { isFirstLaunch() }.catch { }.filterNotNull() .collect { showFirstLaunchScreen -> withContext(Dispatchers.Main) { - if (showFirstLaunchScreen) { + val anyOfPermissionsIsDenied = listOf( + CroissantPermission.AccessHoYoLABSession.permission, + CroissantPermission.PostNotifications.permission + ).any { + ContextCompat.checkSelfPermission( + context, + it + ) == PackageManager.PERMISSION_DENIED + } || context.getSystemService() + ?.canScheduleExactAlarmsCompat() == false + + if (showFirstLaunchScreen || anyOfPermissionsIsDenied) { onShowFirstLaunchScreen() } else { onShowDefaultScreen() diff --git a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/global/screen/FirstLaunchScreen.kt b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/global/screen/FirstLaunchScreen.kt index 1ea84cb8..d08fe3eb 100644 --- a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/global/screen/FirstLaunchScreen.kt +++ b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/global/screen/FirstLaunchScreen.kt @@ -62,6 +62,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.joeloewi.croissant.R import com.joeloewi.croissant.ui.theme.DefaultDp @@ -101,7 +102,7 @@ private fun FirstLaunchContent( CroissantPermission.PostNotifications.permission ), onPermissionsResult = { - if (scheduleExactAlarmPermissionState.status != PermissionStatus.Granted) { + if (!scheduleExactAlarmPermissionState.status.isGranted) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM) diff --git a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/settings/screen/DeveloperInfoScreen.kt b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/settings/screen/DeveloperInfoScreen.kt index 829ad0b9..53e386c5 100644 --- a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/settings/screen/DeveloperInfoScreen.kt +++ b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/settings/screen/DeveloperInfoScreen.kt @@ -2,6 +2,7 @@ package com.joeloewi.croissant.ui.navigation.main.settings.screen import android.content.Intent import android.net.Uri +import android.speech.tts.TextToSpeech import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -37,6 +38,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner @@ -49,7 +51,6 @@ import com.joeloewi.croissant.util.navigationIconButton import com.joeloewi.croissant.viewmodel.DeveloperInfoViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import nl.marc_apps.tts.TextToSpeechInstance @Composable fun DeveloperInfoScreen( @@ -67,7 +68,7 @@ fun DeveloperInfoScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DeveloperInfoContent( - textToSpeech: () -> LCE, + textToSpeech: () -> LCE, onNavigateUp: () -> Unit ) { val activity = LocalActivity.current @@ -111,7 +112,12 @@ private fun DeveloperInfoContent( ) { coroutineScope.launch(Dispatchers.IO) { textToSpeech().content?.runCatching { - say("안아줘요", true) + speak( + "안아줘요", + TextToSpeech.QUEUE_FLUSH, + bundleOf(), + "hug_me" + ) } } }, diff --git a/app/src/main/kotlin/com/joeloewi/croissant/util/TextToSpeechFactory.kt b/app/src/main/kotlin/com/joeloewi/croissant/util/TextToSpeechFactory.kt new file mode 100644 index 00000000..5062ede4 --- /dev/null +++ b/app/src/main/kotlin/com/joeloewi/croissant/util/TextToSpeechFactory.kt @@ -0,0 +1,42 @@ +package com.joeloewi.croissant.util + +import android.content.Context +import android.speech.tts.TextToSpeech +import com.joeloewi.croissant.state.foldAsLce +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import java.util.Locale +import javax.inject.Inject + +class TextToSpeechFactory @Inject constructor( + @ApplicationContext private val context: Context +) { + val flow = callbackFlow { + var textToSpeech: TextToSpeech? = null + + textToSpeech = TextToSpeech(context) { status -> + trySend(runCatching { + if (status != TextToSpeech.SUCCESS) { + throw IllegalStateException() + } + textToSpeech!!.apply { + val currentLocale = Locale.getDefault() + val targetLocale = if (availableLanguages.contains(currentLocale)) { + currentLocale + } else { + Locale.ENGLISH + } + language = targetLocale + } + }.foldAsLce()) + } + + awaitClose { + with(textToSpeech) { + stop() + shutdown() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/joeloewi/croissant/viewmodel/DeveloperInfoViewModel.kt b/app/src/main/kotlin/com/joeloewi/croissant/viewmodel/DeveloperInfoViewModel.kt index 6908a93c..3c15ef66 100644 --- a/app/src/main/kotlin/com/joeloewi/croissant/viewmodel/DeveloperInfoViewModel.kt +++ b/app/src/main/kotlin/com/joeloewi/croissant/viewmodel/DeveloperInfoViewModel.kt @@ -3,37 +3,20 @@ package com.joeloewi.croissant.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.joeloewi.croissant.state.LCE +import com.joeloewi.croissant.util.TextToSpeechFactory import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import nl.marc_apps.tts.TextToSpeechFactory import javax.inject.Inject @HiltViewModel class DeveloperInfoViewModel @Inject constructor( - private val textToSpeechFactory: TextToSpeechFactory + textToSpeechFactory: TextToSpeechFactory ) : ViewModel() { - val textToSpeech = callbackFlow { - val textToSpeech = textToSpeechFactory.runCatching { - createOrThrow() - }.fold( - onSuccess = { - LCE.Content(it) - }, - onFailure = { - LCE.Error(it) - } - ) - - trySend(textToSpeech) - - awaitClose { textToSpeech.content?.close() } - }.catch { }.flowOn(Dispatchers.IO).stateIn( + val textToSpeech = textToSpeechFactory.flow.catch { }.flowOn(Dispatchers.IO).stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = LCE.Loading