Commit 30c19aac authored by Geoffrey Métais's avatar Geoffrey Métais
Browse files

TV Module

parent 1e526c9e
Pipeline #12812 passed with stage
in 5 minutes and 11 seconds
......@@ -130,4 +130,5 @@ android {
dependencies {
implementation project(':vlc-android')
implementation project(':television')
}
......@@ -3,7 +3,7 @@
package="org.videolan.mobile.app">
<application
android:name="org.videolan.vlc.VLCApplication"
android:name=".VLCApplication"
android:allowBackup="true"
android:appCategory="video"
android:banner="@drawable/banner"
......
/*****************************************************************************
* VLCApplication.java
* AppSetupDelegate.ki
*
* Copyright © 2010-2013 VLC authors and VideoLAN
* Copyright © 2020 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -17,21 +17,14 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*/
package org.videolan.vlc
package org.videolan.mobile.app
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.ContextWrapper
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.launch
import org.videolan.libvlc.Dialog
import org.videolan.libvlc.FactoryManager
......@@ -40,8 +33,12 @@ import org.videolan.libvlc.MediaFactory
import org.videolan.libvlc.interfaces.ILibVLCFactory
import org.videolan.libvlc.interfaces.IMediaFactory
import org.videolan.libvlc.util.AndroidUtil
import org.videolan.moviepedia.MoviepediaIndexer
import org.videolan.moviepedia.provider.MoviepediaTvshowProvider
import org.videolan.resources.AppContextProvider
import org.videolan.tools.*
import org.videolan.tools.AppScope
import org.videolan.tools.Settings
import org.videolan.vlc.BuildConfig
import org.videolan.vlc.gui.SendCrashActivity
import org.videolan.vlc.gui.helpers.AudioUtil
import org.videolan.vlc.gui.helpers.NotificationHelper
......@@ -49,82 +46,52 @@ import org.videolan.vlc.util.DialogDelegate
import org.videolan.vlc.util.SettingsMigration
import org.videolan.vlc.util.VLCInstance
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class VLCApplication : MultiDexApplication(), Dialog.Callbacks by DialogDelegate {
interface AppDelegate {
val appContextProvider : AppContextProvider
fun Context.setupApplication()
}
class AppSetupDelegate : AppDelegate {
init {
AppContextProvider.init(this)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
// Store AppContextProvider to prevent GC
override val appContextProvider = AppContextProvider
@TargetApi(Build.VERSION_CODES.O)
override fun Context.setupApplication() {
if (AndroidUtil.isOOrLater) NotificationHelper.createNotificationChannels(this)
appContextProvider.init(this)
// Service loaders
FactoryManager.registerFactory(IMediaFactory.factoryId, MediaFactory())
FactoryManager.registerFactory(ILibVLCFactory.factoryId, LibVLCFactory())
}
@TargetApi(Build.VERSION_CODES.O)
override fun onCreate() {
super.onCreate()
// Register movipedia to resume tv shows/movies
MoviepediaTvshowProvider.getProviders().forEach {
appContextProvider.mediaContentResolvers.put(it.first, it.second)
}
// Setup Moviepedia indexing after Medialibrary scan
(AppContextProvider.indexingListeners as MutableList).add(MoviepediaIndexer.indexListener)
//Initiate Kotlinx Dispatchers in a thread to prevent ANR
backgroundInit()
}
// init operations executed in background threads
private fun Context.backgroundInit() {
Thread(Runnable {
locale = Settings.getInstance(this).getString("set_locale", "")
locale.takeIf { !it.isNullOrEmpty() }?.let {
AppContextProvider.init(ContextWrapper(this).wrap(it))
}
AppContextProvider.setLocale(Settings.getInstance(this).getString("set_locale", ""))
AppScope.launch(Dispatchers.IO) {
// Prepare cache folder constants
AudioUtil.prepareCacheFolder(AppContextProvider.appContext)
AudioUtil.prepareCacheFolder(this@backgroundInit)
if (!VLCInstance.testCompatibleCPU(AppContextProvider.appContext)) return@launch
Dialog.setCallbacks(VLCInstance[AppContextProvider.appContext], DialogDelegate)
Dialog.setCallbacks(VLCInstance[this@backgroundInit], DialogDelegate)
}
packageManager.setComponentEnabledSetting(ComponentName(this, SendCrashActivity::class.java),
if (BuildConfig.BETA) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
SettingsMigration.migrateSettings(this)
}).start()
if (AndroidUtil.isOOrLater)
NotificationHelper.createNotificationChannels(this@VLCApplication)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
locale.takeIf { !it.isNullOrEmpty() }?.let {
AppContextProvider.init(ContextWrapper(this).wrap(it))
}
}
/**
* Called when the overall system is running low on memory
*/
override fun onLowMemory() {
super.onLowMemory()
Log.w(TAG, "System is running low on memory")
BitmapCache.clear()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.w(TAG, "onTrimMemory, level: $level")
BitmapCache.clear()
}
companion object {
private const val TAG = "VLC/VLCApplication"
const val ACTION_MEDIALIBRARY_READY = "VLC/VLCApplication"
// Property to get the new locale only on restart to prevent change the locale partially on runtime
var locale: String? = ""
private set
/**
* Check if application is currently displayed
* @return true if an activity is displayed, false if app is in background.
*/
val isForeground: Boolean
get() = ProcessLifecycleOwner.get().isStarted()
}
}
}
\ No newline at end of file
/*****************************************************************************
* VLCApplication.ki
*
* Copyright © 2010-2020 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*/
package org.videolan.mobile.app
import android.annotation.TargetApi
import android.content.res.Configuration
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.videolan.libvlc.Dialog
import org.videolan.tools.BitmapCache
import org.videolan.vlc.util.DialogDelegate
private const val TAG = "VLC/VLCApplication"
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class VLCApplication : MultiDexApplication(), Dialog.Callbacks by DialogDelegate, AppDelegate by AppSetupDelegate() {
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
@TargetApi(Build.VERSION_CODES.O)
override fun onCreate() {
setupApplication()
super.onCreate()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
appContextProvider.updateContext()
}
/**
* Called when the overall system is running low on memory
*/
override fun onLowMemory() {
super.onLowMemory()
Log.w(TAG, "System is running low on memory")
BitmapCache.clear()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.w(TAG, "onTrimMemory, level: $level")
BitmapCache.clear()
}
}
......@@ -41,7 +41,7 @@ ext {
androidxPreferencesVersion = '1.1.0'
androidxVersion = '1.1.0'
androidxActivityVersion = '1.1.0-rc03'
androidxFragmentVersion = '1.2.0-rc04'
androidxFragmentVersion = '1.2.0-rc05'
androidxAnnotationVersion = '1.1.0'
androidxAppcompatVersion = '1.1.0'
androidxRecyclerviewVersion = '1.1.0'
......
......@@ -6,6 +6,10 @@ apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
dataBinding {
enabled = true
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
......@@ -35,8 +39,7 @@ android {
dependencies {
implementation project(':tools')
debugImplementation "org.videolan.android:medialibrary-all:$rootProject.ext.medialibraryVersion"
devImplementation project(':medialibrary')
implementation project(':vlc-android')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.ext.kotlinx_version"
......
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.videolan.moviepedia" />
package="org.videolan.moviepedia" >
<application>
<activity
android:name="org.videolan.moviepedia.ui.MoviepediaActivity"
android:theme="@style/Theme.VLC" />
</application>
</manifest>
package org.videolan.moviepedia
import android.content.Context
import org.videolan.medialibrary.interfaces.Medialibrary
import org.videolan.moviepedia.database.models.MediaMetadata
import org.videolan.moviepedia.database.models.getYear
fun getHeaderMoviepedia(context: Context?, sort: Int, item: MediaMetadata?, aboveItem: MediaMetadata?) = if (context !== null && item != null) when (sort) {
Medialibrary.SORT_DEFAULT,
Medialibrary.SORT_FILENAME,
Medialibrary.SORT_ALPHA -> {
val letter = if (item.title.isEmpty() || !Character.isLetter(item.title[0])) "#" else item.title.substring(0, 1).toUpperCase()
if (aboveItem == null) letter
else {
val previous = if (aboveItem.title.isEmpty() || !Character.isLetter(aboveItem.title[0])) "#" else aboveItem.title.substring(0, 1).toUpperCase()
letter.takeIf { it != previous }
}
}
// SORT_DURATION -> {
// val length = item.getLength()
// val lengthCategory = length.lengthToCategory()
// if (aboveItem == null) lengthCategory
// else {
// val previous = aboveItem.getLength().lengthToCategory()
// lengthCategory.takeIf { it != previous }
// }
// }
Medialibrary.SORT_RELEASEDATE -> {
val year = item.getYear()
if (aboveItem == null) year
else {
val previous = aboveItem.getYear()
year.takeIf { it != previous }
}
}
// SORT_LASTMODIFICATIONDATE -> {
// val timestamp = (item as MediaWrapper).lastModified
// val category = getTimeCategory(timestamp)
// if (aboveItem == null) getTimeCategoryString(context, category)
// else {
// val prevCat = getTimeCategory((aboveItem as MediaWrapper).lastModified)
// if (prevCat != category) getTimeCategoryString(context, category) else null
// }
// }
// SORT_ARTIST -> {
// val artist = (item as MediaWrapper).artist ?: ""
// if (aboveItem == null) artist
// else {
// val previous = (aboveItem as MediaWrapper).artist ?: ""
// artist.takeIf { it != previous }
// }
// }
// SORT_ALBUM -> {
// val album = (item as MediaWrapper).album ?: ""
// if (aboveItem == null) album
// else {
// val previous = (aboveItem as MediaWrapper).album ?: ""
// album.takeIf { it != previous }
// }
// }
else -> null
} else null
\ No newline at end of file
......@@ -26,9 +26,7 @@ package org.videolan.moviepedia
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.videolan.medialibrary.interfaces.Medialibrary
import org.videolan.medialibrary.interfaces.media.MediaWrapper
......@@ -39,11 +37,14 @@ import org.videolan.moviepedia.repository.MediaMetadataRepository
import org.videolan.moviepedia.repository.MediaPersonRepository
import org.videolan.moviepedia.repository.MoviepediaApiRepository
import org.videolan.moviepedia.repository.PersonRepository
import org.videolan.resources.AppContextProvider
import org.videolan.resources.interfaces.IndexingListener
import org.videolan.tools.AppScope
import org.videolan.tools.getLocaleLanguages
object MoviepediaIndexer : CoroutineScope by MainScope() {
object MoviepediaIndexer {
fun indexMedialib(context: Context) = launch(Dispatchers.IO) {
fun indexMedialib(context: Context) = AppScope.launch(Dispatchers.IO) {
val medias = Medialibrary.getInstance().getPagedVideos(Medialibrary.SORT_DEFAULT, false, 1000, 0)
val filesToIndex = HashMap<Long, Uri>()
......@@ -183,4 +184,10 @@ object MoviepediaIndexer : CoroutineScope by MainScope() {
mediaMetadata.hasCast = true
MediaMetadataRepository.getInstance(context).addMetadataImmediate(mediaMetadata)
}
val indexListener = object : IndexingListener {
override fun onIndexingDone() {
indexMedialib(AppContextProvider.appContext)
}
}
}
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.providers
package org.videolan.moviepedia.provider
import android.content.Context
import androidx.lifecycle.LiveData
......@@ -31,7 +31,7 @@ import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import org.videolan.moviepedia.database.models.MediaMetadataType
import org.videolan.moviepedia.database.models.MediaMetadataWithImages
import org.videolan.vlc.providers.datasources.MovieDataSourceFactory
import org.videolan.moviepedia.provider.datasources.MovieDataSourceFactory
class MoviepediaMovieProvider(private val context: Context, private val mediaType: MediaMetadataType) : MoviepediaProvider(context) {
......
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.providers
package org.videolan.moviepedia.provider
import android.content.Context
import androidx.lifecycle.LiveData
......@@ -30,8 +30,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedList
import org.videolan.medialibrary.interfaces.Medialibrary
import org.videolan.moviepedia.database.models.MediaMetadataWithImages
import org.videolan.moviepedia.getHeaderMoviepedia
import org.videolan.resources.util.HeaderProvider
import org.videolan.tools.Settings
import org.videolan.vlc.util.ModelsHelper
abstract class MoviepediaProvider(private val context: Context) : HeaderProvider() {
......@@ -79,7 +80,7 @@ abstract class MoviepediaProvider(private val context: Context) : HeaderProvider
startposition > 0 -> pagedList.value?.getOrNull(startposition + position - 1)
else -> null
}
ModelsHelper.getHeaderMoviepedia(context, sort, item.metadata, previous?.metadata)?.let {
getHeaderMoviepedia(context, sort, item.metadata, previous?.metadata)?.let {
privateHeaders.put(startposition + position, it)
}
}
......
......@@ -22,15 +22,18 @@
*
*/
package org.videolan.vlc.providers
package org.videolan.moviepedia.provider
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.videolan.medialibrary.interfaces.Medialibrary
import org.videolan.medialibrary.interfaces.media.MediaWrapper
import org.videolan.moviepedia.database.models.MediaMetadataWithImages
import org.videolan.moviepedia.repository.MediaMetadataRepository
import org.videolan.vlc.util.getFromMl
import org.videolan.vlc.viewmodels.Season
import org.videolan.moviepedia.viewmodel.Season
import org.videolan.resources.interfaces.IMediaContentResolver
import org.videolan.resources.util.getFromMl
class MoviepediaTvshowProvider(private val context: Context) {
......@@ -160,4 +163,25 @@ class MoviepediaTvshowProvider(private val context: Context) {
}
return mediasToPlay
}
}
\ No newline at end of file
companion object {
fun getProviders() : List<Pair<String, IMediaContentResolver>> = mutableListOf<Pair<String, IMediaContentResolver>>().apply {
add(Pair("resume_", object : IMediaContentResolver {
override suspend fun getList(context: Context, id: String): Pair<List<MediaWrapper>, Int>? {
val provider = MoviepediaTvshowProvider(context)
return withContext(Dispatchers.IO) { Pair(provider.getResumeMediasById(id.substringAfter("_")), 0) }
}
}))
add(Pair("episode_", object : IMediaContentResolver {
override suspend fun getList(context: Context, id: String): Pair<List<MediaWrapper>, Int>? {
val provider = MoviepediaTvshowProvider(context)
val moviepediaId = id.substringAfter("_")
return withContext(Dispatchers.IO) { provider.getShowIdForEpisode(moviepediaId)?.let { provider.getAllEpisodesForShow(it) } }?.let {
Pair(it.mapNotNull { episode -> episode.media }, it.indexOfFirst { it.metadata.moviepediaId == moviepediaId })
}
}
}))
}
}
}
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.providers.datasources
package org.videolan.moviepedia.provider.datasources
import android.content.Context
import androidx.lifecycle.MutableLiveData
......
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.gui
package org.videolan.moviepedia.ui
import android.os.Bundle
import android.text.Editable
......@@ -36,13 +36,15 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import org.videolan.medialibrary.interfaces.media.MediaWrapper
import org.videolan.vlc.R
import org.videolan.vlc.databinding.MoviepediaActivityBinding
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.helpers.applyTheme
import org.videolan.moviepedia.R
import org.videolan.moviepedia.databinding.MoviepediaActivityBinding
import org.videolan.moviepedia.models.identify.Media
import org.videolan.moviepedia.models.identify.getAllResults
import org.videolan.moviepedia.viewmodel.MoviepediaModel
import org.videolan.resources.MOVIEPEDIA_MEDIA
import org.videolan.vlc.gui.BaseActivity
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.helpers.applyTheme
open class MoviepediaActivity : BaseActivity(), TextWatcher, TextView.OnEditorActionListener {
......@@ -65,7 +67,7 @@ open class MoviepediaActivity : BaseActivity(), TextWatcher, TextView.OnEditorAc
binding.nextResults.adapter = moviepediaResultAdapter
binding.nextResults.layoutManager = GridLayoutManager(this, 2)
media = intent.getParcelableExtra(MEDIA)
media = intent.getParcelableExtra(MOVIEPEDIA_MEDIA)
binding.searchEditText.addTextChangedListener(this)
binding.searchEditText.setOnEditorActionListener(this)
......@@ -108,10 +110,4 @@ open class MoviepediaActivity : BaseActivity(), TextWatcher, TextView.OnEditorAc
finish()
}
}
companion object {
const val MEDIA: String = "media"
const val TAG = "VLC/SearchActivity"
}
}
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.gui
package org.videolan.moviepedia.ui
import android.view.LayoutInflater
import android.view.ViewGroup
......@@ -31,11 +31,11 @@ import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.videolan.moviepedia.databinding.MoviepediaItemBinding
import org.videolan.moviepedia.models.identify.Media
import org.videolan.moviepedia.models.identify.getImageUri
import org.videolan.moviepedia.models.identify.getYear
import org.videolan.tools.getLocaleLanguages
import org.videolan.vlc.databinding.MoviepediaItemBinding
import org.videolan.vlc.gui.helpers.SelectorViewHolder
class MoviepediaResultAdapter internal constructor(private val layoutInflater: LayoutInflater) : RecyclerView.Adapter<MoviepediaResultAdapter.ViewHolder>() {
......
......@@ -22,7 +22,7 @@
*
*/
package org.videolan.vlc.viewmodels
package org.videolan.moviepedia.viewmodel
import android.content.Context
import androidx.lifecycle.MediatorLiveData
......@@ -34,10 +34,10 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
import org.videolan.moviepedia.MoviepediaIndexer
import org.videolan.moviepedia.database.models.*
import org.videolan.vlc.providers.MoviepediaTvshowProvider
import org.videolan.moviepedia.provider.MoviepediaTvshowProvider
import org.videolan.moviepedia.repository.MediaMetadataRepository
import org.videolan.moviepedia.repository.MediaPersonRepository
import org.videolan.vlc.util.getFromMl
import org.videolan.resources.util.getFromMl
class MediaMetadataModel(private val context: Context, mlId: Long? = null, moviepediaId: String? = null) : ViewModel(), CoroutineScope by MainScope() {