Compare commits

...

8 Commits

Author SHA1 Message Date
renovate[bot]
bd98065766 Update dependency org.jellyfin.sdk:jellyfin-core to v1.3.5
(cherry picked from commit 1cafe6c539)
2022-08-20 10:31:03 +02:00
Niels van Velzen
4d68e3d31e Properly handle specials in item naming
(cherry picked from commit 14e7391bc2)
2022-08-20 10:30:53 +02:00
Niels van Velzen
9c33bb34ff Don't open library settings for unsupported types
Only collection types that use the grid should open the DisplayPreferencesScreen. Added a special case to open the Live TV (guide) settings as a bonus.

(cherry picked from commit 5bb1ee7819)
2022-08-20 10:30:41 +02:00
Niels van Velzen
9f7891f129 Fix ActivityLifecycleCallback calls on Android 9 and lower
(cherry picked from commit cd33bea725)
2022-08-20 10:30:32 +02:00
Niels van Velzen
11e5b9526d Migrate DefaultLifecycleObserver setup to androidx.startup
(cherry picked from commit a404b99ab1)
2022-08-20 10:30:13 +02:00
Niels van Velzen
a0fb27491c Allow app installation on external storage
(cherry picked from commit e592017602)
2022-08-20 10:30:04 +02:00
Niels van Velzen
b1babfe477 Fix alignment of ClockUserView in live TV guide
(cherry picked from commit 6960f808f2)
2022-08-20 10:29:54 +02:00
Niels van Velzen
ed3b92df08 Catch decoding exception in AuthenticationStore
Fixes corrupt JSON causing the app to crash

(cherry picked from commit 0074062633)
2022-08-20 10:29:46 +02:00
12 changed files with 147 additions and 53 deletions

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.jellyfin.androidtv">
package="org.jellyfin.androidtv"
android:installLocation="auto">
<!-- Android TV Integration -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
@@ -88,6 +89,9 @@
<meta-data
android:name="org.jellyfin.androidtv.SessionInitializer"
android:value="androidx.startup" />
<meta-data
android:name="org.jellyfin.androidtv.ProcessLifecycleInitializer"
android:value="androidx.startup" />
</provider>
<provider

View File

@@ -2,8 +2,6 @@ package org.jellyfin.androidtv
import android.app.Application
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.work.BackoffPolicy
@@ -15,16 +13,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ACRA
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.data.eventhandling.SocketHandler
import org.jellyfin.androidtv.data.repository.NotificationsRepository
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.telemetry.TelemetryService
import org.jellyfin.androidtv.util.AutoBitrate
import org.koin.android.ext.android.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.TimeUnit
@Suppress("unused")
@@ -37,28 +31,6 @@ class JellyfinApplication : Application() {
val notificationsRepository by inject<NotificationsRepository>()
notificationsRepository.addDefaultNotifications()
// Register application lifecycle events
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
/**
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
*/
override fun onCreate(owner: LifecycleOwner) {
// Register activity lifecycle callbacks
getKoin().getAll<ActivityLifecycleCallbacks>().forEach(::registerActivityLifecycleCallbacks)
}
/**
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
*/
override fun onStart(owner: LifecycleOwner) {
Timber.i("Process lifecycle started")
owner.lifecycleScope.launch {
get<SessionRepository>().restoreSession()
}
}
})
}
/**

View File

@@ -0,0 +1,50 @@
package org.jellyfin.androidtv
import android.app.Application
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.di.KoinInitializer
import timber.log.Timber
@Suppress("unused")
class ProcessLifecycleInitializer : Initializer<Unit> {
override fun create(context: Context) {
val koin = AppInitializer.getInstance(context)
.initializeComponent(KoinInitializer::class.java)
.koin
// Register application lifecycle events
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
/**
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
*/
override fun onCreate(owner: LifecycleOwner) {
// Register activity lifecycle callbacks
val callbacks = koin.getAll<Application.ActivityLifecycleCallbacks>()
Timber.i("Registering ${callbacks.size} ActivityLifecycleCallbacks")
val app = context.applicationContext as Application
callbacks.forEach { callback -> app.registerActivityLifecycleCallbacks(callback) }
}
/**
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
*/
override fun onStart(owner: LifecycleOwner) {
Timber.i("Process lifecycle started")
owner.lifecycleScope.launch {
koin.get<SessionRepository>().restoreSession()
}
}
})
}
override fun dependencies() = listOf(KoinInitializer::class.java)
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.auth.store
import android.content.Context
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -47,8 +48,15 @@ class AuthenticationStore(
// No store found
if (!storePath.exists()) return emptyMap()
// Parse JSON document
val root = try {
json.parseToJsonElement(storePath.readText()).jsonObject
} catch (e: SerializationException) {
Timber.e(e, "Unable to read JSON")
JsonObject(emptyMap())
}
// Check for version
val root = json.parseToJsonElement(storePath.readText()).jsonObject
return when (root["version"]?.jsonPrimitive?.intOrNull) {
1 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
null -> {

View File

@@ -12,6 +12,7 @@ interface UserViewsRepository {
fun isSupported(collectionType: String?): Boolean
fun allowViewSelection(collectionType: String?): Boolean
fun allowGridView(collectionType: String?): Boolean
}
class UserViewsRepositoryImpl(
@@ -26,7 +27,8 @@ class UserViewsRepositoryImpl(
}
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
override fun allowViewSelection(collectionType: String?) = collectionType != CollectionType.Music
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
private companion object {
private val unsupportedCollectionTypes = arrayOf(
@@ -34,5 +36,16 @@ class UserViewsRepositoryImpl(
CollectionType.Games,
CollectionType.Folders
)
private val disallowViewSelectionCollectionTypes = arrayOf(
CollectionType.livetv,
CollectionType.Music,
CollectionType.Photos,
)
private val disallowGridViewCollectionTypes = arrayOf(
CollectionType.livetv,
CollectionType.Music
)
}
}

View File

@@ -9,9 +9,11 @@ import kotlinx.coroutines.launch
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.repository.UserViewsRepository
import org.jellyfin.androidtv.ui.browsing.DisplayPreferencesScreen
import org.jellyfin.androidtv.ui.livetv.GuideOptionsScreen
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.link
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.apiclient.model.entities.CollectionType
import org.koin.android.ext.android.inject
class LibrariesPreferencesScreen : OptionsFragment() {
@@ -37,11 +39,20 @@ class LibrariesPreferencesScreen : OptionsFragment() {
link {
title = it.name
icon = R.drawable.ic_folder
withFragment<DisplayPreferencesScreen>(bundleOf(
DisplayPreferencesScreen.ARG_ALLOW_VIEW_SELECTION to allowViewSelection,
DisplayPreferencesScreen.ARG_PREFERENCES_ID to it.displayPreferencesId,
))
if (userViewsRepository.allowGridView(it.collectionType)) {
icon = R.drawable.ic_folder
withFragment<DisplayPreferencesScreen>(bundleOf(
DisplayPreferencesScreen.ARG_ALLOW_VIEW_SELECTION to allowViewSelection,
DisplayPreferencesScreen.ARG_PREFERENCES_ID to it.displayPreferencesId,
))
} else if (it.collectionType == CollectionType.livetv) {
icon = R.drawable.ic_guide
withFragment<GuideOptionsScreen>()
} else {
icon = R.drawable.ic_folder
enabled = false
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.ui.shared
import android.app.Activity
import android.os.Build
import android.os.Bundle
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AppTheme
@@ -14,22 +15,38 @@ class AppThemeCallbacks(
private var lastPreferencesTheme: AppTheme? = null
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.applyThemeOnCreated()
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.applyThemeOnCreated()
}
override fun onActivityPreResumed(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.applyThemeOnResume()
}
override fun onActivityResumed(activity: Activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.applyThemeOnResume()
}
private fun Activity.applyThemeOnCreated() {
userPreferences[UserPreferences.appTheme].let {
Timber.i("Applying theme: %s", it)
activity.setTheme(ThemeManager.getTheme(activity, it))
when (activity) {
setTheme(ThemeManager.getTheme(this, it))
when (this) {
is PreferencesActivity -> lastPreferencesTheme = it
else -> lastTheme = it
}
}
}
override fun onActivityPreResumed(activity: Activity) {
val lastThemeForActivity = if (activity is PreferencesActivity) lastPreferencesTheme else lastTheme
private fun Activity.applyThemeOnResume() {
val lastThemeForActivity = if (this is PreferencesActivity) lastPreferencesTheme else lastTheme
userPreferences[UserPreferences.appTheme].let {
if (lastThemeForActivity != null && lastThemeForActivity != it) {
Timber.i("Recreating activity to apply new theme: %s -> %s", lastThemeForActivity, it)
activity.recreate()
recreate()
}
}
}

View File

@@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui.shared
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
@@ -26,14 +27,22 @@ class AuthenticatedUserCallbacks(
}
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
val name = activity::class.qualifiedName
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.checkAuthentication()
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.checkAuthentication()
}
private fun Activity.checkAuthentication() {
val name = this::class.qualifiedName
if (name in ignoredClassNames) {
Timber.i("Activity $name is ignored")
} else if (sessionRepository.currentSession.value == null) {
Timber.w("Activity $name started without a session, bouncing to StartupActivity")
activity.startActivity(Intent(activity, StartupActivity::class.java))
activity.finish()
startActivity(Intent(this, StartupActivity::class.java))
finish()
}
}
}

View File

@@ -36,14 +36,22 @@ fun BaseItemDto?.canPlay() = this != null
&& (!isFolderItem || childCount == null || childCount > 0)
fun BaseItemDto.getFullName(context: Context): String? = when (baseItemType) {
BaseItemType.Episode -> listOfNotNull(
seriesName,
parentIndexNumber?.let { context.getString(R.string.lbl_season_number, it) },
indexNumber?.let { start ->
indexNumberEnd?.let { end -> context.getString(R.string.lbl_episode_range, start, end) }
?: context.getString(R.string.lbl_episode_number, start)
BaseItemType.Episode -> buildList {
add(seriesName)
if (parentIndexNumber == 0) {
add(context.getString(R.string.episode_name_special))
} else {
if (parentIndexNumber != null)
add(context.getString(R.string.lbl_season_number, parentIndexNumber))
if (indexNumber != null && indexNumberEnd != null)
add(context.getString(R.string.lbl_episode_range, indexNumber, indexNumberEnd))
else if (indexNumber != null)
add(context.getString(R.string.lbl_episode_number, indexNumber))
}
).filter { it.isNotEmpty() }.joinToString(" ")
}.filterNot { it.isNullOrBlank() }.joinToString(" ")
// we actually want the artist name if available
BaseItemType.Audio,
BaseItemType.MusicAlbum -> listOfNotNull(albumArtist, name)

View File

@@ -92,7 +92,7 @@
android:layout_height="150sp"
android:id="@+id/programImage"
android:layout_gravity="left|top"
android:layout_margin="15sp" />
android:layout_margin="20sp" />
<TextView
android:layout_width="wrap_content"

View File

@@ -495,4 +495,6 @@
<string name="pref_crash_report_logs_disabled">Logs will not be included in crash reports</string>
<string name="crash_report_toast">Oops! Something went wrong, a crash report was sent to your Jellyfin server.</string>
<string name="server_setup_incomplete">The setup of this server has not been completed. Open Jellyfin in a web browser to finish setup before signing in.</string>
<string name="enable_picture_viewer_title">Enable new picture viewer</string>
<string name="episode_name_special">special</string>
</resources>

View File

@@ -24,7 +24,7 @@ glide = "4.13.2"
gson = "2.8.9"
jellyfin-apiclient = "v0.7.10"
jellyfin-exoplayer-ffmpegextension = "2.18.1+1"
jellyfin-sdk = "1.3.4"
jellyfin-sdk = "1.3.5"
junit = "4.13.2"
kenburnsview = "1.0.7"
koin = "3.2.0"