Explorar el Código

Merge remote-tracking branch 'origin/master'

kunfei hace 3 años
padre
commit
77bde68c92

+ 3 - 2
app/src/main/java/io/legado/app/base/BaseService.kt

@@ -18,8 +18,9 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
         scope: CoroutineScope = this,
         context: CoroutineContext = Dispatchers.IO,
         start: CoroutineStart = CoroutineStart.DEFAULT,
+        executeContext: CoroutineContext = Dispatchers.Main,
         block: suspend CoroutineScope.() -> T
-    ) = Coroutine.async(scope, context, start) { block() }
+    ) = Coroutine.async(scope, context, start, executeContext, block)
 
     @CallSuper
     override fun onCreate() {
@@ -57,7 +58,7 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
     /**
      * 检测通知权限
      */
-    private fun checkNotificationPermission()  {
+    private fun checkNotificationPermission() {
         PermissionsCompat.Builder()
             .addPermissions(Permissions.POST_NOTIFICATIONS)
             .rationale(R.string.notification_permission_rationale)

+ 4 - 1
app/src/main/java/io/legado/app/base/BaseViewModel.kt

@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
 import io.legado.app.App
 import io.legado.app.help.coroutine.Coroutine
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
 import kotlin.coroutines.CoroutineContext
@@ -19,9 +20,11 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio
     fun <T> execute(
         scope: CoroutineScope = viewModelScope,
         context: CoroutineContext = Dispatchers.IO,
+        start: CoroutineStart = CoroutineStart.DEFAULT,
+        executeContext: CoroutineContext = Dispatchers.Main,
         block: suspend CoroutineScope.() -> T
     ): Coroutine<T> {
-        return Coroutine.async(scope, context) { block() }
+        return Coroutine.async(scope, context, start, executeContext, block)
     }
 
     fun <R> submit(

+ 1 - 1
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -183,7 +183,7 @@ data class BookSource(
 
     fun getInvalidGroupNames(): String {
         return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter {
-            "失效" in it
+            "失效" in it || it == "校验超时"
         }?.joinToString() ?: ""
     }
 

+ 4 - 2
app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt

@@ -25,6 +25,7 @@ class Coroutine<T>(
     val scope: CoroutineScope,
     context: CoroutineContext = Dispatchers.IO,
     val startOption: CoroutineStart = CoroutineStart.DEFAULT,
+    val executeContext: CoroutineContext = Dispatchers.Main,
     block: suspend CoroutineScope.() -> T
 ) {
 
@@ -36,9 +37,10 @@ class Coroutine<T>(
             scope: CoroutineScope = DEFAULT,
             context: CoroutineContext = Dispatchers.IO,
             start: CoroutineStart = CoroutineStart.DEFAULT,
+            executeContext: CoroutineContext = Dispatchers.Main,
             block: suspend CoroutineScope.() -> T
         ): Coroutine<T> {
-            return Coroutine(scope, context, start, block)
+            return Coroutine(scope, context, start, executeContext, block)
         }
 
     }
@@ -158,7 +160,7 @@ class Coroutine<T>(
         context: CoroutineContext,
         block: suspend CoroutineScope.() -> T
     ): Job {
-        return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
+        return (scope.plus(executeContext)).launch(start = startOption) {
             try {
                 start?.let { dispatchVoidCallback(this, it) }
                 ensureActive()

+ 12 - 4
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -57,9 +57,8 @@ object LocalBook {
                     }.firstOrNull()?.let {
                         getBookInputStream(it)
                     }
-                } else if (webDavUrl != null) {
+                } else if (webDavUrl != null && downloadRemoteBook(book)) {
                     // 下载远程链接
-                    downloadRemoteBook(book)
                     getBookInputStream(book)
                 } else {
                     null
@@ -90,12 +89,15 @@ object LocalBook {
             book.isEpub -> {
                 EpubFile.getChapterList(book)
             }
+
             book.isUmd -> {
                 UmdFile.getChapterList(book)
             }
+
             book.isPdf -> {
                 PdfFile.getChapterList(book)
             }
+
             else -> {
                 TextFile.getChapterList(book)
             }
@@ -117,12 +119,15 @@ object LocalBook {
                 book.isEpub -> {
                     EpubFile.getContent(book, chapter)
                 }
+
                 book.isUmd -> {
                     UmdFile.getContent(book, chapter)
                 }
+
                 book.isPdf -> {
                     PdfFile.getContent(book, chapter)
                 }
+
                 else -> {
                     TextFile.getContent(book, chapter)
                 }
@@ -218,7 +223,7 @@ object LocalBook {
         }
     }
 
-   /* 批量导入 支持自动导入压缩包的支持书籍 */
+    /* 批量导入 支持自动导入压缩包的支持书籍 */
     fun importFiles(uri: Uri): List<Book> {
         val books = mutableListOf<Book>()
         val fileDoc = FileDoc.fromUri(uri, false)
@@ -329,6 +334,7 @@ object LocalBook {
                     Base64.DEFAULT
                 )
             )
+
             else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
         }
         return saveBookFile(inputStream, fileName)
@@ -383,7 +389,7 @@ object LocalBook {
     }
 
     //下载book对应的远程文件 并更新Book
-    private fun downloadRemoteBook(localBook: Book) {
+    private fun downloadRemoteBook(localBook: Book): Boolean {
         val webDavUrl = localBook.getRemoteUrl()
         if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
         try {
@@ -415,9 +421,11 @@ object LocalBook {
                     localBook.save()
                 }
             }
+            return true
         } catch (e: Exception) {
             e.printOnDebug()
             AppLog.put("自动下载webDav书籍失败", e)
+            return false
         }
     }
 

+ 5 - 2
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -122,7 +122,11 @@ class CheckSourceService : BaseService() {
      *校验书源
      */
     private fun check(source: BookSource) {
-        execute(context = searchCoroutine, start = CoroutineStart.LAZY) {
+        execute(
+            context = searchCoroutine,
+            start = CoroutineStart.LAZY,
+            executeContext = IO
+        ) {
             Debug.startChecking(source)
             var searchWord = CheckSource.keyword
             source.ruleSearch?.checkKeyWord?.let {
@@ -188,7 +192,6 @@ class CheckSourceService : BaseService() {
                         "" else "\n\n${source.bookSourceComment}"
                 Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
             }.onSuccess(searchCoroutine) {
-                source.removeGroup("校验超时")
                 Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
             }.onFinally(IO) {
                 source.respondTime = Debug.getRespondTime(source.bookSourceUrl)

+ 35 - 41
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -26,7 +26,7 @@ import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.ensureActive
@@ -68,32 +68,21 @@ class HttpReadAloudService : BaseReadAloudService(),
         super.onDestroy()
         downloadTask?.cancel()
         exoPlayer.release()
+        Coroutine.async {
+            removeCacheFile()
+        }
     }
 
     override fun play() {
         pageChanged = false
         exoPlayer.stop()
+        if (!requestFocus()) return
         if (contentList.isEmpty()) {
             AppLog.putDebug("朗读列表为空")
             ReadBook.readAloud()
         } else {
             super.play()
-            kotlin.runCatching {
-                if (nowSpeak == 0) {
-                    downloadAudio()
-                } else {
-                    val fileName = md5SpeakFileName(contentList[nowSpeak])
-                    val file = getSpeakFileAsMd5(fileName)
-                    if (file.exists()) {
-                        playAudio(file)
-                    } else if (!downloadTaskActiveLock.isLocked) {
-                        downloadAudio()
-                    }
-                }
-            }.onFailure {
-                toastOnUi("朗读出错:${it.localizedMessage}")
-                AppLog.put("朗读出错:${it.localizedMessage}", it)
-            }
+            downloadAndPlayAudios()
         }
     }
 
@@ -105,21 +94,21 @@ class HttpReadAloudService : BaseReadAloudService(),
         readAloudNumber += contentList[nowSpeak].length + 1
         if (nowSpeak < contentList.lastIndex) {
             nowSpeak++
-            play()
         } else {
             nextChapter()
         }
     }
 
-    private fun downloadAudio() {
+    private fun downloadAndPlayAudios() {
+        exoPlayer.clearMediaItems()
         downloadTask?.cancel()
         downloadTask = execute {
             downloadTaskActiveLock.withLock {
                 ensureActive()
-                removeCacheFile()
                 val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
                 contentList.forEachIndexed { index, content ->
                     ensureActive()
+                    if (index < nowSpeak) return@forEachIndexed
                     val fileName = md5SpeakFileName(content)
                     val speakText = content.replace(AppPattern.notReadAloudRegex, "")
                     if (speakText.isEmpty()) {
@@ -141,13 +130,23 @@ class HttpReadAloudService : BaseReadAloudService(),
                             return@execute
                         }
                     }
-                    if (index == nowSpeak) {
-                        val file = getSpeakFileAsMd5(fileName)
-                        playAudio(file)
+                    val file = getSpeakFileAsMd5(fileName)
+                    val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
+                    launch(Main) {
+                        if (exoPlayer.playbackState == Player.STATE_ENDED) {
+                            exoPlayer.stop()
+                            exoPlayer.clearMediaItems()
+                        }
+                        exoPlayer.addMediaItem(mediaItem)
+                        if (!exoPlayer.isPlaying) {
+                            exoPlayer.playWhenReady = !pause
+                            exoPlayer.prepare()
+                        }
                     }
                 }
             }
-        }.onError(IO) {
+        }.onError {
+            toastOnUi("朗读出错:${it.localizedMessage}")
             AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
         }
     }
@@ -193,6 +192,7 @@ class HttpReadAloudService : BaseReadAloudService(),
                         e.printOnDebug()
                         throw e
                     }
+
                     is SocketTimeoutException, is ConnectException -> {
                         downloadErrorNo++
                         if (downloadErrorNo > 5) {
@@ -202,6 +202,7 @@ class HttpReadAloudService : BaseReadAloudService(),
                             throw e
                         }
                     }
+
                     else -> {
                         downloadErrorNo++
                         val msg = "tts下载错误\n${e.localizedMessage}"
@@ -223,22 +224,6 @@ class HttpReadAloudService : BaseReadAloudService(),
         return null
     }
 
-    @Synchronized
-    private fun playAudio(file: File) {
-        if (requestFocus()) {
-            launch {
-                kotlin.runCatching {
-                    val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
-                    exoPlayer.setMediaItem(mediaItem)
-                    exoPlayer.playWhenReady = true
-                    exoPlayer.prepare()
-                }.onFailure {
-                    it.printOnDebug()
-                }
-            }
-        }
-    }
-
     private fun md5SpeakFileName(content: String): String {
         return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
                 MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
@@ -338,7 +323,7 @@ class HttpReadAloudService : BaseReadAloudService(),
         downloadTask?.cancel()
         exoPlayer.stop()
         speechRate = AppConfig.speechRatePlay + 5
-        downloadAudio()
+        downloadAndPlayAudios()
     }
 
     override fun onPlaybackStateChanged(playbackState: Int) {
@@ -347,15 +332,18 @@ class HttpReadAloudService : BaseReadAloudService(),
             Player.STATE_IDLE -> {
                 // 空闲
             }
+
             Player.STATE_BUFFERING -> {
                 // 缓冲中
             }
+
             Player.STATE_READY -> {
                 // 准备好
                 if (pause) return
                 exoPlayer.play()
                 upPlayPos()
             }
+
             Player.STATE_ENDED -> {
                 // 结束
                 playErrorNo = 0
@@ -364,6 +352,12 @@ class HttpReadAloudService : BaseReadAloudService(),
         }
     }
 
+    override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
+        if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return
+        upPlayPos()
+        playNext()
+    }
+
     override fun onPlayerError(error: PlaybackException) {
         super.onPlayerError(error)
         AppLog.put("朗读错误\n${contentList[nowSpeak]}", error)

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -44,6 +44,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 
 /**
@@ -204,7 +205,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
             }
         }
         launch {
-            appDb.bookSourceDao.flowEnabledGroups().conflate().collect {
+            appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect {
                 groups.clear()
                 groups.addAll(it)
                 upGroupMenu()

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -47,8 +47,10 @@ import io.legado.app.utils.startActivity
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.visible
+import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 
 
@@ -230,7 +232,7 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
             }
         }
         launch {
-            appDb.bookSourceDao.flowEnabledGroups().conflate().collect {
+            appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect {
                 groups.clear()
                 groups.addAll(it)
                 upGroupMenu()

+ 8 - 2
app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt

@@ -108,7 +108,8 @@ abstract class BaseReadBookActivity :
      */
     fun upSystemUiVisibility(
         isInMultiWindow: Boolean,
-        toolBarHide: Boolean = true
+        toolBarHide: Boolean = true,
+        useBgMeanColor: Boolean = false
     ) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             window.insetsController?.run {
@@ -129,7 +130,10 @@ abstract class BaseReadBookActivity :
             setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark())
         } else {
             val statusBarColor =
-                if (AppConfig.readBarStyleFollowPage && ReadBookConfig.durConfig.curBgType() == 0) {
+                if (AppConfig.readBarStyleFollowPage
+                    && ReadBookConfig.durConfig.curBgType() == 0
+                    || useBgMeanColor
+                ) {
                     ReadBookConfig.bgMeanColor
                 } else {
                     ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar)
@@ -184,12 +188,14 @@ abstract class BaseReadBookActivity :
                             width = MATCH_PARENT
                             gravity = Gravity.BOTTOM
                         }
+
                     Gravity.LEFT -> layoutParams =
                         (layoutParams as FrameLayout.LayoutParams).apply {
                             height = MATCH_PARENT
                             width = navigationBarHeight
                             gravity = Gravity.LEFT
                         }
+
                     Gravity.RIGHT -> layoutParams =
                         (layoutParams as FrameLayout.LayoutParams).apply {
                             height = MATCH_PARENT

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -1044,7 +1044,7 @@ class ReadBookActivity : BaseReadBookActivity(),
      * 更新状态栏,导航栏
      */
     override fun upSystemUiVisibility() {
-        upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible)
+        upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible, bottomDialog > 0)
         upNavigationBarColor()
     }
 

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt

@@ -93,7 +93,7 @@ class SpeakEngineDialog(val callBack: CallBack) : BaseDialogFragment(R.layout.di
                 labelSys.visible()
                 cbName.text = "系统默认"
                 cbName.tag = ""
-                cbName.isChecked = ttsEngine == null || ttsEngine!!.toIntOrNull() == null
+                cbName.isChecked = ttsEngine == null || ttsEngine!!.isJsonObject()
                         && GSON.fromJsonObject<SelectItem<String>>(ttsEngine)
                     .getOrNull()?.value.isNullOrEmpty()
                 cbName.setOnClickListener {

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt

@@ -31,6 +31,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
@@ -279,7 +280,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
             adapter.setItems(it)
         }
         launch {
-            appDb.bookSourceDao.flowEnabledGroups().collect {
+            appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).collect {
                 groups = it
             }
         }

+ 5 - 2
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -42,9 +42,11 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
 /**
@@ -321,7 +323,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 }
             }.catch {
                 AppLog.put("书源界面更新书源出错", it)
-            }.conflate().collect { data ->
+            }.flowOn(IO).conflate().collect { data ->
                 adapter.setItems(data, adapter.diffItemCallback)
                 itemTouchCallback.isCanDrag = sort == BookSourceSort.Default
                 delay(500)
@@ -336,10 +338,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
 
     private fun initLiveDataGroup() {
         launch {
-            appDb.bookSourceDao.flowGroups().conflate().collect {
+            appDb.bookSourceDao.flowGroups().flowOn(IO).conflate().collect {
                 groups.clear()
                 groups.addAll(it)
                 upGroupMenu()
+                delay(500)
             }
         }
     }

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt

@@ -25,6 +25,8 @@ import io.legado.app.utils.applyTint
 import io.legado.app.utils.requestInputMethod
 import io.legado.app.utils.setLayout
 import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 
 
@@ -55,7 +57,7 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
 
     private fun initData() {
         launch {
-            appDb.bookSourceDao.flowGroups().collect {
+            appDb.bookSourceDao.flowGroups().flowOn(IO).collect {
                 adapter.setItems(it)
             }
         }

+ 4 - 1
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -20,6 +20,7 @@ import io.legado.app.model.webBook.WebBook
 import io.legado.app.service.CacheBookService
 import io.legado.app.utils.postEvent
 import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers.IO
 import java.util.concurrent.CopyOnWriteArraySet
 import java.util.concurrent.Executors
 import kotlin.math.min
@@ -85,9 +86,11 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
                         upTocJob?.cancel()
                         upTocJob = null
                     }
+
                     onUpTocBooks.size < threadCount -> {
                         updateToc()
                     }
+
                     else -> {
                         delay(500)
                     }
@@ -119,7 +122,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
         }
         waitUpTocBooks.remove(bookUrl)
         upTocAdd(bookUrl)
-        execute(context = upTocPool) {
+        execute(context = upTocPool, executeContext = IO) {
             kotlin.runCatching {
                 val oldBook = book.copy()
                 WebBook.runPreUpdateJs(source, book)

+ 3 - 1
app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt

@@ -29,9 +29,11 @@ import io.legado.app.utils.setEdgeEffectColor
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 
 /**
@@ -107,7 +109,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
 
     private fun initGroupData() {
         launch {
-            appDb.bookSourceDao.flowExploreGroups().conflate().collect {
+            appDb.bookSourceDao.flowExploreGroups().flowOn(IO).conflate().collect {
                 groups.clear()
                 groups.addAll(it)
                 upGroupsMenu()

+ 4 - 2
app/src/main/res/layout/item_source_import.xml

@@ -11,6 +11,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:maxLines="1"
+        android:singleLine="true"
         app:layout_constrainedWidth="true"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintHorizontal_bias="0"
@@ -25,12 +26,13 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:padding="8dp"
+        android:singleLine="true"
         android:textColor="@color/secondaryText"
         app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintLeft_toRightOf="@id/cb_source_name"
         app:layout_constraintRight_toLeftOf="@id/tv_open"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintHorizontal_chainStyle="packed" />
+        app:layout_constraintTop_toTopOf="parent" />
 
     <TextView
         android:id="@+id/tv_open"