From 4a85b2cb3cbb8e499208103b4059c5a47e35d194 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Fri, 28 Nov 2025 10:46:53 +0100 Subject: [PATCH 1/3] Fix viewbinding (#442) * testing fixes for crashes see #441 * formatting * readded missing torrent status update * fix torrent conversion to download not working if files are reselected * readded scrolling delay parallelization * formatting * stop service after 20 minutes if no torrents are active see #403 * skip wrong warning if no file is selected 0 will trigger this * Update ForegroundTorrentService.kt * formatting --- .../data/service/ForegroundTorrentService.kt | 38 +++++++++++--- .../unchained/lists/view/ListsTabFragment.kt | 50 ++++++++++--------- .../unchained/start/view/StartFragment.kt | 30 +++++++++-- .../view/TorrentDetailsFragment.kt | 7 ++- app/app/src/main/res/values-es/strings.xml | 1 + app/app/src/main/res/values-fr/strings.xml | 1 + app/app/src/main/res/values-it/strings.xml | 1 + app/app/src/main/res/values-ko/strings.xml | 1 + app/app/src/main/res/values-tr/strings.xml | 1 + app/app/src/main/res/values/strings.xml | 1 + 10 files changed, 94 insertions(+), 37 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt index 1b2cca78..f67e3942 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt @@ -32,6 +32,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +const val MAX_SERVICE_DURATION = 5 * 60 * 60 * 1000 +const val MIN_SERVICE_DURATION = 20 * 60 * 1000 + @AndroidEntryPoint @SuppressLint("MissingPermission") class ForegroundTorrentService : LifecycleService() { @@ -157,21 +160,38 @@ class ForegroundTorrentService : LifecycleService() { // right now on api >= 35 after 6 hours the service will crash // because of system imposed limits if ( - Build.VERSION.SDK_INT >= 35 && - System.currentTimeMillis() - serviceStart > 5 * 60 * 60 * 1000 + Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + System.currentTimeMillis() - serviceStart > MAX_SERVICE_DURATION ) { Timber.w("Service has been running for too long, stopping it.") break } try { - torrentsLiveData.postValue(getTorrentList()) + val torrentList = getTorrentList() + torrentsLiveData.postValue(torrentList) + + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + System.currentTimeMillis() - serviceStart > MIN_SERVICE_DURATION + ) { + // if there are no active torrents and the services has been started + // for at least some minutes, stop the service + val unfinishedTorrents = + torrentList.count { loadingStatusList.contains(it.status) } + if (unfinishedTorrents == 0) { + Timber.i( + "Service has been running and no torrents are active, stopping it." + ) + break + } + } } catch (ex: IllegalArgumentException) { // no valid token ready, retry later } // update notifications every 5 seconds delay(updateTiming) } - stopSelf() + stopTorrentService() } } @@ -271,10 +291,14 @@ class ForegroundTorrentService : LifecycleService() { } private fun stopTorrentService() { - torrentsLiveData.value?.let { - it.forEach { torrent -> notificationManager.cancel(torrent.id.hashCode()) } + lifecycleScope.launch { + // delay used to let the notification finish + delay(1000) + notificationManager.cancel(SUMMARY_ID) + // this will avoid removing the notifications, so the user can see what happened in the + // meanwhile + stopForeground(STOP_FOREGROUND_DETACH) } - stopSelf() } companion object { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt index 5ef00d57..d072099a 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt @@ -512,14 +512,18 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { // removes the loading icon from the swipe layout val downloadObserver = Observer> { - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { + val b = + _binding + ?: return@launch // capture binding and bail out if view was destroyed + downloadAdapter.submitData(it) // stop the refresh animation if playing - if (binding.srLayout.isRefreshing) { - binding.srLayout.isRefreshing = false + if (b.srLayout.isRefreshing) { + b.srLayout.isRefreshing = false // scroll to top if we were refreshing lifecycleScope.launch { - binding.rvDownloadList.delayedScrolling(requireContext()) + b.rvDownloadList.delayedScrolling(requireContext()) } } // delay for notifying the list that the items have changed, otherwise stuff @@ -699,19 +703,19 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { } else context?.showToast(R.string.select_one_item) } binding.bDetailsSelected.setOnClickListener { - if (torrentTracker.selection.toList().size == 1) { - val item: TorrentItem = torrentTracker.selection.toList().first() - val action = - if (beforeSelectionStatusList.contains(item.status)) - ListsTabFragmentDirections.actionListTabsDestToTorrentProcessingFragment( - torrentID = item.id - ) - else ListsTabFragmentDirections.actionListsTabToTorrentDetails(item) - findNavController().navigate(action) - } else - Timber.e( - "Somehow user triggered openSelectedDetails with a selection size of ${torrentTracker.selection.toList().size}" - ) + if (torrentTracker.selection.size() != 1) { + // we can only open a single torrent at a time + return@setOnClickListener + } + + val item: TorrentItem = torrentTracker.selection.toList().first() + val action = + if (beforeSelectionStatusList.contains(item.status)) + ListsTabFragmentDirections.actionListTabsDestToTorrentProcessingFragment( + torrentID = item.id + ) + else ListsTabFragmentDirections.actionListsTabToTorrentDetails(item) + findNavController().navigate(action) } binding.bAddNew?.setOnClickListener { // landscape only @@ -736,13 +740,13 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { val torrentObserver = Observer> { - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { + val b = _binding ?: return@launch + torrentAdapter.submitData(it) - if (binding.srLayout.isRefreshing) { - binding.srLayout.isRefreshing = false - lifecycleScope.launch { - binding.rvTorrentList.delayedScrolling(requireContext()) - } + if (b.srLayout.isRefreshing) { + b.srLayout.isRefreshing = false + lifecycleScope.launch { b.rvTorrentList.delayedScrolling(requireContext()) } } delay(300) torrentAdapter.notifyDataSetChanged() diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt index 31d1a73b..64272b17 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.base.UnchainedFragment @@ -40,19 +41,19 @@ class StartFragment : UnchainedFragment() { FSMAuthenticationState.StartNewLogin -> { val action = StartFragmentDirections.actionStartFragmentToAuthenticationFragment() - findNavController().navigate(action) + safeNavigate(action) } FSMAuthenticationState.AuthenticatedOpenToken -> { val action = StartFragmentDirections.actionStartFragmentToUserProfileFragment() - findNavController().navigate(action) - activityViewModel.goToStartUpScreen() + val navigated = safeNavigate(action) + if (navigated) activityViewModel.goToStartUpScreen() } FSMAuthenticationState.AuthenticatedPrivateToken -> { val action = StartFragmentDirections.actionStartFragmentToUserProfileFragment() - findNavController().navigate(action) - activityViewModel.goToStartUpScreen() + val navigated = safeNavigate(action) + if (navigated) activityViewModel.goToStartUpScreen() } is FSMAuthenticationState.WaitingUserAction -> { // todo: show action needed @@ -103,6 +104,25 @@ class StartFragment : UnchainedFragment() { return binding.root } + private fun safeNavigate(action: NavDirections): Boolean { + val nav = findNavController() + val current = nav.currentDestination + if (current != null && current.getAction(action.actionId) != null) { + try { + nav.navigate(action) + return true + } catch (e: IllegalArgumentException) { + Timber.w(e, "Safe navigate failed for actionId=${action.actionId}") + return false + } + } else { + Timber.w( + "Navigation action not found from destination ${current?.id} for actionId=${action.actionId}" + ) + return false + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt index 75b5d022..4ef979a7 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt @@ -120,7 +120,7 @@ class TorrentDetailsFragment : UnchainedFragment(), TorrentContentListener { binding.tvStatus.text = statusTranslation[args.item.status] ?: args.item.status binding.fabShareMagnet.setOnClickListener { onShareMagnetClick() } binding.fabCopyMagnet.setOnClickListener { onCopyMagnetClick() } - binding.bDownload.setOnClickListener { onDownloadClick(args.item) } + binding.bDownload.setOnClickListener { onDownloadClick() } val adapter = TorrentContentFilesAdapter() binding.rvFileList.adapter = adapter @@ -133,6 +133,8 @@ class TorrentDetailsFragment : UnchainedFragment(), TorrentContentListener { torrent.files?.count { file -> file.selected == 1 } ?: 0 binding.tvSelectedFilesNumber.text = selectedFiles.toString() + binding.tvStatus.text = statusTranslation[torrent.status] ?: torrent.status + binding.tvTotalFiles.text = (torrent.files?.count() ?: 0).toString() binding.tvName.text = torrent.filename binding.tvProgressPercent.text = @@ -278,7 +280,8 @@ class TorrentDetailsFragment : UnchainedFragment(), TorrentContentListener { _binding = null } - fun onDownloadClick(item: TorrentItem) { + fun onDownloadClick() { + val item: TorrentItem = viewModel.torrentLiveData.value?.peekContent() ?: args.item if (item.links.size > 1) { val action = TorrentDetailsFragmentDirections.actionTorrentDetailsToTorrentFolder( diff --git a/app/app/src/main/res/values-es/strings.xml b/app/app/src/main/res/values-es/strings.xml index 8f9dc2b6..f19445b8 100644 --- a/app/app/src/main/res/values-es/strings.xml +++ b/app/app/src/main/res/values-es/strings.xml @@ -489,6 +489,7 @@ Nuevo servicio Dispositivo remoto Servicio suprimido + El servicio se ha detenido Servicio Servicios diff --git a/app/app/src/main/res/values-fr/strings.xml b/app/app/src/main/res/values-fr/strings.xml index 56b75fea..a9f7b533 100644 --- a/app/app/src/main/res/values-fr/strings.xml +++ b/app/app/src/main/res/values-fr/strings.xml @@ -489,6 +489,7 @@ Nouveau service Dispositif à distance Service supprimé + Le service a été interrompu Service Services diff --git a/app/app/src/main/res/values-it/strings.xml b/app/app/src/main/res/values-it/strings.xml index 1ed1ae50..769db875 100644 --- a/app/app/src/main/res/values-it/strings.xml +++ b/app/app/src/main/res/values-it/strings.xml @@ -490,6 +490,7 @@ Nuovo servizio Dispositivo remoto Servizio eliminato + Il servizio è stato fermato Servizio %d servizi diff --git a/app/app/src/main/res/values-ko/strings.xml b/app/app/src/main/res/values-ko/strings.xml index ad538b61..f78e205e 100644 --- a/app/app/src/main/res/values-ko/strings.xml +++ b/app/app/src/main/res/values-ko/strings.xml @@ -493,6 +493,7 @@ 새 서비스 원격 기기 서비스 삭제됨 + 서비스가 중지되었습니다 서비스 서비스 diff --git a/app/app/src/main/res/values-tr/strings.xml b/app/app/src/main/res/values-tr/strings.xml index 50b63c06..41c1f317 100644 --- a/app/app/src/main/res/values-tr/strings.xml +++ b/app/app/src/main/res/values-tr/strings.xml @@ -492,6 +492,7 @@ Yeni Hizmet Uzak Cihaz Hizmet silindi + Hizmet durduruldu Hizmet Hizmetler diff --git a/app/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml index fd2f9667..a35ce10d 100644 --- a/app/app/src/main/res/values/strings.xml +++ b/app/app/src/main/res/values/strings.xml @@ -711,6 +711,7 @@ Remote Device Jackett Service deleted + Service has been stopped Service Services From 95a1988b9ece72d558528de1d940ab6219ab97c0 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Fri, 28 Nov 2025 23:45:37 +0100 Subject: [PATCH 2/3] bumped version to 1.5.1 --- app/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 24534779..2059edd4 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -43,8 +43,8 @@ android { applicationId = "com.github.livingwithhippos.unchained" minSdk = 27 targetSdk = 36 - versionCode = 55 - versionName = "1.5.0" + versionCode = 56 + versionName = "1.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } From 20506e375a0534cbdb8f2cab964bf61451805aaf Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Fri, 28 Nov 2025 23:51:03 +0100 Subject: [PATCH 3/3] updated changelog --- fastlane/metadata/android/en-US/changelogs/55.txt | 2 +- fastlane/metadata/android/en-US/changelogs/56.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/56.txt diff --git a/fastlane/metadata/android/en-US/changelogs/55.txt b/fastlane/metadata/android/en-US/changelogs/55.txt index 816e6515..c60835ff 100644 --- a/fastlane/metadata/android/en-US/changelogs/55.txt +++ b/fastlane/metadata/android/en-US/changelogs/55.txt @@ -1,4 +1,4 @@ -What's new 1.3.8: +What's new in 1.5.0: - added Korean translation thanks to poihoii - torrent file can be selected by pressing on their whole row - show user code needed for new authentication diff --git a/fastlane/metadata/android/en-US/changelogs/56.txt b/fastlane/metadata/android/en-US/changelogs/56.txt new file mode 100644 index 00000000..d0cdfbc3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/56.txt @@ -0,0 +1,5 @@ +What's new in 1.5.1: +- fixed missing torrent status update +- fixed torrent to download conversion not working when reselecting files +- stop torrent monitoring services after 20 minutes if there's no activity +- tried to fix crashes