diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 25c64c796..3d40cb7b9 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -2,7 +2,6 @@ import java.util.Properties plugins { id("com.android.application") - id("org.jetbrains.kotlin.android") alias(libs.plugins.kotlin.serialization) id("org.jetbrains.kotlin.plugin.parcelize") id("com.google.dagger.hilt.android") @@ -38,6 +37,8 @@ ktfmt { kotlin { jvmToolchain(11) } +room { schemaDirectory("$projectDir/schemas") } + android { namespace = "com.github.livingwithhippos.unchained" compileSdk = 37 @@ -46,14 +47,12 @@ android { applicationId = "com.github.livingwithhippos.unchained" minSdk = 27 targetSdk = 37 - versionCode = 59 - versionName = "1.7.0" + versionCode = 60 + versionName = "1.7.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - room { schemaDirectory("$projectDir/schemas") } - packaging { jniLibs { excludes.addAll(listOf("META-INF/proguard/*")) } resources { @@ -89,12 +88,6 @@ android { } buildTypes { - applicationVariants.forEach { variant -> - variant.outputs - .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } - .forEach { it.outputFileName = "${variant.name}-${variant.versionName}.apk" } - } - debug { versionNameSuffix = "-dev" applicationIdSuffix = ".debug" @@ -226,7 +219,15 @@ dependencies { androidTestImplementation(libs.test.rules) androidTestImplementation(libs.test.junit) androidTestImplementation(libs.test.truth) - testImplementation(libs.test.core) testImplementation(libs.junit) testImplementation(libs.robolectric) } + +androidComponents { + onVariants(selector().all()) { variant -> + variant.outputs.forEach { output -> + val versionName = output.versionName.orNull ?: "unspecified" + output.outputFileName.set("${variant.name}-$versionName.apk") + } + } +} diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt index 100e41ea5..cdc012654 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt @@ -16,6 +16,7 @@ import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds /** * A [ViewModel] subclass. It offers LiveData to be observed during the authentication process and @@ -55,7 +56,7 @@ constructor( val secretData = authRepository.getSecrets(credentials.deviceCode) if (secretData != null) secretLiveData.postEvent(SecretResult.Retrieved(secretData)) else { - delay(SECRET_CALLS_DELAY) + delay(SECRET_CALLS_DELAY.milliseconds) secretLiveData.postEvent(SecretResult.Empty) } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/DeleteDialogFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/DeleteDialogFragment.kt index 80e11da9f..bf4d99bfd 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/DeleteDialogFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/DeleteDialogFragment.kt @@ -2,7 +2,6 @@ package com.github.livingwithhippos.unchained.base import android.app.Dialog import android.os.Bundle -import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import com.github.livingwithhippos.unchained.R @@ -19,7 +18,11 @@ class DeleteDialogFragment : DialogFragment() { .setMessage(R.string.confirm_item_removal_description) .setTitle(title) .setPositiveButton(R.string.delete) { _, _ -> - setFragmentResult("deleteActionKey", bundleOf("deleteConfirmation" to true)) + + val bundle = Bundle().apply { + putBoolean("deleteConfirmation", true) + } + setFragmentResult("deleteActionKey", bundle) } .setNegativeButton(R.string.close) { dialog, _ -> dialog.cancel() } builder.create() diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/MainActivity.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/MainActivity.kt index f0b060ddc..57f08e4b8 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/MainActivity.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/MainActivity.kt @@ -15,7 +15,6 @@ import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Parcelable import android.view.Menu import android.view.MenuInflater import android.view.MenuItem @@ -60,6 +59,7 @@ import com.github.livingwithhippos.unchained.utilities.SIGNATURE import com.github.livingwithhippos.unchained.utilities.TelemetryManager import com.github.livingwithhippos.unchained.utilities.extension.downloadFileInStandardFolder import com.github.livingwithhippos.unchained.utilities.extension.openExternalWebPage +import com.github.livingwithhippos.unchained.utilities.extension.parcelable import com.github.livingwithhippos.unchained.utilities.extension.showToast import com.github.livingwithhippos.unchained.utilities.extension.toHex import com.google.android.material.bottomnavigation.BottomNavigationView @@ -72,6 +72,7 @@ import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds /** A [AppCompatActivity] subclass. Shared between all the fragments except for the preferences. */ @AndroidEntryPoint @@ -385,10 +386,8 @@ class MainActivity : AppCompatActivity() { }, ) - // monitor if the torrent notification service needs to be started. It monitor the - // preference - // change itself - // for the shutting down part + // monitor if the torrent notification service needs to be started. It monitors the + // preference change itself for the shutting down part preferences.registerOnSharedPreferenceChangeListener { sharedPreferences, key -> if (key == KEY_TORRENT_NOTIFICATIONS) { val enableTorrentNotifications = sharedPreferences.getBoolean(key, false) @@ -485,7 +484,7 @@ class MainActivity : AppCompatActivity() { when (viewModel.getDownloadManagerPreference()) { PreferenceKeys.DownloadManager.SYSTEM -> { val manager = - applicationContext.getSystemService(Context.DOWNLOAD_SERVICE) + applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager var downloadsStarted = 0 content.downloads.forEach { download -> @@ -525,7 +524,7 @@ class MainActivity : AppCompatActivity() { if (viewModel.getDownloadOnUnmeteredOnlyPreference()) { val connectivityManager = applicationContext.getSystemService( - Context.CONNECTIVITY_SERVICE + CONNECTIVITY_SERVICE ) as ConnectivityManager if (connectivityManager.isActiveNetworkMetered) { applicationContext.showToast( @@ -558,7 +557,7 @@ class MainActivity : AppCompatActivity() { PreferenceKeys.DownloadManager.SYSTEM -> { val manager = - applicationContext.getSystemService(Context.DOWNLOAD_SERVICE) + applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager val queuedDownload = @@ -592,7 +591,7 @@ class MainActivity : AppCompatActivity() { if (viewModel.getDownloadOnUnmeteredOnlyPreference()) { val connectivityManager = applicationContext.getSystemService( - Context.CONNECTIVITY_SERVICE + CONNECTIVITY_SERVICE ) as ConnectivityManager if (connectivityManager.isActiveNetworkMetered) { applicationContext.showToast( @@ -671,9 +670,8 @@ class MainActivity : AppCompatActivity() { } "*/*" -> { - // replace with intent.getParcelableExtra(Intent.EXTRA_STREAM, - // Uri::class.java) when stabilized - (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + val data: Uri? = intent.parcelable(Intent.EXTRA_STREAM) + data?.let { if ( it.lastPathSegment?.endsWith(TYPE_UNCHAINED, ignoreCase = true) == true @@ -759,7 +757,7 @@ class MainActivity : AppCompatActivity() { CurrentFSMAuthentication.Waiting -> { // auth may become ok, delay and continue loop - delay(100) + delay(100.milliseconds) } } } @@ -798,13 +796,12 @@ class MainActivity : AppCompatActivity() { private suspend fun doubleClickBottomItem(destinationID: Int) { val bottomNav = findViewById(R.id.bottom_nav_view) - // if the tab was already selected, a single tap will bring us back to the first fragment of - // its - // navigation xml. Otherwise, simulate another click after a delay + // if the tab was already selected, a single tap will bring us back to the first fragment + // of its navigation XML. Otherwise, simulate another click after a delay if (bottomNav.selectedItemId != destinationID) { bottomNav.selectedItemId = destinationID } - delay(100) + delay(100.milliseconds) bottomNav.selectedItemId = destinationID } @@ -844,7 +841,7 @@ class MainActivity : AppCompatActivity() { binding.bottomNavView.setupWithNavController(navController) - // Setup the ActionBar with navController and 3 top level destinations + // Set up the ActionBar with navController and 3 top level destinations // these won't show a back/up arrow appBarConfiguration = AppBarConfiguration( diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/customview/StatComponent.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/customview/StatComponent.kt index 30ab764b0..9c0750266 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/customview/StatComponent.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/customview/StatComponent.kt @@ -54,7 +54,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 adapter = StatAdapter(showLabel, showCaption, showIcon) recyclerView.adapter = adapter - val layoutManager: FlexboxLayoutManager = FlexboxLayoutManager(context) + val layoutManager = FlexboxLayoutManager(context) layoutManager.flexDirection = if (direction == 1) FlexDirection.COLUMN else FlexDirection.ROW layoutManager.justifyContent = JustifyContent.CENTER diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/AuthenticationRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/AuthenticationRepository.kt index 3c96f3b83..f2f2c5097 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/AuthenticationRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/AuthenticationRepository.kt @@ -11,7 +11,7 @@ import javax.inject.Inject class AuthenticationRepository @Inject -constructor(private val protoStore: ProtoStore, private val apiHelper: AuthApiHelper) : +constructor(protoStore: ProtoStore, private val apiHelper: AuthApiHelper) : BaseRepository(protoStore) { suspend fun getVerificationCode(): Authentication? { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt index 44c91fbc0..f82f87a27 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody +import kotlin.time.Duration.Companion.milliseconds @Singleton class UnrestrictRepository @@ -72,7 +73,7 @@ constructor(protoStore: ProtoStore, private val unrestrictApiHelper: UnrestrictA linksList.forEach { unrestrictedLinks.add(getEitherUnrestrictedLink(it, password, remote)) // just to be on the safe side... - delay(callDelay) + delay(callDelay.milliseconds) } return unrestrictedLinks } 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 bb600e6e3..d22a7ab81 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,7 @@ import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds const val MAX_SERVICE_DURATION = 5 * 60 * 60 * 1000 const val MIN_SERVICE_DURATION = 20 * 60 * 1000 @@ -189,7 +190,7 @@ class ForegroundTorrentService : LifecycleService() { // no valid token ready, retry later } // update notifications every 5 seconds - delay(updateTiming) + delay(updateTiming.milliseconds) } stopTorrentService() } @@ -293,7 +294,7 @@ class ForegroundTorrentService : LifecycleService() { private fun stopTorrentService() { lifecycleScope.launch { // delay used to let the notification finish - delay(1000) + delay(1000.milliseconds) notificationManager.cancel(SUMMARY_ID) // this will avoid removing the notifications, so the user can see what happened in the // meanwhile diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt index 012d442c9..83fdedb81 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt @@ -502,10 +502,8 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener { browserLayout.visibility = View.GONE } else { browserLayout.setOnClickListener { - { - context?.openExternalWebPage(RD_STREAMING_URL + args.details.id) - if (popup.isShowing) popup.dismiss() - } + context?.openExternalWebPage(RD_STREAMING_URL + args.details.id) + if (popup.isShowing) popup.dismiss() } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/viewmodel/FolderListViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/viewmodel/FolderListViewModel.kt index 0e1721bf6..53a9a4e7b 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/viewmodel/FolderListViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/viewmodel/FolderListViewModel.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class FolderListViewModel @@ -109,7 +110,7 @@ constructor( queryJob?.cancel() queryJob = viewModelScope.launch { - delay(500) + delay(500.milliseconds) if (isActive) queryLiveData.postValue(query?.trim() ?: "") } } 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 6b285d09b..f01062480 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 @@ -65,6 +65,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds /** * A simple [UnchainedFragment] subclass. It is capable of showing a list of both [DownloadItem] and @@ -119,7 +120,7 @@ class ListsTabFragment : UnchainedFragment() { queryJob?.cancel() queryJob = lifecycleScope.launch { - delay(500) + delay(500.milliseconds) if (isActive) viewModel.setListFilter(newText) } return true @@ -226,7 +227,7 @@ class ListsTabFragment : UnchainedFragment() { loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest ) { - delay(100) + delay(100.milliseconds) } if (controller.currentDestination?.id == R.id.list_tabs_dest) controller.navigate(action) @@ -262,7 +263,7 @@ class ListsTabFragment : UnchainedFragment() { loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest ) { - delay(100) + delay(100.milliseconds) } if (controller.currentDestination?.id == R.id.list_tabs_dest) controller.navigate(action) @@ -285,7 +286,7 @@ class ListsTabFragment : UnchainedFragment() { loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest ) { - delay(100) + delay(100.milliseconds) } if (controller.currentDestination?.id == R.id.list_tabs_dest) controller.navigate(action) @@ -293,6 +294,7 @@ class ListsTabFragment : UnchainedFragment() { } is ListEvent.SetTab -> { + if (_binding == null) return@EventObserver if (event.tab == DOWNLOADS_TAB) { if (binding.listPager.currentItem == TORRENTS_TAB) @@ -443,6 +445,7 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { object : SelectionTracker.SelectionObserver() { override fun onSelectionChanged() { super.onSelectionChanged() + if (_binding == null) return binding.cbSelectAll.text = downloadTracker.selection.size().toString() } } @@ -496,7 +499,10 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { } } - binding.srLayout.setOnRefreshListener { downloadAdapter.refresh() } + binding.srLayout.setOnRefreshListener { + if (_binding == null) return@setOnRefreshListener + downloadAdapter.refresh() + } context?.let { // theme the swipe refresh circle and arrow @@ -511,24 +517,21 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { // removes the loading icon from the swipe layout val downloadObserver = Observer> { + if (_binding == null) return@Observer 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 (b.srLayout.isRefreshing) { - b.srLayout.isRefreshing = false + if (binding.srLayout.isRefreshing) { + binding.srLayout.isRefreshing = false // scroll to top if we were refreshing lifecycleScope.launch { - b.rvDownloadList.delayedScrolling(requireContext()) + binding.rvDownloadList.delayedScrolling(requireContext()) } } // delay for notifying the list that the items have changed, otherwise stuff // like the // status and the progress are not updated until you scroll away and back there - delay(300) + delay(300.milliseconds) downloadAdapter.notifyDataSetChanged() } } @@ -571,7 +574,7 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { when (it) { ListState.UpdateDownload -> { lifecycleScope.launch { - delay(300L) + delay(300.milliseconds) downloadAdapter.refresh() lifecycleScope.launch { binding.rvDownloadList.delayedScrolling(requireContext()) @@ -675,6 +678,7 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { object : SelectionTracker.SelectionObserver() { override fun onSelectionChanged() { super.onSelectionChanged() + if (_binding == null) return binding.cbSelectAll.text = torrentTracker.selection.size().toString() if (torrentTracker.selection.size() == 1) { binding.bDetailsSelected.visibility = View.VISIBLE @@ -737,19 +741,24 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { } } - binding.srLayout.setOnRefreshListener { torrentAdapter.refresh() } + binding.srLayout.setOnRefreshListener { + if (_binding == null) return@setOnRefreshListener + torrentAdapter.refresh() + } val torrentObserver = Observer> { viewLifecycleOwner.lifecycleScope.launch { - val b = _binding ?: return@launch + if (_binding == null) return@launch torrentAdapter.submitData(it) - if (b.srLayout.isRefreshing) { - b.srLayout.isRefreshing = false - lifecycleScope.launch { b.rvTorrentList.delayedScrolling(requireContext()) } + if (binding.srLayout.isRefreshing) { + binding.srLayout.isRefreshing = false + lifecycleScope.launch { + binding.rvTorrentList.delayedScrolling(requireContext()) + } } - delay(300) + delay(300.milliseconds) torrentAdapter.notifyDataSetChanged() } } @@ -813,7 +822,7 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { when (it) { ListState.UpdateTorrent -> { lifecycleScope.launch { - delay(300L) + delay(300.milliseconds) torrentAdapter.refresh() lifecycleScope.launch { binding.rvTorrentList.delayedScrolling(requireContext()) @@ -843,7 +852,7 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { lifecycleScope.launch { val controller = findNavController() while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { - delay(100) + delay(100.milliseconds) } if (item.status == "downloaded" || item.status == "ready") { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt index af5719ee0..f91468e26 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt @@ -44,9 +44,6 @@ constructor( private val unrestrictRepository: UnrestrictRepository, ) : ViewModel() { - private val MAX_PAGE_SIZE = 2500 - private val INITIAL_LOAD = 100 - // stores the last query value private val queryLiveData = MutableLiveData() @@ -191,6 +188,10 @@ constructor( const val DOWNLOADS_DELETED = -2 const val DOWNLOADS_DELETED_ALL = -3 const val DOWNLOAD_NOT_DELETED = -4 + + private const val MAX_PAGE_SIZE = 2500 + + private const val INITIAL_LOAD = 100 } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt index 315376f04..57e815e7c 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt @@ -48,6 +48,7 @@ import java.io.IOException import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds /** * A simple [UnchainedFragment] subclass. Allow the user to create a new download from a link or a @@ -251,7 +252,7 @@ class NewDownloadFragment : UnchainedFragment() { lifecycleScope.launch { currentToast.cancel() // if we call this too soon between toasts we'll miss some - if (System.currentTimeMillis() - lastToastTime < 1000L) delay(1000) + if (System.currentTimeMillis() - lastToastTime < 1000L) delay(1000.milliseconds) currentToast.setText(it) currentToast.show() lastToastTime = System.currentTimeMillis() diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/plugins/Parser.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/plugins/Parser.kt index 5f8583cfd..7ff5b8f6d 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/plugins/Parser.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/plugins/Parser.kt @@ -27,6 +27,7 @@ import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.jsoup.select.Elements import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds sealed class WebResponse { data class Success(val source: String) : WebResponse() @@ -111,7 +112,7 @@ class Parser( emit(ParserResult.SearchStarted(innerSource.size)) if (innerSource.isNotEmpty()) { for (link in innerSource) { - delay(50) + delay(50.milliseconds) // parse every page linked to the results val s = getSource(link) if (s.isNotBlank()) { @@ -172,7 +173,7 @@ class Parser( ) emit(ParserResult.SearchStarted(links.size)) links.forEach { - delay(50) + delay(50.milliseconds) val itemSource = getSource(it) if (itemSource.isNotBlank()) { val scrapedItem = @@ -273,7 +274,7 @@ class Parser( regexes.nameRegex.regexps.forEach { val parsedName = cleanName(parseSingle(it, source, baseUrl) ?: "") // this is a single string, no need to check for regexUse - if (!parsedName.isNullOrBlank()) { + if (parsedName.isNotBlank()) { name = parsedName return@forEach } @@ -781,7 +782,7 @@ class Parser( val containerClass: Element? = if (parser.className != null) doc.getElementsByClass(parser.className).firstOrNull() else if (parser.idName != null) doc.getElementById(parser.idName) else null - val entries: Elements = Elements() + val entries = Elements() if (containerClass != null) { if (parser.entryClass != null) entries.addAll(containerClass.getElementsByClass(parser.entryClass)) @@ -858,7 +859,7 @@ class Parser( .trim() // replace newlines anf multiple spaces with single space .replace("[\\t\\n\\r\\s]+".toRegex(), " ") - // remove all html tags from the name. Will replace anything like <*> if it's in the + // remove all HTML tags from the name. Will replace anything like <*> if it's in the // name .replace("<[^>]+>".toRegex(), "") } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceListAdapter.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceListAdapter.kt index 3555774e8..006763d41 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceListAdapter.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceListAdapter.kt @@ -78,8 +78,8 @@ class RemoteDeviceViewHolder( binding.cvDevice.setOnClickListener { listener.onDeviceClick(item) } } - fun getItemDetails(): ItemDetailsLookup.ItemDetails = - object : ItemDetailsLookup.ItemDetails() { + fun getItemDetails(): ItemDetails = + object : ItemDetails() { override fun getPosition(): Int = layoutPosition override fun getSelectionKey(): RemoteDevice? = mItem diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt index 1cfab82b8..ebf9a0ca1 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.json.JSONException import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class RepositoryViewModel @@ -249,7 +250,7 @@ constructor( fun addRepository(url: String) { viewModelScope.launch { databasePluginsRepository.addRepositoryUrl(url) - delay(100) + delay(100.milliseconds) // used to update on the main screen the repo checkCurrentRepositories() } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/viewmodel/SearchViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/viewmodel/SearchViewModel.kt index 8ff0e5071..44743286f 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/viewmodel/SearchViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/viewmodel/SearchViewModel.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class SearchViewModel @@ -262,7 +263,7 @@ constructor( } parsingLiveData.value = ParserResult.SearchStarted(-1) - delay(100) + delay(100.milliseconds) supervisorScope { // accumulate results from all plugin/service searches so they survive fragment diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt index 3c503e401..3c5dd7415 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt @@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds /** * a [ViewModel] subclass. Shared between the fragments to observe the authentication status and @@ -555,7 +556,7 @@ constructor( } } 9 -> { - Timber.e("onParseCallFailure " + 9) + Timber.e("onParseCallFailure 9") // 9 is permission denied which should mean the token is not valid at all // and should be // discarded @@ -640,7 +641,7 @@ constructor( refreshJob?.cancel() refreshJob = viewModelScope.launch { // secondsDelay*950L -> expiration time - 5% - delay(secondsDelay * 950L) + delay((secondsDelay * 950L).milliseconds) if (isActive) refreshToken() } } @@ -998,7 +999,7 @@ constructor( fun requireNotificationPermissions(callDelay: Boolean = true) { viewModelScope.launch { if (callDelay) { - delay(500) + delay(500.milliseconds) } messageLiveData.postValue(Event(MainActivityMessage.RequireNotificationPermissions)) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/viewmodel/TorrentDetailsViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/viewmodel/TorrentDetailsViewModel.kt index 36c985eb1..4595e1cc0 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/viewmodel/TorrentDetailsViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/viewmodel/TorrentDetailsViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds /** a [ViewModel] subclass. Retrieves a torrent's details */ @HiltViewModel @@ -59,7 +60,7 @@ constructor( if (torrentData != null) torrentLiveData.postEvent(torrentData) if (endedStatusList.contains(torrentData?.status)) job.cancelIfActive() - delay(2000) + delay(2000.milliseconds) } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentfilepicker/viewmodel/TorrentProcessingViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentfilepicker/viewmodel/TorrentProcessingViewModel.kt index 7109d6c9a..1dcc327ff 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentfilepicker/viewmodel/TorrentProcessingViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentfilepicker/viewmodel/TorrentProcessingViewModel.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class TorrentProcessingViewModel @@ -141,7 +142,7 @@ constructor( } } } - delay(1500) + delay(1500.milliseconds) } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt index 7b287b397..ba3de2ea5 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt @@ -65,6 +65,7 @@ class UserProfileFragment : UnchainedFragment() { } activityViewModel.userLiveData.observe(viewLifecycleOwner) { + if (_binding == null) return@observe populateUserView(it.peekContent()) lifecycleScope.launch { if (activityViewModel.isTokenPrivate()) { @@ -159,6 +160,7 @@ class UserProfileFragment : UnchainedFragment() { } fun populateUserView(user: User?) { + if (_binding == null) return user?.let { binding.tvName.text = it.username binding.tvMail.text = it.email diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt index 381af670f..280165aba 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt @@ -18,7 +18,10 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle import android.os.Environment +import android.os.Parcelable import android.os.VibrationEffect import android.os.Vibrator import android.provider.OpenableColumns @@ -32,10 +35,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import androidx.core.net.toUri import androidx.fragment.app.Fragment -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.settings.view.SettingsFragment.Companion.THEME_AUTO import com.github.livingwithhippos.unchained.settings.view.SettingsFragment.Companion.THEME_DAY @@ -147,8 +146,8 @@ fun Context.showToast(stringResource: Int, length: Int = Toast.LENGTH_SHORT) = /** * Show a toast message * - * @param message: the message and shown - * @param length: the duration of the toast. Defaults to short + * @param message the message and shown + * @param length the duration of the toast. Defaults to short */ fun Context.showToast(message: String, length: Int = Toast.LENGTH_SHORT) { Toast.makeText(this, message, length).show() @@ -252,35 +251,6 @@ fun DownloadManager.downloadFileInStandardFolder( } } -fun DownloadManager.downloadFileInCustomFolder( - source: Uri, - title: String, - description: String, - fileName: String = title, - folder: Uri, -): EitherResult { - // https://issuetracker.google.com/issues/134858946 - val destination = Uri.withAppendedPath(folder, fileName) - Timber.e(destination.toString()) - val request: DownloadManager.Request = - DownloadManager.Request(source) - .setTitle(title) - .setDescription(description) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationUri(destination) - - return try { - val downloadID = this.enqueue(request) - EitherResult.Success(downloadID) - } catch (e: Exception) { - Timber.e( - "Error starting download in custom folder $destination of ${source.path}, exception ${e.message}" - ) - e.printStackTrace() - EitherResult.Failure(e) - } -} - /** * Return the Uri from a downloaded file id returned by the download manager * @@ -364,53 +334,6 @@ fun Activity.getUpdatedLocaleContext(context: Context, language: String): Contex return context.createConfigurationContext(configuration) } -fun LiveData.combineWith(liveData: LiveData, block: (T?, K?) -> R): LiveData { - val result = MediatorLiveData() - result.addSource(this) { result.value = block(this.value, liveData.value) } - result.addSource(liveData) { result.value = block(this.value, liveData.value) } - return result -} - -fun zipLiveData(t: LiveData, k: LiveData): LiveData> { - return MediatorLiveData>().apply { - var lastT: T? = null - var lastK: K? = null - - fun update() { - val localLastT = lastT - val localLastK = lastK - if (localLastT != null && localLastK != null) this.value = Pair(localLastT, localLastK) - } - - addSource(t) { - lastT = it - update() - } - addSource(k) { - lastK = it - update() - } - } -} - -fun LiveData.observeOnce( - lifecycleOwner: LifecycleOwner, - observer: Observer, - untilNotNull: Boolean = false, -) { - observe( - lifecycleOwner, - object : Observer { - override fun onChanged(value: T) { - observer.onChanged(value) - if (untilNotNull) { - if (value != null) removeObserver(this) - } else removeObserver(this) - } - }, - ) -} - fun AppCompatActivity.setNavigationBarColor(color: Int, alpha: Int = 0) { val newColor = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) @@ -420,7 +343,7 @@ fun AppCompatActivity.setNavigationBarColor(color: Int, alpha: Int = 0) { val luminance = Color.luminance(color) if (luminance >= 0.25) { when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + SDK_INT >= Build.VERSION_CODES.R -> { window.insetsController?.setSystemBarsAppearance( WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, @@ -520,3 +443,13 @@ fun AssetManager.smartList(path: String): Array? { } fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } + +inline fun Intent.parcelable(key: String): T? = when { + SDK_INT >= 34 -> getParcelableExtra(key, T::class.java) + else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T +} + +inline fun Bundle.parcelable(key: String): T? = when { + SDK_INT >= 34 -> getParcelable(key, T::class.java) + else -> @Suppress("DEPRECATION") getParcelable(key) as? T +} diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/ViewExtension.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/ViewExtension.kt index 5ccf2b4b0..1bbcb04ac 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/ViewExtension.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/ViewExtension.kt @@ -25,6 +25,7 @@ import com.github.livingwithhippos.unchained.data.local.RemoteServiceType import com.github.livingwithhippos.unchained.repository.model.PluginStatus import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds /** * tint the color of a [ProgressBar] layer drawable @@ -140,7 +141,7 @@ fun RecyclerView.LayoutManager.verticalScrollToPosition( */ suspend fun RecyclerView.delayedScrolling(context: Context, delay: Long = 300) { this.layoutManager?.let { - delay(delay) + delay(delay.milliseconds) it.verticalScrollToPosition(context) } } diff --git a/app/app/src/main/res/drawable/download_progressbar.xml b/app/app/src/main/res/drawable/download_progressbar.xml deleted file mode 100644 index 961828f77..000000000 --- a/app/app/src/main/res/drawable/download_progressbar.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/app/src/main/res/layout/dialog_kodi_management.xml b/app/app/src/main/res/layout/dialog_kodi_management.xml deleted file mode 100644 index 58b1c4420..000000000 --- a/app/app/src/main/res/layout/dialog_kodi_management.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - -