Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd98065766 | ||
|
|
4d68e3d31e | ||
|
|
9c33bb34ff | ||
|
|
9f7891f129 | ||
|
|
11e5b9526d | ||
|
|
a0fb27491c | ||
|
|
b1babfe477 | ||
|
|
ed3b92df08 |
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user