From 8ef4a6699e89c81757dae63811a57a224fc044fc Mon Sep 17 00:00:00 2001 From: to268 <39095391+to268@users.noreply.github.com> Date: Wed, 25 Aug 2021 21:05:37 +0200 Subject: [PATCH 1/2] TTS feature --- .../QKSMS/feature/compose/ComposeActivity.kt | 60 ++++++++++++++++--- .../moez/QKSMS/feature/compose/ComposeView.kt | 1 + .../QKSMS/feature/compose/ComposeViewModel.kt | 58 ++++++++++-------- .../res/drawable/ic_speech_black_24dp.xml | 30 ++++++++++ presentation/src/main/res/menu/compose.xml | 9 ++- .../src/main/res/values-fr/strings.xml | 3 +- presentation/src/main/res/values/strings.xml | 5 +- 7 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 presentation/src/main/res/drawable/ic_speech_black_24dp.xml diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt index 3b42535b6..2bdb9b05d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2017,2021 Moez Bhatti ,to268 * * This file is part of QKSMS. * @@ -30,7 +30,9 @@ import android.os.Build import android.os.Bundle import android.provider.ContactsContract import android.provider.MediaStore +import android.speech.tts.TextToSpeech import android.text.format.DateFormat +import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog @@ -66,12 +68,16 @@ import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import kotlinx.android.synthetic.main.compose_activity.* +import kotlinx.android.synthetic.main.compose_activity.attachments +import kotlinx.android.synthetic.main.compose_activity.sim +import kotlinx.android.synthetic.main.compose_activity.simIndex +import kotlinx.android.synthetic.main.message_list_item_in.* import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject import kotlin.collections.HashMap -class ComposeActivity : QkThemedActivity(), ComposeView { +class ComposeActivity : QkThemedActivity(), ComposeView, TextToSpeech.OnInitListener { companion object { private const val SelectContactRequestCode = 0 @@ -82,12 +88,18 @@ class ComposeActivity : QkThemedActivity(), ComposeView { private const val CameraDestinationKey = "camera_destination" } - @Inject lateinit var attachmentAdapter: AttachmentAdapter - @Inject lateinit var chipsAdapter: ChipsAdapter - @Inject lateinit var dateFormatter: DateFormatter - @Inject lateinit var messageAdapter: MessagesAdapter - @Inject lateinit var navigator: Navigator - @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject + lateinit var attachmentAdapter: AttachmentAdapter + @Inject + lateinit var chipsAdapter: ChipsAdapter + @Inject + lateinit var dateFormatter: DateFormatter + @Inject + lateinit var messageAdapter: MessagesAdapter + @Inject + lateinit var navigator: Navigator + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() override val chipsSelectedIntent: Subject> = PublishSubject.create() @@ -119,6 +131,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] } private var cameraDestination: Uri? = null + private var tts: TextToSpeech? = null override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) @@ -160,6 +173,9 @@ class ComposeActivity : QkThemedActivity(), ComposeView { if (Build.VERSION.SDK_INT <= 22) { messageBackground.setBackgroundTint(resolveThemeColor(R.attr.bubbleColor)) } + + // Set tts info + tts = TextToSpeech(this, this) } override fun onStart() { @@ -206,6 +222,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && state.selectedMessages > 0 toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1 + toolbar.menu.findItem(R.id.speech)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.previous)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() toolbar.menu.findItem(R.id.next)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() toolbar.menu.findItem(R.id.clear)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() @@ -251,6 +268,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { .show() } + override fun speechText(text: String) { + tts!!.speak(text, TextToSpeech.QUEUE_FLUSH, null, "") + } + override fun requestDefaultSms() { navigator.showDefaultSmsDialog(this) } @@ -399,4 +420,27 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override fun onBackPressed() = backPressedIntent.onNext(Unit) + // Text to speech + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + val result = tts!!.setLanguage(Locale.getDefault()) + + if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { + Log.e("TTS", "The default language is not supported !") + } + + } else { + Log.e("TTS", "Initialization failed !") + } + } + + override fun onDestroy() { + if (tts != null) { + tts!!.stop() + tts!!.shutdown() + } + + super.onDestroy() + } + } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt index d8a78eec2..50fd3ea3b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt @@ -58,6 +58,7 @@ interface ComposeView : QkView { fun clearSelection() fun showDetails(details: String) + fun speechText(text: String) fun requestDefaultSms() fun requestStoragePermission() fun requestSmsPermission() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index ba601f2f7..9fac4b364 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2017,2021 Moez Bhatti ,to268 * * This file is part of QKSMS. * @@ -74,29 +74,29 @@ import javax.inject.Inject import javax.inject.Named class ComposeViewModel @Inject constructor( - @Named("query") private val query: String, - @Named("threadId") private val threadId: Long, - @Named("addresses") private val addresses: List, - @Named("text") private val sharedText: String, - @Named("attachments") private val sharedAttachments: Attachments, - private val contactRepo: ContactRepository, - private val context: Context, - private val activeConversationManager: ActiveConversationManager, - private val addScheduledMessage: AddScheduledMessage, - private val billingManager: BillingManager, - private val cancelMessage: CancelDelayedMessage, - private val conversationRepo: ConversationRepository, - private val deleteMessages: DeleteMessages, - private val markRead: MarkRead, - private val messageDetailsFormatter: MessageDetailsFormatter, - private val messageRepo: MessageRepository, - private val navigator: Navigator, - private val permissionManager: PermissionManager, - private val phoneNumberUtils: PhoneNumberUtils, - private val prefs: Preferences, - private val retrySending: RetrySending, - private val sendMessage: SendMessage, - private val subscriptionManager: SubscriptionManagerCompat + @Named("query") private val query: String, + @Named("threadId") private val threadId: Long, + @Named("addresses") private val addresses: List, + @Named("text") private val sharedText: String, + @Named("attachments") private val sharedAttachments: Attachments, + private val contactRepo: ContactRepository, + private val context: Context, + private val activeConversationManager: ActiveConversationManager, + private val addScheduledMessage: AddScheduledMessage, + private val billingManager: BillingManager, + private val cancelMessage: CancelDelayedMessage, + private val conversationRepo: ConversationRepository, + private val deleteMessages: DeleteMessages, + private val markRead: MarkRead, + private val messageDetailsFormatter: MessageDetailsFormatter, + private val messageRepo: MessageRepository, + private val navigator: Navigator, + private val permissionManager: PermissionManager, + private val phoneNumberUtils: PhoneNumberUtils, + private val prefs: Preferences, + private val retrySending: RetrySending, + private val sendMessage: SendMessage, + private val subscriptionManager: SubscriptionManagerCompat ) : QkViewModel(ComposeState( editingMode = threadId == 0L && addresses.isEmpty(), threadId = threadId, @@ -340,6 +340,16 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe { view.showDetails(it) } + // Speech text + view.optionsItemIntent + .filter { it == R.id.speech } + .withLatestFrom(view.messagesSelectedIntent) { _, messages -> messages } + .mapNotNull { messages -> messages.firstOrNull().also { view.clearSelection() } } + .mapNotNull(messageRepo::getMessage) + .mapNotNull(Message::getText) + .autoDisposable(view.scope()) + .subscribe { view.speechText(it) } + // Delete the messages view.optionsItemIntent .filter { it == R.id.delete } diff --git a/presentation/src/main/res/drawable/ic_speech_black_24dp.xml b/presentation/src/main/res/drawable/ic_speech_black_24dp.xml new file mode 100644 index 000000000..cba949793 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_speech_black_24dp.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/presentation/src/main/res/menu/compose.xml b/presentation/src/main/res/menu/compose.xml index c39f25ac2..e98db8d52 100644 --- a/presentation/src/main/res/menu/compose.xml +++ b/presentation/src/main/res/menu/compose.xml @@ -1,6 +1,6 @@ - + diff --git a/common/src/main/java/com/moez/QKSMS/common/util/extensions/ContextExtensions.kt b/common/src/main/java/com/moez/QKSMS/common/util/extensions/ContextExtensions.kt index f5534a9fc..256c7cdca 100644 --- a/common/src/main/java/com/moez/QKSMS/common/util/extensions/ContextExtensions.kt +++ b/common/src/main/java/com/moez/QKSMS/common/util/extensions/ContextExtensions.kt @@ -20,6 +20,7 @@ package com.moez.QKSMS.common.util.extensions import android.app.job.JobScheduler import android.content.Context +import android.content.pm.PackageInfo import android.content.res.ColorStateList import android.graphics.Color import android.util.TypedValue @@ -27,6 +28,7 @@ import android.widget.Toast import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import androidx.core.content.pm.PackageInfoCompat import com.moez.QKSMS.util.tryOrNull fun Context.getColorCompat(colorRes: Int): Int { @@ -85,7 +87,7 @@ fun Context.isInstalled(packageName: String): Boolean { } val Context.versionCode: Int - get() = packageManager.getPackageInfo(packageName, 0).versionCode + get() = PackageInfoCompat.getLongVersionCode(PackageInfo()).toInt() val Context.jobScheduler: JobScheduler get() = getSystemService()!! diff --git a/data/build.gradle b/data/build.gradle index a76aff2b4..74ac944cb 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -42,6 +42,7 @@ android { withAnalytics { dimension "analytics" } noAnalytics { dimension "analytics" } } + namespace 'com.moez.QKSMS.data' } dependencies { diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml index 3c6184a70..21edd1fd3 100644 --- a/data/src/main/AndroidManifest.xml +++ b/data/src/main/AndroidManifest.xml @@ -16,4 +16,4 @@ ~ You should have received a copy of the GNU General Public License ~ along with QKSMS. If not, see . --> - + diff --git a/domain/build.gradle b/domain/build.gradle index 85a69fd8d..6cb92629f 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -34,6 +34,7 @@ android { minSdkVersion 21 targetSdkVersion 29 } + namespace 'com.moez.QKSMS.domain' } dependencies { diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml index 00485b51a..21edd1fd3 100644 --- a/domain/src/main/AndroidManifest.xml +++ b/domain/src/main/AndroidManifest.xml @@ -16,4 +16,4 @@ ~ You should have received a copy of the GNU General Public License ~ along with QKSMS. If not, see . --> - + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4b9cec46b..0299a449a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/presentation/build.gradle b/presentation/build.gradle index 7dee8d3c8..10734f0b9 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -24,7 +24,6 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 - buildToolsVersion "29.0.3" flavorDimensions "analytics" defaultConfig { @@ -61,14 +60,15 @@ android { jvmTarget = "1.8" } - lintOptions { - abortOnError false - } productFlavors { withAnalytics { dimension "analytics" } noAnalytics { dimension "analytics" } } + namespace 'com.moez.QKSMS' + lint { + abortOnError false + } if (System.getenv("CI") == "true") { signingConfigs.release.storeFile = file("../keystore") diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 796dc6a33..351f1de03 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -17,8 +17,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with QKSMS. If not, see . --> - + diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt index 0c104b81c..36d091956 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt @@ -20,6 +20,7 @@ package com.moez.QKSMS.common.widget import android.content.Context import android.util.AttributeSet +import kotlin.math.ceil class TightTextView @JvmOverloads constructor( context: Context, @@ -35,11 +36,9 @@ class TightTextView @JvmOverloads constructor( return } - val maxLineWidth = (0 until layout.lineCount) - .map(layout::getLineWidth) - .max() ?: 0f + val maxLineWidth = (0 until layout.lineCount).maxOfOrNull(layout::getLineWidth) ?: 0f - val width = Math.ceil(maxLineWidth.toDouble()).toInt() + compoundPaddingLeft + compoundPaddingRight + val width = ceil(maxLineWidth.toDouble()).toInt() + compoundPaddingLeft + compoundPaddingRight if (width < measuredWidth) { val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) super.onMeasure(widthSpec, heightMeasureSpec) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt index b547ec306..0a89c0380 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt @@ -64,7 +64,7 @@ class BackupPresenter @Inject constructor( .distinctUntilChanged() .switchMap { backupRepo.getBackups() } .doOnNext { backups -> newState { copy(backups = backups) } } - .map { backups -> backups.map { it.date }.max() ?: 0L } + .map { backups -> backups.maxOfOrNull { it.date } ?: 0L } .map { lastBackup -> when (lastBackup) { 0L -> context.getString(R.string.backup_never) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt index c954a5592..656ea55b1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt @@ -29,7 +29,7 @@ import javax.inject.Inject class UpgradeButtonExperiment @Inject constructor( context: Context, analytics: AnalyticsManager -) : Experiment<@StringRes Int>(context, analytics) { +) : Experiment<@receiver:StringRes Int>(context, analytics) { override val key: String = "Upgrade Button"