From fb9df61bc35c014c692243dc92a053ad355fc518 Mon Sep 17 00:00:00 2001 From: touge Date: Tue, 16 May 2023 17:01:00 +0800 Subject: [PATCH 1/6] Support translation via mlkit --- app/build.gradle.kts | 1 + .../ui/conversation/ConversationFragment.kt | 53 +++++++++++++++++-- .../ui/conversation/adapter/MessageAdapter.kt | 13 +++++ .../ui/conversation/holder/TextHolder.kt | 41 ++++++++++++++ .../android/widget/MaxWidthRelativeLayout.kt | 42 +++++++++++++++ .../java/one/mixin/android/widget/ToolView.kt | 5 ++ .../res/drawable/ic_baseline_translate_24.xml | 5 ++ app/src/main/res/layout/item_chat_text.xml | 45 ++++++++++++++++ app/src/main/res/layout/view_tool.xml | 12 +++++ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/attrs.xml | 3 ++ app/src/main/res/values/strings.xml | 2 + 12 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt create mode 100644 app/src/main/res/drawable/ic_baseline_translate_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b6c42e85a..e14f6f5995 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -579,6 +579,7 @@ dependencies { // ML Kit implementation("com.google.mlkit:entity-extraction:$entityExtractionVersion") + implementation("com.google.mlkit:translate:17.0.1") testImplementation("com.google.protobuf:protobuf-javalite") { version { diff --git a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt index ab840c0a4a..bdf735975f 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt @@ -61,6 +61,11 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar +import com.google.mlkit.common.model.DownloadConditions +import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.Translation +import com.google.mlkit.nl.translate.Translator +import com.google.mlkit.nl.translate.TranslatorOptions import com.twilio.audioswitch.AudioSwitch import com.uber.autodispose.autoDispose import dagger.hilt.android.AndroidEntryPoint @@ -471,26 +476,30 @@ class ConversationFragment() : when { messageAdapter.selectSet.isEmpty() -> binding.toolView.fadeOut() messageAdapter.selectSet.size == 1 -> { + val firstItem = messageAdapter.selectSet.valueAt(0) try { - if (messageAdapter.selectSet.valueAt(0).isText()) { + if (firstItem.isText()) { binding.toolView.copyIv.visibility = VISIBLE + binding.toolView.translateIv.isVisible = messageAdapter.notTranslated(firstItem.messageId) } else { binding.toolView.copyIv.visibility = GONE + binding.toolView.translateIv.visibility = GONE } } catch (e: ArrayIndexOutOfBoundsException) { binding.toolView.copyIv.visibility = GONE + binding.toolView.translateIv.visibility = GONE } - if (messageAdapter.selectSet.valueAt(0).isData()) { + if (firstItem.isData()) { binding.toolView.shareIv.visibility = VISIBLE } else { binding.toolView.shareIv.visibility = GONE } - if (messageAdapter.selectSet.valueAt(0).supportSticker()) { + if (firstItem.supportSticker()) { binding.toolView.addStickerIv.visibility = VISIBLE } else { binding.toolView.addStickerIv.visibility = GONE } - if (messageAdapter.selectSet.valueAt(0).canNotReply()) { + if (firstItem.canNotReply()) { binding.toolView.replyIv.visibility = GONE } else { binding.toolView.replyIv.visibility = VISIBLE @@ -501,6 +510,7 @@ class ConversationFragment() : binding.toolView.forwardIv.visibility = VISIBLE binding.toolView.replyIv.visibility = GONE binding.toolView.copyIv.visibility = GONE + binding.toolView.translateIv.visibility = GONE binding.toolView.addStickerIv.visibility = GONE binding.toolView.shareIv.visibility = GONE binding.toolView.pinIv.visibility = GONE @@ -524,8 +534,10 @@ class ConversationFragment() : if (b) { if (messageItem.isText()) { binding.toolView.copyIv.visibility = VISIBLE + binding.toolView.translateIv.isVisible = messageAdapter.notTranslated(messageItem.messageId) } else { binding.toolView.copyIv.visibility = GONE + binding.toolView.translateIv.visibility = GONE } if (messageItem.isData()) { binding.toolView.shareIv.visibility = VISIBLE @@ -1633,6 +1645,11 @@ class ConversationFragment() : } closeTool() } + binding.toolView.translateIv.setOnClickListener { + if (messageAdapter.selectSet.isEmpty()) return@setOnClickListener + translate(messageAdapter.selectSet.valueAt(0)) + closeTool() + } binding.toolView.forwardIv.setOnClickListener { showForwardDialog() } @@ -1859,6 +1876,34 @@ class ConversationFragment() : } } + private var translator: Translator? = null + + private fun translate(messageItem: MessageItem) { + val content = messageItem.content ?: return + if (translator == null) { + val options = + TranslatorOptions.Builder() + .setSourceLanguage(TranslateLanguage.ENGLISH) + .setTargetLanguage(TranslateLanguage.CHINESE) + .build() + translator = Translation.getClient(options) + translator?.let { lifecycle.addObserver(it) } + } + val conditions = DownloadConditions.Builder().requireWifi().build() + translator?.downloadModelIfNeeded(conditions) + ?.addOnSuccessListener { + translator?.translate(content) + ?.addOnSuccessListener { translated -> + messageAdapter.updateTranslated(messageItem.messageId, translated) + }?.addOnFailureListener { e -> + Timber.w(e) + } + } + ?.addOnFailureListener { e -> + Timber.w(e) + } + } + private var deleteDialog: AlertDialog? = null private fun deleteMessage(messages: List) { diff --git a/app/src/main/java/one/mixin/android/ui/conversation/adapter/MessageAdapter.kt b/app/src/main/java/one/mixin/android/ui/conversation/adapter/MessageAdapter.kt index ff769ad47d..56a47ac4e0 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/adapter/MessageAdapter.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/adapter/MessageAdapter.kt @@ -126,6 +126,18 @@ class MessageAdapter( private val isSecret: Boolean = true, var keyword: String? = null, ) : RecyclerView.Adapter(), MixinStickyRecyclerHeadersAdapter { + private val translatedMap = mutableMapOf() + + fun updateTranslated( + messageId: String, + translated: String, + ) { + translatedMap[messageId] = translated + notifyDataSetChanged() + } + + fun notTranslated(messageId: String): Boolean = translatedMap[messageId] == null + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -282,6 +294,7 @@ class MessageAdapter( (holder as TextHolder).bind( it, keyword, + translatedMap[it.messageId], isLast(position), isFirst(position), selectSet.size > 0, diff --git a/app/src/main/java/one/mixin/android/ui/conversation/holder/TextHolder.kt b/app/src/main/java/one/mixin/android/ui/conversation/holder/TextHolder.kt index 4f9b186a26..02f6676dcc 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/holder/TextHolder.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/holder/TextHolder.kt @@ -1,11 +1,13 @@ package one.mixin.android.ui.conversation.holder +import android.content.ClipData import android.graphics.Color import android.util.TypedValue import android.view.GestureDetector import android.view.MotionEvent import android.view.View import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible import one.mixin.android.Constants import one.mixin.android.Constants.Colors.LINK_COLOR import one.mixin.android.Constants.Colors.SELECT_COLOR @@ -15,6 +17,7 @@ import one.mixin.android.databinding.ItemChatTextBinding import one.mixin.android.event.MentionReadEvent import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.doubleClickVibrate +import one.mixin.android.extension.getClipboardManager import one.mixin.android.extension.initChatMode import one.mixin.android.extension.maxItemWidth import one.mixin.android.extension.renderMessage @@ -34,11 +37,13 @@ class TextHolder constructor(val binding: ItemChatTextBinding) : BaseMentionHold binding.chatTime.changeSize(textSize - 4f) binding.chatName.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.chatTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + binding.translateTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) } } binding.chatTv.initChatMode(LINK_COLOR) binding.chatTv.setSelectedStateColor(SELECT_COLOR) binding.chatLayout.setMaxWidth(itemView.context.maxItemWidth()) + binding.translateRl.setMaxWidth(itemView.context.maxItemWidth()) } override fun chatLayout( @@ -81,11 +86,31 @@ class TextHolder constructor(val binding: ItemChatTextBinding) : BaseMentionHold } } + private fun translatedLayout(isMe: Boolean) { + val lp = binding.translateRl.layoutParams as ConstraintLayout.LayoutParams + if (isMe) { + lp.horizontalBias = 1f + setItemBackgroundResource( + binding.translateRl, + R.drawable.chat_bubble_me, + R.drawable.chat_bubble_me_night, + ) + } else { + lp.horizontalBias = 0f + setItemBackgroundResource( + binding.translateRl, + R.drawable.chat_bubble_other, + R.drawable.chat_bubble_other_night, + ) + } + } + private var onItemListener: MessageAdapter.OnItemListener? = null fun bind( messageItem: MessageItem, keyword: String?, + translated: String? = null, isLast: Boolean, isFirst: Boolean = false, hasSelect: Boolean, @@ -141,6 +166,13 @@ class TextHolder constructor(val binding: ItemChatTextBinding) : BaseMentionHold } } + binding.translateTv.setOnLongClickListener { + binding.root.context.getClipboardManager().setPrimaryClip( + ClipData.newPlainText(null, binding.translateTv.text), + ) + true + } + itemView.setOnClickListener { if (hasSelect) { onItemListener.onSelect(!isSelect, messageItem, absoluteAdapterPosition) @@ -179,6 +211,14 @@ class TextHolder constructor(val binding: ItemChatTextBinding) : BaseMentionHold binding.chatTv.renderMessage(messageItem.content, keyword) } + if (translated.isNullOrBlank()) { + binding.translateTv.text = null + binding.translateRl.isVisible = false + } else { + binding.translateTv.text = translated + binding.translateRl.isVisible = true + } + val isMe = meId == messageItem.userId if (isFirst && !isMe) { binding.chatName.visibility = View.VISIBLE @@ -201,6 +241,7 @@ class TextHolder constructor(val binding: ItemChatTextBinding) : BaseMentionHold chatJumpLayout(binding.chatJump, isMe, messageItem.expireIn, messageItem.expireAt, R.id.chat_layout) chatLayout(isMe, isLast) + translatedLayout(isMe) attachAction = if (messageItem.mentionRead == false) { diff --git a/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt b/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt new file mode 100644 index 0000000000..aa1b6ff040 --- /dev/null +++ b/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt @@ -0,0 +1,42 @@ +package one.mixin.android.widget + +import android.content.Context +import android.util.AttributeSet +import android.widget.RelativeLayout +import one.mixin.android.R +import one.mixin.android.extension.dp + +class MaxWidthRelativeLayout : RelativeLayout { + private var maxWidth: Int = 0 + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr, + ) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.MaxWidthRelativeLayout, defStyleAttr, 0) + maxWidth = ta.getDimensionPixelSize(R.styleable.MaxWidthRelativeLayout_mr_max_width, 300.dp) + ta.recycle() + } + + fun setMaxWidth(maxWidth: Int) { + if (this.maxWidth != maxWidth) { + this.maxWidth = maxWidth + requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + var wms = widthMeasureSpec + if (maxWidth in 1 until measuredWidth) { + val measureMode = MeasureSpec.getMode(widthMeasureSpec) + wms = MeasureSpec.makeMeasureSpec(maxWidth, measureMode) + } + super.onMeasure(wms, heightMeasureSpec) + } +} diff --git a/app/src/main/java/one/mixin/android/widget/ToolView.kt b/app/src/main/java/one/mixin/android/widget/ToolView.kt index 24db045f2d..e2ed78c66a 100644 --- a/app/src/main/java/one/mixin/android/widget/ToolView.kt +++ b/app/src/main/java/one/mixin/android/widget/ToolView.kt @@ -30,6 +30,7 @@ class ToolView constructor(context: Context, attrs: AttributeSet) : RelativeLayo val forwardIv = binding.forwardIv val shareIv = binding.shareIv val pinIv = binding.pinIv + val translateIv = binding.translateIv init { closeIv.setOnLongClickListener { @@ -71,6 +72,10 @@ class ToolView constructor(context: Context, attrs: AttributeSet) : RelativeLayo ) true } + translateIv.setOnLongClickListener { + showTip(it, R.string.Translate) + true + } } private val tipView by lazy { diff --git a/app/src/main/res/drawable/ic_baseline_translate_24.xml b/app/src/main/res/drawable/ic_baseline_translate_24.xml new file mode 100644 index 0000000000..9cc75def24 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_translate_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/item_chat_text.xml b/app/src/main/res/layout/item_chat_text.xml index 3480b6cef7..aa93385ed7 100644 --- a/app/src/main/res/layout/item_chat_text.xml +++ b/app/src/main/res/layout/item_chat_text.xml @@ -79,6 +79,51 @@ + + + + + + + + diff --git a/app/src/main/res/layout/view_tool.xml b/app/src/main/res/layout/view_tool.xml index 0e97cf62a5..24ae6dfed9 100644 --- a/app/src/main/res/layout/view_tool.xml +++ b/app/src/main/res/layout/view_tool.xml @@ -113,4 +113,16 @@ android:src="@drawable/ic_share" android:layout_marginEnd="8dp" tools:ignore="ContentDescription" /> + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2af59c3fe5..3ed1494ac7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2596,4 +2596,6 @@ 不必输入复杂地址,彻底告别转账错误 拦截恶意授权,杜绝应用偷转资产 没有代币 + 翻译 + 由 Google 翻译 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index df253a9ada..9792579d73 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -83,6 +83,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfd44e3754..17b18b2b84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2673,4 +2673,6 @@ Avoid transfer errors caused by complex wallet addresses. Block malicious apps from gaining unsafe permissions. No Tokens + Translate + translate by Google From 9447241cb0ee4a88a1410e6d76b88cd8a91b9669 Mon Sep 17 00:00:00 2001 From: touge Date: Wed, 17 May 2023 13:18:43 +0800 Subject: [PATCH 2/6] Add translate setting --- .../main/java/one/mixin/android/Constants.kt | 1 + .../ui/conversation/ConversationFragment.kt | 8 +++- .../android/ui/setting/AppearanceFragment.kt | 6 +++ .../ui/setting/ui/page/AppearancePage.kt | 38 +++++++++++++++++++ .../main/res/layout/fragment_appearance.xml | 21 +++++++++- app/src/main/res/layout/item_chat_text.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- 8 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/one/mixin/android/Constants.kt b/app/src/main/java/one/mixin/android/Constants.kt index 9f49328bb1..19b8e95ec4 100644 --- a/app/src/main/java/one/mixin/android/Constants.kt +++ b/app/src/main/java/one/mixin/android/Constants.kt @@ -94,6 +94,7 @@ object Constants { const val PREF_CLEANUP_THUMB = "pref_cleanup_thumb" const val PREF_CLEANUP_QUOTE_CONTENT = "pref_cleanup_quote_content" const val PREF_TRANSFER_SCENE = "pref_transfer_scene" + const val PREF_SHOW_TRANSLATE_BUTTON = "pref_show_translate_button" const val PREF_LOGIN_VERIFY = "pref_login_verify" const val PREF_LOGIN_OR_SIGN_UP = "pref_login_or_sign_up" const val PREF_NOTIFY_ENABLE_BIOMETRIC = "pref_notify_enable_biometric" diff --git a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt index bdf735975f..431de0686a 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt @@ -480,7 +480,7 @@ class ConversationFragment() : try { if (firstItem.isText()) { binding.toolView.copyIv.visibility = VISIBLE - binding.toolView.translateIv.isVisible = messageAdapter.notTranslated(firstItem.messageId) + binding.toolView.translateIv.isVisible = shouldShowTranslate(firstItem.messageId) } else { binding.toolView.copyIv.visibility = GONE binding.toolView.translateIv.visibility = GONE @@ -534,7 +534,7 @@ class ConversationFragment() : if (b) { if (messageItem.isText()) { binding.toolView.copyIv.visibility = VISIBLE - binding.toolView.translateIv.isVisible = messageAdapter.notTranslated(messageItem.messageId) + binding.toolView.translateIv.isVisible = shouldShowTranslate(messageItem.messageId) } else { binding.toolView.copyIv.visibility = GONE binding.toolView.translateIv.visibility = GONE @@ -1904,6 +1904,10 @@ class ConversationFragment() : } } + private fun shouldShowTranslate(messageId: String): Boolean = + defaultSharedPreferences.getBoolean(Constants.Account.PREF_SHOW_TRANSLATE_BUTTON, false) && + messageAdapter.notTranslated(messageId) + private var deleteDialog: AlertDialog? = null private fun deleteMessage(messages: List) { diff --git a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt index 21c3e751d0..8c9eeea17c 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt @@ -13,6 +13,7 @@ import androidx.core.os.LocaleListCompat import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import one.mixin.android.Constants +import one.mixin.android.Constants.Account.PREF_SHOW_TRANSLATE_BUTTON import one.mixin.android.R import one.mixin.android.RxBus import one.mixin.android.databinding.FragmentAppearanceBinding @@ -130,6 +131,11 @@ class AppearanceFragment : BaseFragment(R.layout.fragment_appearance) { textSizeRl.setOnClickListener { navTo(SettingSizeFragment.newInstance(), SettingSizeFragment.TAG) } + translateSv.setContent(R.string.Show_Translate_Button) + translateSv.isChecked = defaultSharedPreferences.getBoolean(PREF_SHOW_TRANSLATE_BUTTON, false) + translateSv.setOnCheckedChangeListener { _, isChecked -> + defaultSharedPreferences.putBoolean(PREF_SHOW_TRANSLATE_BUTTON, isChecked) + } val quoteColor = requireContext().defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false) quoteColorDescTv.setText( if (quoteColor) { diff --git a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt index 1027d123c3..d86d05d9b9 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt @@ -17,10 +17,14 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.material.RadioButton import androidx.compose.material.RadioButtonDefaults import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -37,6 +41,8 @@ import one.mixin.android.R import one.mixin.android.compose.MixinAlertDialog import one.mixin.android.compose.MixinBackButton import one.mixin.android.compose.MixinTopAppBar +import one.mixin.android.compose.SettingTile +import one.mixin.android.compose.booleanValueAsState import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.findFragmentActivityOrNull @@ -101,6 +107,10 @@ fun AppearancePage() { color = MixinAppTheme.colors.textPrimary, ) } + + Box(modifier = Modifier.height(20.dp)) + + TranslateItem() } } } @@ -379,6 +389,34 @@ private fun CurrencyItem() { } } +@Composable +private fun TranslateItem() { + var showTranslateButton by LocalContext.current.defaultSharedPreferences + .booleanValueAsState( + key = Constants.Account.PREF_SHOW_TRANSLATE_BUTTON, + defaultValue = false, + ) + SettingTile( + title = stringResource(R.string.Show_Translate_Button), + description = stringResource(R.string.show_translate_button_hint), + trailing = { + Switch( + checked = showTranslateButton, + colors = + SwitchDefaults.colors( + checkedThumbColor = MixinAppTheme.colors.accent, + uncheckedThumbColor = MixinAppTheme.colors.unchecked, + checkedTrackColor = MixinAppTheme.colors.accent, + uncheckedTrackColor = MixinAppTheme.colors.unchecked, + ), + onCheckedChange = null, + ) + }, + ) { + showTranslateButton = !showTranslateButton + } +} + @Composable @Preview fun AppearanceItemPreview() { diff --git a/app/src/main/res/layout/fragment_appearance.xml b/app/src/main/res/layout/fragment_appearance.xml index 1a8a27475c..0808f7e4e6 100644 --- a/app/src/main/res/layout/fragment_appearance.xml +++ b/app/src/main/res/layout/fragment_appearance.xml @@ -172,5 +172,24 @@ tools:ignore="RelativeOverlap" /> + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_chat_text.xml b/app/src/main/res/layout/item_chat_text.xml index aa93385ed7..c4c4668827 100644 --- a/app/src/main/res/layout/item_chat_text.xml +++ b/app/src/main/res/layout/item_chat_text.xml @@ -122,7 +122,7 @@ android:layout_toEndOf="@id/done_iv" android:textSize="12sp" android:textColor="?text_minor" - android:text="@string/translate_by_google"/> + android:text="@string/translation_done"/> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3ed1494ac7..1545ede0fb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2597,5 +2597,7 @@ 拦截恶意授权,杜绝应用偷转资产 没有代币 翻译 - 由 Google 翻译 + 翻译完成 + 显示翻译按钮 + \'翻译\'会在你长按一条文字消息时出现。\n\nGoogle 可能会获取到这条你要翻译的文字消息的内容。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17b18b2b84..8c39fbd9cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2674,5 +2674,7 @@ Block malicious apps from gaining unsafe permissions. No Tokens Translate - translate by Google + translation done + Show Translate Button + The \'Translate\' button will appear when you make a long press on a text message.\n\nGoogle may have access to the text of messages you translate. From d7598c3127ba4bffb08afebd987c5e5ba3d324fa Mon Sep 17 00:00:00 2001 From: touge Date: Wed, 17 May 2023 15:35:56 +0800 Subject: [PATCH 3/6] Refine translate process --- app/build.gradle.kts | 1 + .../main/java/one/mixin/android/Constants.kt | 1 + .../ui/conversation/ConversationFragment.kt | 50 +++--- .../android/ui/setting/AppearanceFragment.kt | 11 ++ .../one/mixin/android/util/LanguageUtil.kt | 2 + .../mixin/android/util/TranslateManager.kt | 142 ++++++++++++++++++ .../main/res/layout/fragment_appearance.xml | 30 +++- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 205 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/util/TranslateManager.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e14f6f5995..09ee947209 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -580,6 +580,7 @@ dependencies { // ML Kit implementation("com.google.mlkit:entity-extraction:$entityExtractionVersion") implementation("com.google.mlkit:translate:17.0.1") + implementation("com.google.mlkit:language-id:17.0.4") testImplementation("com.google.protobuf:protobuf-javalite") { version { diff --git a/app/src/main/java/one/mixin/android/Constants.kt b/app/src/main/java/one/mixin/android/Constants.kt index 19b8e95ec4..5aba393cce 100644 --- a/app/src/main/java/one/mixin/android/Constants.kt +++ b/app/src/main/java/one/mixin/android/Constants.kt @@ -95,6 +95,7 @@ object Constants { const val PREF_CLEANUP_QUOTE_CONTENT = "pref_cleanup_quote_content" const val PREF_TRANSFER_SCENE = "pref_transfer_scene" const val PREF_SHOW_TRANSLATE_BUTTON = "pref_show_translate_button" + const val PREF_TRANSLATE_TARGET_LANG = "pref_translate_target_lang" const val PREF_LOGIN_VERIFY = "pref_login_verify" const val PREF_LOGIN_OR_SIGN_UP = "pref_login_or_sign_up" const val PREF_NOTIFY_ENABLE_BIOMETRIC = "pref_notify_enable_biometric" diff --git a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt index 431de0686a..fd06c98015 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt @@ -61,11 +61,6 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar -import com.google.mlkit.common.model.DownloadConditions -import com.google.mlkit.nl.translate.TranslateLanguage -import com.google.mlkit.nl.translate.Translation -import com.google.mlkit.nl.translate.Translator -import com.google.mlkit.nl.translate.TranslatorOptions import com.twilio.audioswitch.AudioSwitch import com.uber.autodispose.autoDispose import dagger.hilt.android.AndroidEntryPoint @@ -221,6 +216,7 @@ import one.mixin.android.util.ErrorHandler.Companion.FORBIDDEN import one.mixin.android.util.GsonHelper import one.mixin.android.util.MusicPlayer import one.mixin.android.util.SINGLE_DB_THREAD +import one.mixin.android.util.TranslateManager import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.util.debug.debugLongClick import one.mixin.android.util.markdown.MarkwonUtil.Companion.getMiniMarkwon @@ -1365,6 +1361,7 @@ class ConversationFragment() : AudioPlayer.pause() AudioPlayer.setStatusListener(null) AudioPlayer.release() + translateManager?.release() context?.let { if (!anyCallServiceRunning(it)) { audioSwitch.safeStop() @@ -1647,7 +1644,20 @@ class ConversationFragment() : } binding.toolView.translateIv.setOnClickListener { if (messageAdapter.selectSet.isEmpty()) return@setOnClickListener - translate(messageAdapter.selectSet.valueAt(0)) + val messageItem = messageAdapter.selectSet.valueAt(0) + val content = messageItem.content ?: return@setOnClickListener + if (translateManager == null) { + translateManager = TranslateManager() + } + lifecycleScope.launch { + val translated = + withContext(Dispatchers.IO) { + translateManager?.translate(requireContext(), content) + } + if (translated != null) { + messageAdapter.updateTranslated(messageItem.messageId, translated) + } + } closeTool() } binding.toolView.forwardIv.setOnClickListener { @@ -1876,33 +1886,7 @@ class ConversationFragment() : } } - private var translator: Translator? = null - - private fun translate(messageItem: MessageItem) { - val content = messageItem.content ?: return - if (translator == null) { - val options = - TranslatorOptions.Builder() - .setSourceLanguage(TranslateLanguage.ENGLISH) - .setTargetLanguage(TranslateLanguage.CHINESE) - .build() - translator = Translation.getClient(options) - translator?.let { lifecycle.addObserver(it) } - } - val conditions = DownloadConditions.Builder().requireWifi().build() - translator?.downloadModelIfNeeded(conditions) - ?.addOnSuccessListener { - translator?.translate(content) - ?.addOnSuccessListener { translated -> - messageAdapter.updateTranslated(messageItem.messageId, translated) - }?.addOnFailureListener { e -> - Timber.w(e) - } - } - ?.addOnFailureListener { e -> - Timber.w(e) - } - } + private var translateManager: TranslateManager? = null private fun shouldShowTranslate(messageId: String): Boolean = defaultSharedPreferences.getBoolean(Constants.Account.PREF_SHOW_TRANSLATE_BUTTON, false) && diff --git a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt index 8c9eeea17c..0f8e5ad1f4 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt @@ -10,10 +10,12 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.ListPopupWindow import androidx.core.content.ContextCompat import androidx.core.os.LocaleListCompat +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import one.mixin.android.Constants import one.mixin.android.Constants.Account.PREF_SHOW_TRANSLATE_BUTTON +import one.mixin.android.Constants.Account.PREF_TRANSLATE_TARGET_LANG import one.mixin.android.R import one.mixin.android.RxBus import one.mixin.android.databinding.FragmentAppearanceBinding @@ -29,6 +31,7 @@ import one.mixin.android.session.Session import one.mixin.android.ui.common.BaseFragment import one.mixin.android.util.TimeCache import one.mixin.android.util.getLanguage +import one.mixin.android.util.getLanguageOrDefault import one.mixin.android.util.getLocaleString import one.mixin.android.util.isFollowSystem import one.mixin.android.util.viewBinding @@ -135,7 +138,15 @@ class AppearanceFragment : BaseFragment(R.layout.fragment_appearance) { translateSv.isChecked = defaultSharedPreferences.getBoolean(PREF_SHOW_TRANSLATE_BUTTON, false) translateSv.setOnCheckedChangeListener { _, isChecked -> defaultSharedPreferences.putBoolean(PREF_SHOW_TRANSLATE_BUTTON, isChecked) + targetRl.isVisible = isChecked } + targetLangTv.text = + if (isFollowSystem()) { + getString(R.string.Follow_system) + } else { + defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault()) + } + targetLangTv.setOnClickListener { } val quoteColor = requireContext().defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false) quoteColorDescTv.setText( if (quoteColor) { diff --git a/app/src/main/java/one/mixin/android/util/LanguageUtil.kt b/app/src/main/java/one/mixin/android/util/LanguageUtil.kt index 409a9fe7e9..789e7d411e 100644 --- a/app/src/main/java/one/mixin/android/util/LanguageUtil.kt +++ b/app/src/main/java/one/mixin/android/util/LanguageUtil.kt @@ -10,6 +10,8 @@ import java.util.Locale fun getLanguage(): String? = AppCompatDelegate.getApplicationLocales().get(0)?.language +fun getLanguageOrDefault(): String = AppCompatDelegate.getApplicationLocales().get(0)?.language ?: Locale.getDefault().language + fun getCountry(): String? = AppCompatDelegate.getApplicationLocales().get(0)?.country fun getLocaleString(): String = AppCompatDelegate.getApplicationLocales().get(0).toString() diff --git a/app/src/main/java/one/mixin/android/util/TranslateManager.kt b/app/src/main/java/one/mixin/android/util/TranslateManager.kt new file mode 100644 index 0000000000..64a02756da --- /dev/null +++ b/app/src/main/java/one/mixin/android/util/TranslateManager.kt @@ -0,0 +1,142 @@ +package one.mixin.android.util + +import android.content.Context +import android.util.LruCache +import com.google.mlkit.common.model.DownloadConditions +import com.google.mlkit.nl.languageid.LanguageIdentification +import com.google.mlkit.nl.languageid.LanguageIdentificationOptions +import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.Translation +import com.google.mlkit.nl.translate.Translator +import com.google.mlkit.nl.translate.TranslatorOptions +import one.mixin.android.Constants.Account.PREF_TRANSLATE_TARGET_LANG +import one.mixin.android.extension.defaultSharedPreferences +import timber.log.Timber +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class TranslateManager { + private val executor = Executors.newCachedThreadPool() + private val languageIdentifier = LanguageIdentification.getClient( + LanguageIdentificationOptions.Builder() + .setExecutor(executor) + .setConfidenceThreshold(0.5f) + .build(), + ) + private val translators = + object : LruCache(3) { + override fun create(options: TranslatorOptions): Translator { + return Translation.getClient(options) + } + + override fun entryRemoved( + evicted: Boolean, + key: TranslatorOptions, + oldValue: Translator, + newValue: Translator?, + ) { + oldValue.close() + } + } + + val availableLanguages: List = TranslateLanguage.getAllLanguages() + .map { Language(it) } + + fun translate( + context: Context, + text: String, + targetLang: String? = context.defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault()), + ): String? { + if (targetLang.isNullOrBlank()) { + Timber.d("$TAG targetLang is $targetLang") + return null + } + var latch = CountDownLatch(1) + val identifierTask = languageIdentifier.identifyLanguage(text).addOnCompleteListener { + latch.countDown() + } + val identifierDone = latch.await(5, TimeUnit.SECONDS) + if (!identifierDone) { + Timber.d("$TAG languageIdentifier timeout ${identifierTask.exception?.stackTraceToString()}") + return null + } + val sourceLangCode = TranslateLanguage.fromLanguageTag(identifierTask.result) + val targetLangCode = TranslateLanguage.fromLanguageTag(targetLang) + if (sourceLangCode == null || targetLangCode == null) { + Timber.d("$TAG sourceLangCode: $sourceLangCode, targetLangCode: $targetLangCode") + return null + } + + val options = TranslatorOptions.Builder() + .setSourceLanguage(sourceLangCode) + .setTargetLanguage(targetLangCode) + .setExecutor(executor) + .build() + val translator = translators[options] + + val conditions = DownloadConditions.Builder() + .requireWifi() + .build() + latch = CountDownLatch(1) + val downloadTask = translator.downloadModelIfNeeded(conditions).addOnCompleteListener { + latch.countDown() + } + val downloaded = latch.await(5, TimeUnit.SECONDS) + if (!downloaded || !downloadTask.isSuccessful) { + Timber.d("$TAG download model timeout or failed") + return null + } + + latch = CountDownLatch(1) + val task = translator.translate(text).addOnCompleteListener { + latch.countDown() + } + val translated = latch.await(5, TimeUnit.SECONDS) + if (!translated || !task.isSuccessful) { + Timber.d("$TAG translate timeout or failed") + return null + } + return task.result + } + + fun release() { + languageIdentifier.close() + translators.evictAll() + } + + class Language(val code: String) : Comparable { + + val displayName: String + get() = Locale(code).displayName + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + + if (other !is Language) { + return false + } + + val otherLang = other as Language? + return otherLang!!.code == code + } + + override fun toString(): String { + return displayName + } + + override fun compareTo(other: Language): Int { + return this.displayName.compareTo(other.displayName) + } + + override fun hashCode(): Int { + return code.hashCode() + } + } + companion object { + const val TAG = "TranslateManager" + } +} diff --git a/app/src/main/res/layout/fragment_appearance.xml b/app/src/main/res/layout/fragment_appearance.xml index 0808f7e4e6..1dee4b514d 100644 --- a/app/src/main/res/layout/fragment_appearance.xml +++ b/app/src/main/res/layout/fragment_appearance.xml @@ -173,11 +173,39 @@ + + + + + + + 翻译完成 显示翻译按钮 \'翻译\'会在你长按一条文字消息时出现。\n\nGoogle 可能会获取到这条你要翻译的文字消息的内容。 + 目标语言 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c39fbd9cc..c7db8de484 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2677,4 +2677,5 @@ translation done Show Translate Button The \'Translate\' button will appear when you make a long press on a text message.\n\nGoogle may have access to the text of messages you translate. + Target Language From bf0fa422a346f6e516a2ce3bcf48d867cb4dce82 Mon Sep 17 00:00:00 2001 From: touge Date: Wed, 17 May 2023 17:09:19 +0800 Subject: [PATCH 4/6] Add language selector --- .../android/ui/setting/AppearanceFragment.kt | 18 ++- .../CurrencyBottomSheetDialogFragment.kt | 6 +- ...nslateLanguageBottomSheetDialogFragment.kt | 115 ++++++++++++++++++ .../ui/setting/ui/page/AppearancePage.kt | 31 ++++- .../one/mixin/android/util/LanguageUtil.kt | 2 + .../mixin/android/util/TranslateManager.kt | 19 ++- ... => fragment_search_list_bottom_sheet.xml} | 2 +- app/src/main/res/layout/item_language.xml | 46 +++++++ 8 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt rename app/src/main/res/layout/{fragment_currency_bottom_sheet.xml => fragment_search_list_bottom_sheet.xml} (98%) create mode 100644 app/src/main/res/layout/item_language.xml diff --git a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt index 0f8e5ad1f4..392c16f9ed 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt @@ -30,6 +30,7 @@ import one.mixin.android.extension.singleChoice import one.mixin.android.session.Session import one.mixin.android.ui.common.BaseFragment import one.mixin.android.util.TimeCache +import one.mixin.android.util.TranslateManager import one.mixin.android.util.getLanguage import one.mixin.android.util.getLanguageOrDefault import one.mixin.android.util.getLocaleString @@ -141,12 +142,19 @@ class AppearanceFragment : BaseFragment(R.layout.fragment_appearance) { targetRl.isVisible = isChecked } targetLangTv.text = - if (isFollowSystem()) { - getString(R.string.Follow_system) - } else { - defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault()) + defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault())?.let { + TranslateManager.Language(it).nameInCurrentLanguage } - targetLangTv.setOnClickListener { } + targetRl.setOnClickListener { + val languageBottom = TranslateLanguageBottomSheetDialogFragment.newInstance() + languageBottom.callback = + object : TranslateLanguageBottomSheetDialogFragment.Callback { + override fun onLanguageClick(language: TranslateManager.Language) { + targetLangTv.text = language.nameInCurrentLanguage + } + } + languageBottom.showNow(parentFragmentManager, TranslateLanguageBottomSheetDialogFragment.TAG) + } val quoteColor = requireContext().defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false) quoteColorDescTv.setText( if (quoteColor) { diff --git a/app/src/main/java/one/mixin/android/ui/setting/CurrencyBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/CurrencyBottomSheetDialogFragment.kt index 9c03bdc0f0..c2e88cf0c3 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/CurrencyBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/CurrencyBottomSheetDialogFragment.kt @@ -10,7 +10,7 @@ import one.mixin.android.R import one.mixin.android.RxBus import one.mixin.android.api.handleMixinResponse import one.mixin.android.api.request.AccountUpdateRequest -import one.mixin.android.databinding.FragmentCurrencyBottomSheetBinding +import one.mixin.android.databinding.FragmentSearchListBottomSheetBinding import one.mixin.android.event.WalletOperationType import one.mixin.android.event.WalletRefreshedEvent import one.mixin.android.extension.appCompatActionBarHeight @@ -38,7 +38,7 @@ class CurrencyBottomSheetDialogFragment : MixinBottomSheetDialogFragment() { private val currencyAdapter = CurrencyAdapter() private val currencies = arrayListOf() - private val binding by viewBinding(FragmentCurrencyBottomSheetBinding::inflate) + private val binding by viewBinding(FragmentSearchListBottomSheetBinding::inflate) @SuppressLint("RestrictedApi") override fun setupDialog( @@ -70,7 +70,7 @@ class CurrencyBottomSheetDialogFragment : MixinBottomSheetDialogFragment() { savePreference(currency) } } - currencyRv.adapter = currencyAdapter + rv.adapter = currencyAdapter } currencies.clear() currencies.addAll(getCurrencyData(requireContext().resources)) diff --git a/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..7918060214 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt @@ -0,0 +1,115 @@ +package one.mixin.android.ui.setting + +import android.annotation.SuppressLint +import android.app.Dialog +import android.text.Editable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.google.mlkit.nl.translate.TranslateLanguage +import dagger.hilt.android.AndroidEntryPoint +import one.mixin.android.Constants +import one.mixin.android.databinding.FragmentSearchListBottomSheetBinding +import one.mixin.android.databinding.ItemLanguageBinding +import one.mixin.android.extension.appCompatActionBarHeight +import one.mixin.android.extension.containsIgnoreCase +import one.mixin.android.extension.defaultSharedPreferences +import one.mixin.android.extension.equalsIgnoreCase +import one.mixin.android.extension.putString +import one.mixin.android.extension.statusBarHeight +import one.mixin.android.ui.common.MixinBottomSheetDialogFragment +import one.mixin.android.util.TranslateManager +import one.mixin.android.util.getLanguageOrDefault +import one.mixin.android.util.viewBinding +import one.mixin.android.widget.BottomSheet +import one.mixin.android.widget.SearchView + +@AndroidEntryPoint +class TranslateLanguageBottomSheetDialogFragment : MixinBottomSheetDialogFragment() { + companion object { + const val TAG = "TranslateLanguageBottomSheetDialogFragment" + + fun newInstance() = TranslateLanguageBottomSheetDialogFragment() + } + + private val binding by viewBinding(FragmentSearchListBottomSheetBinding::inflate) + + private val adapter = LanguageAdapter { language -> + defaultSharedPreferences.putString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, language.code) + callback?.onLanguageClick(language) + dismiss() + } + private val languages = TranslateLanguage.getAllLanguages().map { TranslateManager.Language(it) } + + @SuppressLint("RestrictedApi") + override fun setupDialog(dialog: Dialog, style: Int) { + super.setupDialog(dialog, style) + context?.let { c -> + val topOffset = c.statusBarHeight() + c.appCompatActionBarHeight() + binding.root.heightOffset = topOffset + } + contentView = binding.root + (dialog as BottomSheet).setCustomView(contentView) + + binding.apply { + closeIv.setOnClickListener { dismiss() } + searchEt.listener = object : SearchView.OnSearchViewListener { + override fun afterTextChanged(s: Editable?) { + filter(s.toString()) + } + + override fun onSearch() { + } + } + binding.rv.adapter = adapter + adapter.submitList(languages) + } + } + + private fun filter(s: String) { + adapter.submitList( + if (s.isNotBlank()) { + languages.filter { + it.nameInCurrentLanguage.containsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) + }.sortedByDescending { + it.nameInCurrentLanguage.equalsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) + } + } else { + languages + }, + ) + } + + var callback: Callback? = null + + interface Callback { + fun onLanguageClick(language: TranslateManager.Language) + } + + class LanguageAdapter(private val callback: (TranslateManager.Language) -> Unit) : ListAdapter(TranslateManager.Language.DIFF_CALLBACK) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + LanguageHolder(ItemLanguageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + + override fun onBindViewHolder(holder: LanguageHolder, position: Int) { + getItem(position)?.let { holder.bind(it, callback) } + } + } + + class LanguageHolder(private val itemBinding: ItemLanguageBinding) : RecyclerView.ViewHolder(itemBinding.root) { + fun bind(language: TranslateManager.Language, callback: (TranslateManager.Language) -> Unit) { + itemBinding.apply { + if (language.code == root.context.defaultSharedPreferences.getString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault())) { + checkIv.isVisible = true + } else { + checkIv.isInvisible = true + } + name.text = language.nameInCurrentLanguage + desc.text = language.nameInSelfLanguage + } + itemView.setOnClickListener { callback(language) } + } + } +} diff --git a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt index d86d05d9b9..44b9e2e68b 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt @@ -54,8 +54,11 @@ import one.mixin.android.ui.setting.Currency import one.mixin.android.ui.setting.CurrencyBottomSheetDialogFragment import one.mixin.android.ui.setting.LocalSettingNav import one.mixin.android.ui.setting.SettingDestination +import one.mixin.android.ui.setting.TranslateLanguageBottomSheetDialogFragment import one.mixin.android.ui.setting.getLanguagePos import one.mixin.android.util.TimeCache +import one.mixin.android.util.TranslateManager +import one.mixin.android.util.getLanguageOrDefault import one.mixin.android.util.isFollowSystem import one.mixin.android.vo.Fiats import java.util.Locale @@ -391,11 +394,37 @@ private fun CurrencyItem() { @Composable private fun TranslateItem() { - var showTranslateButton by LocalContext.current.defaultSharedPreferences + val context = LocalContext.current + var showTranslateButton by context.defaultSharedPreferences .booleanValueAsState( key = Constants.Account.PREF_SHOW_TRANSLATE_BUTTON, defaultValue = false, ) + val targetLang = + remember { + mutableStateOf( + context.defaultSharedPreferences.getString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault())?.let { + TranslateManager.Language(it).nameInCurrentLanguage + } ?: "", + ) + } + AppearanceItem( + label = stringResource(id = R.string.Target_Language), + value = targetLang.value, + ) { + val activity = context.findFragmentActivityOrNull() ?: return@AppearanceItem + val languageBottom = TranslateLanguageBottomSheetDialogFragment.newInstance() + languageBottom.callback = + object : TranslateLanguageBottomSheetDialogFragment.Callback { + override fun onLanguageClick(language: TranslateManager.Language) { + targetLang.value = language.nameInCurrentLanguage + } + } + languageBottom.showNow( + activity.supportFragmentManager, + TranslateLanguageBottomSheetDialogFragment.TAG, + ) + } SettingTile( title = stringResource(R.string.Show_Translate_Button), description = stringResource(R.string.show_translate_button_hint), diff --git a/app/src/main/java/one/mixin/android/util/LanguageUtil.kt b/app/src/main/java/one/mixin/android/util/LanguageUtil.kt index 789e7d411e..11aadc2ee7 100644 --- a/app/src/main/java/one/mixin/android/util/LanguageUtil.kt +++ b/app/src/main/java/one/mixin/android/util/LanguageUtil.kt @@ -14,6 +14,8 @@ fun getLanguageOrDefault(): String = AppCompatDelegate.getApplicationLocales().g fun getCountry(): String? = AppCompatDelegate.getApplicationLocales().get(0)?.country +fun getLocale(): Locale = AppCompatDelegate.getApplicationLocales().get(0) ?: Locale.getDefault() + fun getLocaleString(): String = AppCompatDelegate.getApplicationLocales().get(0).toString() fun isCurrChinese(): Boolean = (getLanguage() ?: Locale.getDefault().language) == Locale.SIMPLIFIED_CHINESE.language diff --git a/app/src/main/java/one/mixin/android/util/TranslateManager.kt b/app/src/main/java/one/mixin/android/util/TranslateManager.kt index 64a02756da..2312deb759 100644 --- a/app/src/main/java/one/mixin/android/util/TranslateManager.kt +++ b/app/src/main/java/one/mixin/android/util/TranslateManager.kt @@ -2,6 +2,7 @@ package one.mixin.android.util import android.content.Context import android.util.LruCache +import androidx.recyclerview.widget.DiffUtil import com.google.mlkit.common.model.DownloadConditions import com.google.mlkit.nl.languageid.LanguageIdentification import com.google.mlkit.nl.languageid.LanguageIdentificationOptions @@ -107,9 +108,9 @@ class TranslateManager { } class Language(val code: String) : Comparable { - - val displayName: String - get() = Locale(code).displayName + private val locale = Locale(code) + val nameInCurrentLanguage: String = locale.getDisplayName(getLocale()) + val nameInSelfLanguage: String = locale.getDisplayLanguage(locale) override fun equals(other: Any?): Boolean { if (other === this) { @@ -125,16 +126,24 @@ class TranslateManager { } override fun toString(): String { - return displayName + return nameInCurrentLanguage } override fun compareTo(other: Language): Int { - return this.displayName.compareTo(other.displayName) + return this.nameInCurrentLanguage.compareTo(other.nameInCurrentLanguage) } override fun hashCode(): Int { return code.hashCode() } + + companion object { + val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Language, newItem: Language): Boolean = oldItem.code == newItem.code + + override fun areContentsTheSame(oldItem: Language, newItem: Language): Boolean = oldItem == newItem + } + } } companion object { const val TAG = "TranslateManager" diff --git a/app/src/main/res/layout/fragment_currency_bottom_sheet.xml b/app/src/main/res/layout/fragment_search_list_bottom_sheet.xml similarity index 98% rename from app/src/main/res/layout/fragment_currency_bottom_sheet.xml rename to app/src/main/res/layout/fragment_search_list_bottom_sheet.xml index 299613b9c5..fe85d0788a 100644 --- a/app/src/main/res/layout/fragment_currency_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_search_list_bottom_sheet.xml @@ -48,7 +48,7 @@ android:background="?attr/bg_window" /> + + + + + + + + + + \ No newline at end of file From 7165f16a111a3958b34fdb42a9ac0bf9a5aea77f Mon Sep 17 00:00:00 2001 From: touge Date: Wed, 17 May 2023 18:11:04 +0800 Subject: [PATCH 5/6] Check model downloaded --- .../ui/conversation/ConversationFragment.kt | 14 +++- .../mixin/android/util/TranslateManager.kt | 66 +++++++++++++++---- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt index fd06c98015..5ef528a3dd 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt @@ -6,6 +6,7 @@ import android.Manifest import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Activity +import android.app.ProgressDialog import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ContentResolver @@ -120,6 +121,7 @@ import one.mixin.android.extension.getParcelableExtraCompat import one.mixin.android.extension.getUriForFile import one.mixin.android.extension.hideKeyboard import one.mixin.android.extension.inTransaction +import one.mixin.android.extension.indeterminateProgressDialog import one.mixin.android.extension.isAuto import one.mixin.android.extension.isBluetoothHeadsetOrWiredHeadset import one.mixin.android.extension.isGif @@ -1652,7 +1654,16 @@ class ConversationFragment() : lifecycleScope.launch { val translated = withContext(Dispatchers.IO) { - translateManager?.translate(requireContext(), content) + translateManager?.translate(requireContext(), content) { complete -> + lifecycleScope.launch { + if (complete) { + translateDownloadModelDialog?.dismiss() + } else { + translateDownloadModelDialog = + indeterminateProgressDialog(message = R.string.Please_wait_a_bit) + } + } + } } if (translated != null) { messageAdapter.updateTranslated(messageItem.messageId, translated) @@ -1887,6 +1898,7 @@ class ConversationFragment() : } private var translateManager: TranslateManager? = null + private var translateDownloadModelDialog: ProgressDialog? = null private fun shouldShowTranslate(messageId: String): Boolean = defaultSharedPreferences.getBoolean(Constants.Account.PREF_SHOW_TRANSLATE_BUTTON, false) && diff --git a/app/src/main/java/one/mixin/android/util/TranslateManager.kt b/app/src/main/java/one/mixin/android/util/TranslateManager.kt index 2312deb759..0cee39f57d 100644 --- a/app/src/main/java/one/mixin/android/util/TranslateManager.kt +++ b/app/src/main/java/one/mixin/android/util/TranslateManager.kt @@ -3,10 +3,14 @@ package one.mixin.android.util import android.content.Context import android.util.LruCache import androidx.recyclerview.widget.DiffUtil +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks import com.google.mlkit.common.model.DownloadConditions +import com.google.mlkit.common.model.RemoteModelManager import com.google.mlkit.nl.languageid.LanguageIdentification import com.google.mlkit.nl.languageid.LanguageIdentificationOptions import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.TranslateRemoteModel import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.Translator import com.google.mlkit.nl.translate.TranslatorOptions @@ -41,14 +45,13 @@ class TranslateManager { oldValue.close() } } - - val availableLanguages: List = TranslateLanguage.getAllLanguages() - .map { Language(it) } + private val modelManager = RemoteModelManager.getInstance() fun translate( context: Context, text: String, targetLang: String? = context.defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault()), + doWhenDownloadModel: ((Boolean) -> Unit)? = null, // false/true represents download start/end ): String? { if (targetLang.isNullOrBlank()) { Timber.d("$TAG targetLang is $targetLang") @@ -58,7 +61,7 @@ class TranslateManager { val identifierTask = languageIdentifier.identifyLanguage(text).addOnCompleteListener { latch.countDown() } - val identifierDone = latch.await(5, TimeUnit.SECONDS) + val identifierDone = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!identifierDone) { Timber.d("$TAG languageIdentifier timeout ${identifierTask.exception?.stackTraceToString()}") return null @@ -77,24 +80,56 @@ class TranslateManager { .build() val translator = translators[options] - val conditions = DownloadConditions.Builder() - .requireWifi() - .build() + val needDownload: Boolean + var checkDownloadTask = isModelDownload(sourceLangCode) latch = CountDownLatch(1) - val downloadTask = translator.downloadModelIfNeeded(conditions).addOnCompleteListener { + var checkDownloadResult = checkDownloadTask.addOnCompleteListener { latch.countDown() } - val downloaded = latch.await(5, TimeUnit.SECONDS) - if (!downloaded || !downloadTask.isSuccessful) { - Timber.d("$TAG download model timeout or failed") + var checkResult = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) + if (!checkResult || !checkDownloadResult.isSuccessful) { + Timber.d("$TAG check source download timeout or failed") return null } + // if source model downloaded continue check target model, otherwise go download. + if (checkDownloadResult.result) { + checkDownloadTask = isModelDownload(targetLangCode) + latch = CountDownLatch(1) + checkDownloadResult = checkDownloadTask.addOnCompleteListener { + latch.countDown() + } + checkResult = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) + if (!checkResult || !checkDownloadResult.isSuccessful) { + Timber.d("$TAG check target download timeout or failed") + return null + } + needDownload = !checkDownloadResult.result + } else { + needDownload = true + } + + if (needDownload) { + doWhenDownloadModel?.invoke(false) + val conditions = DownloadConditions.Builder() + .requireWifi() + .build() + latch = CountDownLatch(1) + val downloadTask = translator.downloadModelIfNeeded(conditions).addOnCompleteListener { + latch.countDown() + } + val downloaded = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) + doWhenDownloadModel?.invoke(true) + if (!downloaded || !downloadTask.isSuccessful) { + Timber.d("$TAG download model timeout or failed") + return null + } + } latch = CountDownLatch(1) val task = translator.translate(text).addOnCompleteListener { latch.countDown() } - val translated = latch.await(5, TimeUnit.SECONDS) + val translated = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!translated || !task.isSuccessful) { Timber.d("$TAG translate timeout or failed") return null @@ -102,6 +137,11 @@ class TranslateManager { return task.result } + private fun isModelDownload(code: String): Task { + val translateLanguage = TranslateLanguage.fromLanguageTag(code) ?: return Tasks.forResult(false) + return modelManager.isModelDownloaded(TranslateRemoteModel.Builder(translateLanguage).build()) + } + fun release() { languageIdentifier.close() translators.evictAll() @@ -147,5 +187,7 @@ class TranslateManager { } companion object { const val TAG = "TranslateManager" + + private const val TASK_TIMEOUT = 5L } } From b6d9f87b5e65f92320bcb93602d761c7d4f16606 Mon Sep 17 00:00:00 2001 From: touge Date: Wed, 17 May 2023 20:25:01 +0800 Subject: [PATCH 6/6] Fix layout margin --- .../android/ui/setting/AppearanceFragment.kt | 13 ++- ...nslateLanguageBottomSheetDialogFragment.kt | 64 +++++++---- .../ui/setting/ui/page/AppearancePage.kt | 32 +++--- .../mixin/android/util/TranslateManager.kt | 107 ++++++++++-------- .../android/widget/MaxWidthRelativeLayout.kt | 5 +- 5 files changed, 134 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt index 392c16f9ed..fc1e6fd88f 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/AppearanceFragment.kt @@ -6,11 +6,13 @@ import android.os.Build import android.os.Bundle import android.view.Gravity import android.view.View +import android.view.ViewGroup.MarginLayoutParams import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.ListPopupWindow import androidx.core.content.ContextCompat import androidx.core.os.LocaleListCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import one.mixin.android.Constants @@ -22,6 +24,7 @@ import one.mixin.android.databinding.FragmentAppearanceBinding import one.mixin.android.event.QuoteColorEvent import one.mixin.android.extension.alertDialogBuilder import one.mixin.android.extension.defaultSharedPreferences +import one.mixin.android.extension.dp import one.mixin.android.extension.dpToPx import one.mixin.android.extension.navTo import one.mixin.android.extension.putBoolean @@ -136,10 +139,18 @@ class AppearanceFragment : BaseFragment(R.layout.fragment_appearance) { navTo(SettingSizeFragment.newInstance(), SettingSizeFragment.TAG) } translateSv.setContent(R.string.Show_Translate_Button) - translateSv.isChecked = defaultSharedPreferences.getBoolean(PREF_SHOW_TRANSLATE_BUTTON, false) + val showTranslateButton = defaultSharedPreferences.getBoolean(PREF_SHOW_TRANSLATE_BUTTON, false) + translateSv.isChecked = showTranslateButton + targetRl.isVisible = showTranslateButton + translateSv.updateLayoutParams { + topMargin = if (showTranslateButton) 0 else 20.dp + } translateSv.setOnCheckedChangeListener { _, isChecked -> defaultSharedPreferences.putBoolean(PREF_SHOW_TRANSLATE_BUTTON, isChecked) targetRl.isVisible = isChecked + translateSv.updateLayoutParams { + topMargin = if (isChecked) 0 else 20.dp + } } targetLangTv.text = defaultSharedPreferences.getString(PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault())?.let { diff --git a/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt index 7918060214..7940f8f1da 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/TranslateLanguageBottomSheetDialogFragment.kt @@ -37,15 +37,19 @@ class TranslateLanguageBottomSheetDialogFragment : MixinBottomSheetDialogFragmen private val binding by viewBinding(FragmentSearchListBottomSheetBinding::inflate) - private val adapter = LanguageAdapter { language -> - defaultSharedPreferences.putString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, language.code) - callback?.onLanguageClick(language) - dismiss() - } + private val adapter = + LanguageAdapter { language -> + defaultSharedPreferences.putString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, language.code) + callback?.onLanguageClick(language) + dismiss() + } private val languages = TranslateLanguage.getAllLanguages().map { TranslateManager.Language(it) } @SuppressLint("RestrictedApi") - override fun setupDialog(dialog: Dialog, style: Int) { + override fun setupDialog( + dialog: Dialog, + style: Int, + ) { super.setupDialog(dialog, style) context?.let { c -> val topOffset = c.statusBarHeight() + c.appCompatActionBarHeight() @@ -56,14 +60,15 @@ class TranslateLanguageBottomSheetDialogFragment : MixinBottomSheetDialogFragmen binding.apply { closeIv.setOnClickListener { dismiss() } - searchEt.listener = object : SearchView.OnSearchViewListener { - override fun afterTextChanged(s: Editable?) { - filter(s.toString()) - } + searchEt.listener = + object : SearchView.OnSearchViewListener { + override fun afterTextChanged(s: Editable?) { + filter(s.toString()) + } - override fun onSearch() { + override fun onSearch() { + } } - } binding.rv.adapter = adapter adapter.submitList(languages) } @@ -72,11 +77,12 @@ class TranslateLanguageBottomSheetDialogFragment : MixinBottomSheetDialogFragmen private fun filter(s: String) { adapter.submitList( if (s.isNotBlank()) { - languages.filter { - it.nameInCurrentLanguage.containsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) - }.sortedByDescending { - it.nameInCurrentLanguage.equalsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) - } + languages + .filter { + it.nameInCurrentLanguage.containsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) + }.sortedByDescending { + it.nameInCurrentLanguage.equalsIgnoreCase(s) || it.nameInSelfLanguage.containsIgnoreCase(s) + } } else { languages }, @@ -89,17 +95,29 @@ class TranslateLanguageBottomSheetDialogFragment : MixinBottomSheetDialogFragmen fun onLanguageClick(language: TranslateManager.Language) } - class LanguageAdapter(private val callback: (TranslateManager.Language) -> Unit) : ListAdapter(TranslateManager.Language.DIFF_CALLBACK) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - LanguageHolder(ItemLanguageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + class LanguageAdapter( + private val callback: (TranslateManager.Language) -> Unit, + ) : ListAdapter(TranslateManager.Language.DIFF_CALLBACK) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ) = LanguageHolder(ItemLanguageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - override fun onBindViewHolder(holder: LanguageHolder, position: Int) { + override fun onBindViewHolder( + holder: LanguageHolder, + position: Int, + ) { getItem(position)?.let { holder.bind(it, callback) } } } - class LanguageHolder(private val itemBinding: ItemLanguageBinding) : RecyclerView.ViewHolder(itemBinding.root) { - fun bind(language: TranslateManager.Language, callback: (TranslateManager.Language) -> Unit) { + class LanguageHolder( + private val itemBinding: ItemLanguageBinding, + ) : RecyclerView.ViewHolder(itemBinding.root) { + fun bind( + language: TranslateManager.Language, + callback: (TranslateManager.Language) -> Unit, + ) { itemBinding.apply { if (language.code == root.context.defaultSharedPreferences.getString(Constants.Account.PREF_TRANSLATE_TARGET_LANG, getLanguageOrDefault())) { checkIv.isVisible = true diff --git a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt index 44b9e2e68b..075f0c892a 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/ui/page/AppearancePage.kt @@ -408,22 +408,24 @@ private fun TranslateItem() { } ?: "", ) } - AppearanceItem( - label = stringResource(id = R.string.Target_Language), - value = targetLang.value, - ) { - val activity = context.findFragmentActivityOrNull() ?: return@AppearanceItem - val languageBottom = TranslateLanguageBottomSheetDialogFragment.newInstance() - languageBottom.callback = - object : TranslateLanguageBottomSheetDialogFragment.Callback { - override fun onLanguageClick(language: TranslateManager.Language) { - targetLang.value = language.nameInCurrentLanguage + if (showTranslateButton) { + AppearanceItem( + label = stringResource(id = R.string.Target_Language), + value = targetLang.value, + ) { + val activity = context.findFragmentActivityOrNull() ?: return@AppearanceItem + val languageBottom = TranslateLanguageBottomSheetDialogFragment.newInstance() + languageBottom.callback = + object : TranslateLanguageBottomSheetDialogFragment.Callback { + override fun onLanguageClick(language: TranslateManager.Language) { + targetLang.value = language.nameInCurrentLanguage + } } - } - languageBottom.showNow( - activity.supportFragmentManager, - TranslateLanguageBottomSheetDialogFragment.TAG, - ) + languageBottom.showNow( + activity.supportFragmentManager, + TranslateLanguageBottomSheetDialogFragment.TAG, + ) + } } SettingTile( title = stringResource(R.string.Show_Translate_Button), diff --git a/app/src/main/java/one/mixin/android/util/TranslateManager.kt b/app/src/main/java/one/mixin/android/util/TranslateManager.kt index 0cee39f57d..fbeb4dc59e 100644 --- a/app/src/main/java/one/mixin/android/util/TranslateManager.kt +++ b/app/src/main/java/one/mixin/android/util/TranslateManager.kt @@ -24,17 +24,17 @@ import java.util.concurrent.TimeUnit class TranslateManager { private val executor = Executors.newCachedThreadPool() - private val languageIdentifier = LanguageIdentification.getClient( - LanguageIdentificationOptions.Builder() - .setExecutor(executor) - .setConfidenceThreshold(0.5f) - .build(), - ) + private val languageIdentifier = + LanguageIdentification.getClient( + LanguageIdentificationOptions + .Builder() + .setExecutor(executor) + .setConfidenceThreshold(0.5f) + .build(), + ) private val translators = object : LruCache(3) { - override fun create(options: TranslatorOptions): Translator { - return Translation.getClient(options) - } + override fun create(options: TranslatorOptions): Translator = Translation.getClient(options) override fun entryRemoved( evicted: Boolean, @@ -58,9 +58,10 @@ class TranslateManager { return null } var latch = CountDownLatch(1) - val identifierTask = languageIdentifier.identifyLanguage(text).addOnCompleteListener { - latch.countDown() - } + val identifierTask = + languageIdentifier.identifyLanguage(text).addOnCompleteListener { + latch.countDown() + } val identifierDone = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!identifierDone) { Timber.d("$TAG languageIdentifier timeout ${identifierTask.exception?.stackTraceToString()}") @@ -73,19 +74,22 @@ class TranslateManager { return null } - val options = TranslatorOptions.Builder() - .setSourceLanguage(sourceLangCode) - .setTargetLanguage(targetLangCode) - .setExecutor(executor) - .build() + val options = + TranslatorOptions + .Builder() + .setSourceLanguage(sourceLangCode) + .setTargetLanguage(targetLangCode) + .setExecutor(executor) + .build() val translator = translators[options] val needDownload: Boolean var checkDownloadTask = isModelDownload(sourceLangCode) latch = CountDownLatch(1) - var checkDownloadResult = checkDownloadTask.addOnCompleteListener { - latch.countDown() - } + var checkDownloadResult = + checkDownloadTask.addOnCompleteListener { + latch.countDown() + } var checkResult = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!checkResult || !checkDownloadResult.isSuccessful) { Timber.d("$TAG check source download timeout or failed") @@ -95,9 +99,10 @@ class TranslateManager { if (checkDownloadResult.result) { checkDownloadTask = isModelDownload(targetLangCode) latch = CountDownLatch(1) - checkDownloadResult = checkDownloadTask.addOnCompleteListener { - latch.countDown() - } + checkDownloadResult = + checkDownloadTask.addOnCompleteListener { + latch.countDown() + } checkResult = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!checkResult || !checkDownloadResult.isSuccessful) { Timber.d("$TAG check target download timeout or failed") @@ -110,13 +115,16 @@ class TranslateManager { if (needDownload) { doWhenDownloadModel?.invoke(false) - val conditions = DownloadConditions.Builder() - .requireWifi() - .build() + val conditions = + DownloadConditions + .Builder() + .requireWifi() + .build() latch = CountDownLatch(1) - val downloadTask = translator.downloadModelIfNeeded(conditions).addOnCompleteListener { - latch.countDown() - } + val downloadTask = + translator.downloadModelIfNeeded(conditions).addOnCompleteListener { + latch.countDown() + } val downloaded = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) doWhenDownloadModel?.invoke(true) if (!downloaded || !downloadTask.isSuccessful) { @@ -126,9 +134,10 @@ class TranslateManager { } latch = CountDownLatch(1) - val task = translator.translate(text).addOnCompleteListener { - latch.countDown() - } + val task = + translator.translate(text).addOnCompleteListener { + latch.countDown() + } val translated = latch.await(TASK_TIMEOUT, TimeUnit.SECONDS) if (!translated || !task.isSuccessful) { Timber.d("$TAG translate timeout or failed") @@ -147,7 +156,9 @@ class TranslateManager { translators.evictAll() } - class Language(val code: String) : Comparable { + class Language( + val code: String, + ) : Comparable { private val locale = Locale(code) val nameInCurrentLanguage: String = locale.getDisplayName(getLocale()) val nameInSelfLanguage: String = locale.getDisplayLanguage(locale) @@ -165,26 +176,28 @@ class TranslateManager { return otherLang!!.code == code } - override fun toString(): String { - return nameInCurrentLanguage - } + override fun toString(): String = nameInCurrentLanguage - override fun compareTo(other: Language): Int { - return this.nameInCurrentLanguage.compareTo(other.nameInCurrentLanguage) - } + override fun compareTo(other: Language): Int = this.nameInCurrentLanguage.compareTo(other.nameInCurrentLanguage) - override fun hashCode(): Int { - return code.hashCode() - } + override fun hashCode(): Int = code.hashCode() companion object { - val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Language, newItem: Language): Boolean = oldItem.code == newItem.code - - override fun areContentsTheSame(oldItem: Language, newItem: Language): Boolean = oldItem == newItem - } + val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Language, + newItem: Language, + ): Boolean = oldItem.code == newItem.code + + override fun areContentsTheSame( + oldItem: Language, + newItem: Language, + ): Boolean = oldItem == newItem + } } } + companion object { const val TAG = "TranslateManager" diff --git a/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt b/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt index aa1b6ff040..75727499a7 100644 --- a/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt +++ b/app/src/main/java/one/mixin/android/widget/MaxWidthRelativeLayout.kt @@ -30,7 +30,10 @@ class MaxWidthRelativeLayout : RelativeLayout { } } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + override fun onMeasure( + widthMeasureSpec: Int, + heightMeasureSpec: Int, + ) { val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) var wms = widthMeasureSpec if (maxWidth in 1 until measuredWidth) {