Rewrite login flow
Rewrite auth to use mvvm and koin Add add server button Add catch for null ServerInfo in legacy saved credentials Fix resource leaking in JsonFileSource Enable improved logging for resource leaking Add new login flow Use KeyboardFocusChangeListener for handling keyboard hiding Fix codacy issues Add toolbar to server list screen Fix padding on server list Remove unused import Fix back navigation in MainActivity Fix crash due to missing ServerInfo Convert StartupActivity to Kotlin Move add server button to toolbar Fix window not resizing in user login fragment Add hints for username and password fields Fix codacy issues Remove old comment Remove old login code Test Codacy ignore Use local serializer for JsonFileSource Fix codacy issues Move gson instance to koin module Add prefill for username in login fragment Add equals overrides for User and Server models Update blank server name check Add UserConfiguration and UserPolicy models Move resource debug logging to JellyfinApplication Suppress warnings Fix formatting Add loading state to server list Prevent submission of empty login forms Add basic error handling for invalid login Suppress exception handling warnings Remove hardcoded strings
This commit is contained in:
@@ -84,6 +84,10 @@ dependencies {
|
||||
// Dependency Injection
|
||||
val koinVersion = "2.1.6"
|
||||
implementation("org.koin:koin-android-viewmodel:$koinVersion")
|
||||
// Lifecycle extensions
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
|
||||
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
|
||||
// Media players
|
||||
implementation("com.amazon.android:exoplayer:2.11.3")
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- Needed for ChannelManager on older devices (< API 23) -->
|
||||
@@ -41,7 +42,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:logo="@drawable/app_logo"
|
||||
android:noHistory="true"
|
||||
android:screenOrientation="landscape">
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -52,10 +54,6 @@
|
||||
<activity-alias
|
||||
android:name=".startup.StartupActivity"
|
||||
android:targetActivity=".ui.startup.StartupActivity" />
|
||||
<activity
|
||||
android:name=".ui.startup.SelectUserActivity"
|
||||
android:label="@string/title_activity_select_user"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".ui.playback.PlaybackOverlayActivity"
|
||||
android:label="PlaybackOverlayActivity"
|
||||
@@ -69,12 +67,8 @@
|
||||
<activity
|
||||
android:name=".ui.browsing.UserViewActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".ui.startup.SelectServerActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".ui.browsing.CollectionActivity"
|
||||
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".ui.browsing.ByLetterActivity"
|
||||
@@ -119,10 +113,6 @@
|
||||
android:name=".ui.playback.AudioNowPlayingActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity android:name=".ui.playback.ExternalPlayerActivity" />
|
||||
<activity
|
||||
android:name=".ui.startup.DpadPwActivity"
|
||||
android:label="@string/title_activity_dpad_pw"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<receiver android:name=".util.RemoteControlReceiver">
|
||||
<intent-filter>
|
||||
|
||||
@@ -54,6 +54,19 @@ class JellyfinApplication : TvApp() {
|
||||
// Initialize the logging library
|
||||
Timber.plant(DebugTree())
|
||||
Timber.i("Application object created")
|
||||
|
||||
// Enable improved logging for leaking resources
|
||||
// https://wh0.github.io/2020/08/12/closeguard.html
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
Class.forName("dalvik.system.CloseGuard")
|
||||
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
|
||||
.invoke(null, true)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
|
||||
@@ -27,10 +27,8 @@ import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
|
||||
public class TvApp extends Application {
|
||||
public static final String DISPLAY_PREFS_APP_NAME = "ATV";
|
||||
public static final String CREDENTIALS_PATH = "org.jellyfin.androidtv.login.json";
|
||||
|
||||
public static final int LIVE_TV_GUIDE_OPTION_ID = 1000;
|
||||
public static final int LIVE_TV_RECORDINGS_OPTION_ID = 2000;
|
||||
@@ -67,6 +65,7 @@ public class TvApp extends Application {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public UserDto getCurrentUser() {
|
||||
if (currentUser == null) {
|
||||
Timber.e("Called getCurrentUser() but value was null.");
|
||||
@@ -74,6 +73,7 @@ public class TvApp extends Application {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setCurrentUser(UserDto currentUser) {
|
||||
this.currentUser = currentUser;
|
||||
TvManager.clearCache();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
data class LoadingState private constructor(val status: Status, val message: String? = null){
|
||||
companion object {
|
||||
val PENDING = LoadingState(Status.PENDING)
|
||||
val LOADING = LoadingState(Status.LOADING)
|
||||
val SUCCESS = LoadingState(Status.SUCCESS)
|
||||
fun error(msg: String? = null) = LoadingState(Status.ERROR, msg)
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
PENDING,
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.model;
|
||||
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo;
|
||||
import org.jellyfin.apiclient.model.dto.UserDto;
|
||||
|
||||
public class LogonCredentials {
|
||||
private UserDto userDto;
|
||||
|
||||
public LogonCredentials(ServerInfo server, UserDto user) {
|
||||
userDto = user;
|
||||
}
|
||||
|
||||
public UserDto getUserDto() {
|
||||
return userDto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.jellyfin.androidtv.util.apiclient.toServer
|
||||
import org.jellyfin.androidtv.util.apiclient.toUser
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo
|
||||
import org.jellyfin.apiclient.model.dto.UserDto
|
||||
|
||||
@Deprecated("Used for reading legacy credentials file")
|
||||
class LogonCredentials {
|
||||
@SerializedName("serverInfo")
|
||||
var server: Server? = null
|
||||
private set
|
||||
|
||||
@SerializedName("userDto")
|
||||
var user: User? = null
|
||||
private set
|
||||
|
||||
constructor(server: Server?, user: User?) {
|
||||
this.server = server
|
||||
this.user = user
|
||||
}
|
||||
|
||||
constructor(serverInfo: ServerInfo?, userDto: UserDto?) {
|
||||
server = serverInfo?.toServer()
|
||||
user = userDto?.toUser()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Server model to use locally in place of ServerInfo model in ApiClient.
|
||||
*/
|
||||
data class Server(
|
||||
var id: String,
|
||||
var name: String,
|
||||
var address: String,
|
||||
@Deprecated("This is only used for legacy stored credentials")
|
||||
val userId: String? = null,
|
||||
@Deprecated("This is only used for legacy stored credentials")
|
||||
val accessToken: String? = null,
|
||||
val dateLastAccessed: Date = Date(0)
|
||||
) {
|
||||
override fun equals(other: Any?) = (other is Server) && id == other.id
|
||||
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
class ServerList(
|
||||
var currentServerUsers: Map<Server, List<User>> = emptyMap(),
|
||||
var currentServerUsersState: LoadingState = LoadingState.PENDING,
|
||||
var discoveredServersUsers: Map<Server, List<User>> = emptyMap(),
|
||||
var discoveredServersUsersState: LoadingState = LoadingState.PENDING,
|
||||
var savedServersUsers: Map<Server, List<User>> = emptyMap(),
|
||||
var savedServersUsersState: LoadingState = LoadingState.PENDING
|
||||
) {
|
||||
val state: LoadingState
|
||||
get() {
|
||||
return if (currentServerUsersState.status == LoadingState.error().status) {
|
||||
currentServerUsersState
|
||||
} else if (discoveredServersUsersState.status == LoadingState.error().status) {
|
||||
discoveredServersUsersState
|
||||
} else if (savedServersUsersState.status == LoadingState.error().status) {
|
||||
savedServersUsersState
|
||||
} else if (currentServerUsersState == LoadingState.LOADING ||
|
||||
discoveredServersUsersState == LoadingState.LOADING ||
|
||||
savedServersUsersState == LoadingState.LOADING) {
|
||||
LoadingState.LOADING
|
||||
} else if (currentServerUsersState == LoadingState.SUCCESS ||
|
||||
discoveredServersUsersState == LoadingState.SUCCESS ||
|
||||
savedServersUsersState == LoadingState.SUCCESS) {
|
||||
LoadingState.SUCCESS
|
||||
} else {
|
||||
LoadingState.PENDING
|
||||
}
|
||||
}
|
||||
|
||||
val allServersUsers: Map<Server, List<User>>
|
||||
get() = currentServerUsers + savedServersUsers + discoveredServersUsers
|
||||
}
|
||||
27
app/src/main/java/org/jellyfin/androidtv/data/model/User.kt
Normal file
27
app/src/main/java/org/jellyfin/androidtv/data/model/User.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
/**
|
||||
* User model to use locally in place of UserDto model in ApiClient.
|
||||
*/
|
||||
data class User(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val accessToken: String = "",
|
||||
val serverId: String,
|
||||
val primaryImageTag: String = "",
|
||||
val hasPassword: Boolean = true,
|
||||
val hasConfiguredPassword: Boolean = true,
|
||||
val hasConfiguredEasyPassword: Boolean = false,
|
||||
val configuration: UserConfiguration = UserConfiguration(),
|
||||
val policy: UserPolicy = UserPolicy()
|
||||
) {
|
||||
override fun equals(other: Any?) = (other is User)
|
||||
&& serverId == other.serverId
|
||||
&& id == other.id
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + serverId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
/**
|
||||
* UserConfiguration model to use locally in place of UserConfiguration model in ApiClient.
|
||||
*/
|
||||
data class UserConfiguration(
|
||||
val latestItemsExcludes: Array<String> = emptyArray()
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as UserConfiguration
|
||||
|
||||
if (!latestItemsExcludes.contentEquals(other.latestItemsExcludes)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = latestItemsExcludes.contentHashCode()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
/**
|
||||
* UserPolicy model to use locally in place of UserPolicy model in ApiClient.
|
||||
*/
|
||||
data class UserPolicy(
|
||||
val enableLiveTvAccess: Boolean = true,
|
||||
val enableLiveTvManagement: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.jellyfin.androidtv.data.repository
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.source.CredentialsFileSource
|
||||
import org.jellyfin.androidtv.util.apiclient.callApi
|
||||
import org.jellyfin.androidtv.util.apiclient.toServer
|
||||
import org.jellyfin.apiclient.Jellyfin
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.apiclient.model.system.PublicSystemInfo
|
||||
import timber.log.Timber
|
||||
|
||||
interface ServerRepository {
|
||||
suspend fun getServers(): List<Server>
|
||||
|
||||
suspend fun discoverServers(): List<Server>
|
||||
|
||||
suspend fun connect(address: String): Server
|
||||
}
|
||||
|
||||
class ServerRepositoryImpl(
|
||||
private val jellyfin: Jellyfin,
|
||||
private val device: IDevice,
|
||||
private val credentialsFileSource: CredentialsFileSource
|
||||
) : ServerRepository {
|
||||
override suspend fun getServers(): List<Server> {
|
||||
val legacyCredentials = credentialsFileSource.read()
|
||||
if (legacyCredentials?.server != null) {
|
||||
// Augment saved ServerInfo with PublicSystemInfo
|
||||
val api = jellyfin.createApi(serverAddress = legacyCredentials.server!!.address, device = device)
|
||||
val systemInfo: PublicSystemInfo = callApi { callback ->
|
||||
api.GetPublicSystemInfoAsync(callback)
|
||||
}
|
||||
legacyCredentials.server!!.apply {
|
||||
name = systemInfo.serverName
|
||||
id = systemInfo.id
|
||||
}
|
||||
|
||||
return listOf(legacyCredentials.server!!)
|
||||
}
|
||||
|
||||
// TODO: Add new method of saving credentials
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun discoverServers() = withContext(Dispatchers.IO) {
|
||||
jellyfin.discovery.discover().toList().map { it.toServer() }
|
||||
}
|
||||
|
||||
override suspend fun connect(address: String): Server {
|
||||
Timber.d("Creating api for server address %s", address)
|
||||
val api = jellyfin.createApi(serverAddress = address, device = device)
|
||||
val systemInfo: PublicSystemInfo = callApi { callback ->
|
||||
api.GetPublicSystemInfoAsync(callback)
|
||||
}
|
||||
return systemInfo.toServer().apply {
|
||||
// Use the entered address since SystemInfo can be wrong
|
||||
this.address = address
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.jellyfin.androidtv.data.repository
|
||||
|
||||
import org.jellyfin.androidtv.data.model.LogonCredentials
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.data.source.CredentialsFileSource
|
||||
import org.jellyfin.androidtv.util.apiclient.callApi
|
||||
import org.jellyfin.androidtv.util.apiclient.getPublicUsers
|
||||
import org.jellyfin.androidtv.util.apiclient.toServerInfo
|
||||
import org.jellyfin.androidtv.util.apiclient.toUser
|
||||
import org.jellyfin.apiclient.Jellyfin
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.apiclient.model.users.AuthenticationResult
|
||||
|
||||
interface UserRepository {
|
||||
suspend fun getUsers(server: Server): List<User>
|
||||
|
||||
suspend fun login(server: Server, username: String, password: String): AuthenticationResult
|
||||
}
|
||||
|
||||
class UserRepositoryImpl(
|
||||
private val jellyfin: Jellyfin,
|
||||
private val apiClient: ApiClient,
|
||||
private val device: IDevice,
|
||||
private val credentialsFileSource: CredentialsFileSource
|
||||
) : UserRepository {
|
||||
override suspend fun getUsers(server: Server): List<User> {
|
||||
val api = jellyfin.createApi(serverAddress = server.address, device = device)
|
||||
return api.getPublicUsers()?.toList()?.map { it.toUser() } ?: emptyList()
|
||||
}
|
||||
|
||||
override suspend fun login(server: Server, username: String, password: String): AuthenticationResult {
|
||||
// TODO: Are both of these updates needed?
|
||||
// Update the server address the apiClient uses
|
||||
apiClient.EnableAutomaticNetworking(server.toServerInfo())
|
||||
apiClient.ChangeServerLocation(server.address)
|
||||
|
||||
// Authenticate using given credentials
|
||||
val result: AuthenticationResult = callApi { callback ->
|
||||
apiClient.AuthenticateUserAsync(username, password, callback)
|
||||
}
|
||||
|
||||
// Save the current login information
|
||||
credentialsFileSource.write(LogonCredentials(server, result.user.toUser()))
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jellyfin.androidtv.data.source
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import org.jellyfin.androidtv.data.model.LogonCredentials
|
||||
|
||||
const val CREDENTIALS_PATH = "org.jellyfin.androidtv.login.json"
|
||||
|
||||
@Deprecated("Method of retrieving credentials stored in a legacy format")
|
||||
class CredentialsFileSource(
|
||||
private val context: Context,
|
||||
private val serializer: Gson
|
||||
) : JsonFileSource<LogonCredentials>(
|
||||
context,
|
||||
CREDENTIALS_PATH,
|
||||
serializer,
|
||||
LogonCredentials::class.java
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jellyfin.androidtv.data.source
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import timber.log.Timber
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
open class JsonFileSource<T>(
|
||||
private val context: Context,
|
||||
private val fileName: String,
|
||||
private val serializer: Gson,
|
||||
private val clazz: Class<T>
|
||||
) {
|
||||
fun read(): T? {
|
||||
try {
|
||||
context.openFileInput(fileName).use {
|
||||
val data = it.readBytes()
|
||||
.toString(Charsets.UTF_8)
|
||||
|
||||
return serializer.fromJson(data, clazz)
|
||||
}
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Timber.e(ex, "File %s not found", fileName)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun write(t: T) {
|
||||
context.openFileOutput(fileName, Context.MODE_PRIVATE).use {
|
||||
it.write(serializer.toJson(t).toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,27 @@
|
||||
package org.jellyfin.androidtv.di
|
||||
|
||||
import com.google.gson.FieldNamingPolicy
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.jellyfin.androidtv.BuildConfig
|
||||
import org.jellyfin.androidtv.data.eventhandling.TvApiEventListener
|
||||
import org.jellyfin.androidtv.data.repository.ServerRepository
|
||||
import org.jellyfin.androidtv.data.repository.ServerRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.repository.UserRepository
|
||||
import org.jellyfin.androidtv.data.repository.UserRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.source.CredentialsFileSource
|
||||
import org.jellyfin.androidtv.ui.startup.LoginViewModel
|
||||
import org.jellyfin.apiclient.AppInfo
|
||||
import org.jellyfin.apiclient.Jellyfin
|
||||
import org.jellyfin.apiclient.android
|
||||
import org.jellyfin.apiclient.interaction.AndroidDevice
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.apiclient.logging.AndroidLogger
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer
|
||||
import org.jellyfin.apiclient.serialization.ServerInfoDeserializer
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
@@ -22,10 +35,38 @@ val appModule = module {
|
||||
}
|
||||
}
|
||||
|
||||
single<IDevice> {
|
||||
AndroidDevice.fromContext(androidApplication())
|
||||
}
|
||||
|
||||
single {
|
||||
get<Jellyfin>().createApi(
|
||||
device = AndroidDevice.fromContext(androidApplication()),
|
||||
device = get(),
|
||||
eventListener = TvApiEventListener()
|
||||
)
|
||||
}
|
||||
|
||||
single(named("CredentialsFileSerializer")) {
|
||||
GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||
.registerTypeAdapter(ServerInfo::class.java, ServerInfoDeserializer())
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
|
||||
.create()
|
||||
}
|
||||
|
||||
single {
|
||||
CredentialsFileSource(androidApplication(), get(named("CredentialsFileSerializer")))
|
||||
}
|
||||
|
||||
single<ServerRepository> {
|
||||
ServerRepositoryImpl(get(), get(), get())
|
||||
}
|
||||
|
||||
single<UserRepository> {
|
||||
UserRepositoryImpl(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
LoginViewModel(get(), get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.jellyfin.androidtv.integration.ChannelManager;
|
||||
import org.jellyfin.androidtv.preference.SystemPreferences;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.preference.constant.AudioBehavior;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.androidtv.ui.browsing.BrowseRowDef;
|
||||
import org.jellyfin.androidtv.ui.browsing.IRowLoader;
|
||||
import org.jellyfin.androidtv.ui.browsing.StdBrowseFragment;
|
||||
@@ -33,7 +32,7 @@ import org.jellyfin.androidtv.ui.playback.AudioEventListener;
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
|
||||
@@ -48,7 +47,6 @@ import org.jellyfin.apiclient.model.querying.ItemSortBy;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -97,11 +95,7 @@ public class HomeFragment extends StdBrowseFragment {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// Save last login so we can get back proper context on entry
|
||||
try {
|
||||
AuthenticationHelper.saveLoginCredentials(new LogonCredentials(apiClient.getValue().getServerInfo(), TvApp.getApplication().getCurrentUser()), TvApp.CREDENTIALS_PATH);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Unable to save login credentials");
|
||||
}
|
||||
mApplication.setConfiguredAutoCredentials(new LogonCredentials(apiClient.getValue().getServerInfo(), TvApp.getApplication().getCurrentUser()));
|
||||
|
||||
// Init leanback home channels;
|
||||
channelManager = new ChannelManager();
|
||||
|
||||
@@ -13,11 +13,11 @@ import androidx.leanback.widget.RowPresenter;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.preference.PreferencesActivity;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.startup.SelectUserActivity;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.startup.StartupActivity;
|
||||
|
||||
public class HomeFragmentFooterRow extends HomeFragmentRow implements OnItemViewClickedListener {
|
||||
private static final int LOGOUT = 0;
|
||||
@@ -46,10 +46,11 @@ public class HomeFragmentFooterRow extends HomeFragmentRow implements OnItemView
|
||||
|
||||
switch (((GridButton) item).getId()) {
|
||||
case LOGOUT:
|
||||
TvApp app = TvApp.getApplication();
|
||||
// Unset the current user
|
||||
TvApp.getApplication().setCurrentUser(null);
|
||||
|
||||
// Open login activity
|
||||
Intent selectUserIntent = new Intent(activity, SelectUserActivity.class);
|
||||
Intent selectUserIntent = new Intent(activity, StartupActivity.class);
|
||||
selectUserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); // Disallow back button
|
||||
activity.startActivity(selectUserIntent);
|
||||
|
||||
@@ -61,6 +62,5 @@ public class HomeFragmentFooterRow extends HomeFragmentRow implements OnItemView
|
||||
activity.startActivity(settingsIntent);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.ViewType;
|
||||
import org.jellyfin.androidtv.data.model.ChapterItemInfo;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.ui.browsing.BrowseRecordingsActivity;
|
||||
import org.jellyfin.androidtv.ui.browsing.BrowseScheduleActivity;
|
||||
import org.jellyfin.androidtv.ui.browsing.CollectionActivity;
|
||||
@@ -21,15 +20,14 @@ import org.jellyfin.androidtv.ui.itemdetail.ItemListActivity;
|
||||
import org.jellyfin.androidtv.ui.itemdetail.PhotoPlayerActivity;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideActivity;
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.dto.UserDto;
|
||||
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
|
||||
import org.jellyfin.apiclient.model.library.PlayAccess;
|
||||
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
|
||||
@@ -257,21 +255,6 @@ public class ItemLauncher {
|
||||
});
|
||||
|
||||
break;
|
||||
case Server:
|
||||
//Log in to selected server
|
||||
get(ApiClient.class).ChangeServerLocation(rowItem.getServerInfo().getAddress());
|
||||
AuthenticationHelper.enterManualUser(activity);
|
||||
break;
|
||||
|
||||
case User:
|
||||
final UserDto user = rowItem.getUser();
|
||||
if (user.getHasPassword()) {
|
||||
Utils.processPasswordEntry(activity, user);
|
||||
|
||||
} else {
|
||||
AuthenticationHelper.loginUser(user.getName(), "", get(ApiClient.class), activity);
|
||||
}
|
||||
break;
|
||||
|
||||
case SearchHint:
|
||||
final SearchHint hint = rowItem.getSearchHint();
|
||||
|
||||
@@ -2,16 +2,12 @@ package org.jellyfin.androidtv.ui.preference.category
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.TvApp
|
||||
import org.jellyfin.androidtv.data.model.LogonCredentials
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.LoginBehavior
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.OptionsScreen
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.enum
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
fun OptionsScreen.authenticationCategory(
|
||||
userPreferences: UserPreferences,
|
||||
@@ -23,13 +19,9 @@ fun OptionsScreen.authenticationCategory(
|
||||
setTitle(R.string.pref_login_behavior_title)
|
||||
bind {
|
||||
set {
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
if (it == LoginBehavior.AUTO_LOGIN) {
|
||||
try {
|
||||
val credentials = LogonCredentials(apiClient.serverInfo, TvApp.getApplication().currentUser)
|
||||
AuthenticationHelper.saveLoginCredentials(credentials, TvApp.CREDENTIALS_PATH)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Unable to save logon credentials")
|
||||
}
|
||||
// FIXME: Fix autologin preference
|
||||
}
|
||||
|
||||
userPreferences[UserPreferences.loginBehavior] = it
|
||||
@@ -43,7 +35,7 @@ fun OptionsScreen.authenticationCategory(
|
||||
// Auto login is disabled
|
||||
userPreferences[UserPreferences.loginBehavior] != LoginBehavior.AUTO_LOGIN
|
||||
// Or configured user is set to current user
|
||||
|| configuredAutoCredentials.userDto.id == TvApp.getApplication().currentUser.id
|
||||
|| configuredAutoCredentials.user?.id == TvApp.getApplication().currentUser.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +48,9 @@ fun OptionsScreen.authenticationCategory(
|
||||
// Auto login is enabled
|
||||
userPreferences[UserPreferences.loginBehavior] == LoginBehavior.AUTO_LOGIN
|
||||
// Configured user is set to current user
|
||||
&& configuredAutoCredentials.userDto.id == TvApp.getApplication().currentUser.id
|
||||
&& configuredAutoCredentials.user?.id == TvApp.getApplication().currentUser.id
|
||||
// Configured user contains a password
|
||||
&& configuredAutoCredentials.userDto.hasPassword
|
||||
&& configuredAutoCredentials.user?.hasPassword ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.jellyfin.androidtv.ui.shared
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.android.synthetic.main.fragment_alert_dialog.*
|
||||
import org.jellyfin.androidtv.R
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
open class AlertFragment(
|
||||
@StringRes private val title: Int,
|
||||
@StringRes private val description: Int? = null,
|
||||
@StringRes private val confirmButtonText: Int? = null,
|
||||
private val onConfirmCallback: () -> Unit = {},
|
||||
@StringRes private val cancelButtonText: Int? = null,
|
||||
private val onCancelCallback: () -> Unit = {},
|
||||
private val onClose: () -> Unit = {}
|
||||
) : Fragment() {
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val titleView = requireActivity().findViewById<TextView>(R.id.title)
|
||||
titleView.setText(title)
|
||||
|
||||
val descriptionView = requireActivity().findViewById<TextView>(R.id.description)
|
||||
if (description != null) {
|
||||
descriptionView.setText(description)
|
||||
} else {
|
||||
descriptionView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val confirmButton = requireActivity().findViewById<Button>(R.id.confirm)
|
||||
if (confirmButtonText != null) confirmButton.setText(confirmButtonText)
|
||||
confirmButton.requestFocus()
|
||||
confirmButton.setOnClickListener {
|
||||
onConfirmCallback()
|
||||
onClose()
|
||||
}
|
||||
|
||||
val cancelButton = requireActivity().findViewById<Button>(R.id.cancel)
|
||||
if (cancelButtonText != null) cancelButton.setText(cancelButtonText)
|
||||
cancelButton.setOnClickListener {
|
||||
onCancelCallback()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_alert_dialog, container, false)
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,6 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
||||
import org.jellyfin.androidtv.TvApp
|
||||
import org.jellyfin.androidtv.ui.startup.DpadPwActivity
|
||||
import org.jellyfin.androidtv.ui.startup.SelectServerActivity
|
||||
import org.jellyfin.androidtv.ui.startup.SelectUserActivity
|
||||
import org.jellyfin.androidtv.ui.startup.StartupActivity
|
||||
|
||||
private const val LOG_TAG = "AuthUserCallbacks"
|
||||
@@ -37,9 +34,6 @@ class AuthenticatedUserCallbacks : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
|
||||
when (activity) {
|
||||
// Ignore startup activities
|
||||
is DpadPwActivity,
|
||||
is SelectServerActivity,
|
||||
is SelectUserActivity,
|
||||
is StartupActivity -> return
|
||||
// All other activities should have a current user
|
||||
else -> {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jellyfin.androidtv.ui.shared
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.jellyfin.androidtv.R
|
||||
|
||||
open class ToolbarFragment : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_toolbar, container, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.shared.AlertFragment
|
||||
import org.jellyfin.androidtv.ui.shared.KeyboardFocusChangeListener
|
||||
|
||||
class AddServerFragment(
|
||||
private val onConfirmCallback: (url: String) -> Unit = {},
|
||||
private val onCancelCallback: () -> Unit = {},
|
||||
private val onClose: () -> Unit = {}
|
||||
) : AlertFragment(
|
||||
title = R.string.lbl_enter_server_address,
|
||||
description = R.string.lbl_valid_server_address,
|
||||
onCancelCallback = onCancelCallback,
|
||||
onClose = onClose
|
||||
) {
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val contentView = requireActivity().findViewById<LinearLayout>(R.id.content)
|
||||
|
||||
// Build the url field
|
||||
val address = EditText(activity)
|
||||
address.hint = requireActivity().getString(R.string.lbl_ip_hint)
|
||||
address.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
|
||||
address.isSingleLine = true
|
||||
address.onFocusChangeListener = KeyboardFocusChangeListener()
|
||||
address.nextFocusDownId = R.id.confirm
|
||||
address.requestFocus()
|
||||
// Add the url field to the content view
|
||||
contentView.addView(address)
|
||||
|
||||
// Override the default confirm button click listener to return the address field text
|
||||
val confirmButton = requireActivity().findViewById<Button>(R.id.confirm)
|
||||
confirmButton.setOnClickListener {
|
||||
if (address.text.isNotBlank()) {
|
||||
onConfirmCallback(address.text.toString())
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.media.ToneGenerator;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.UserDto;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
public class DpadPwActivity extends FragmentActivity {
|
||||
|
||||
private long lastKeyDown = Long.MAX_VALUE;
|
||||
private long lastKeyUp = 0;
|
||||
private boolean keyUpDetected = true;
|
||||
private boolean processed = false;
|
||||
private int lastKey;
|
||||
private int longPressSensitivity = 600;
|
||||
private int doubleClickSensitivity = 350;
|
||||
private String password = "";
|
||||
|
||||
TextView title;
|
||||
TextView pwField;
|
||||
UserDto user;
|
||||
String directItemId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_dpad_pw);
|
||||
|
||||
title = (TextView)findViewById(R.id.dpad_pw_text);
|
||||
pwField = (TextView)findViewById(R.id.dpad_pw_display);
|
||||
|
||||
user = get(GsonJsonSerializer.class).DeserializeFromString(getIntent().getStringExtra("User"), UserDto.class);
|
||||
directItemId = getIntent().getStringExtra("ItemId");
|
||||
|
||||
title.setText(title.getText() + " for "+ user.getName());
|
||||
pwField.setText(password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
keyUpDetected = true;
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
case KeyEvent.KEYCODE_0:
|
||||
case KeyEvent.KEYCODE_1:
|
||||
case KeyEvent.KEYCODE_2:
|
||||
case KeyEvent.KEYCODE_3:
|
||||
case KeyEvent.KEYCODE_4:
|
||||
case KeyEvent.KEYCODE_5:
|
||||
case KeyEvent.KEYCODE_6:
|
||||
case KeyEvent.KEYCODE_7:
|
||||
case KeyEvent.KEYCODE_8:
|
||||
case KeyEvent.KEYCODE_9:
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && System.currentTimeMillis() - lastKeyDown > longPressSensitivity) {
|
||||
if (processed) return true; //some controllers appear to double send the up on a long press
|
||||
Timber.d("Password finished");
|
||||
Utils.makeTone(ToneGenerator.TONE_CDMA_ANSWER, 200);
|
||||
processed = true;
|
||||
AuthenticationHelper.loginUser(user.getName(), password, get(ApiClient.class), this, directItemId);
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && System.currentTimeMillis() - lastKeyDown > longPressSensitivity) {
|
||||
Timber.d("Password clear");
|
||||
password = "";
|
||||
pwField.setText(password);
|
||||
processed = false;
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && System.currentTimeMillis() - lastKeyDown > longPressSensitivity) {
|
||||
Timber.d("Requesting dialog...");
|
||||
final EditText password = new EditText(this);
|
||||
final Activity activity = this;
|
||||
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.password_prompt)
|
||||
.setMessage(getString(R.string.password_prompt_message, user.getName()))
|
||||
.setView(password)
|
||||
.setPositiveButton(R.string.lbl_ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
String pw = password.getText().toString();
|
||||
AuthenticationHelper.loginUser(user.getName(), pw, get(ApiClient.class), activity, directItemId);
|
||||
}
|
||||
}).show();
|
||||
return true;
|
||||
}
|
||||
lastKeyDown = Long.MAX_VALUE;
|
||||
if (lastKey == keyCode && System.currentTimeMillis() - lastKeyUp <= doubleClickSensitivity) {
|
||||
lastKeyUp = 0;
|
||||
Utils.beep(300);
|
||||
//Remove the single-click value
|
||||
password = password.substring(0,password.length()-1);
|
||||
processKey(keyCode, true, event);
|
||||
} else {
|
||||
lastKeyUp = System.currentTimeMillis();
|
||||
Utils.beep();
|
||||
processKey(keyCode, false, event);
|
||||
}
|
||||
processed = false;
|
||||
return true;
|
||||
|
||||
default:
|
||||
processed = false;
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void processKey(int keyCode, boolean doubleClick, KeyEvent event) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
password += doubleClick ? 6 : 1;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
password += doubleClick ? 7 : 2;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
password += doubleClick ? 8 : 3;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
password += doubleClick ? 9 : 4;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
password += doubleClick ? 5 : 0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_0:
|
||||
case KeyEvent.KEYCODE_1:
|
||||
case KeyEvent.KEYCODE_2:
|
||||
case KeyEvent.KEYCODE_3:
|
||||
case KeyEvent.KEYCODE_4:
|
||||
case KeyEvent.KEYCODE_5:
|
||||
case KeyEvent.KEYCODE_6:
|
||||
case KeyEvent.KEYCODE_7:
|
||||
case KeyEvent.KEYCODE_8:
|
||||
case KeyEvent.KEYCODE_9:
|
||||
password += (char)event.getUnicodeChar();
|
||||
break;
|
||||
}
|
||||
//TvApp.getApplication().getLogger().Debug(password);
|
||||
pwField.setText(password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
if (!keyUpDetected) return false;
|
||||
lastKeyDown = System.currentTimeMillis();
|
||||
lastKey = keyCode;
|
||||
keyUpDetected = false;
|
||||
return true;
|
||||
default:
|
||||
return super.onKeyDown(keyCode, event);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.leanback.app.RowsSupportFragment
|
||||
import androidx.leanback.widget.ArrayObjectAdapter
|
||||
import androidx.leanback.widget.HeaderItem
|
||||
import androidx.leanback.widget.ListRow
|
||||
import androidx.leanback.widget.ListRowPresenter
|
||||
import androidx.leanback.widget.OnItemViewClickedListener
|
||||
import androidx.leanback.widget.Presenter
|
||||
import androidx.leanback.widget.Row
|
||||
import androidx.leanback.widget.RowPresenter
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.model.ServerList
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.ui.GridButton
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter
|
||||
import org.koin.android.viewmodel.ext.android.sharedViewModel
|
||||
import timber.log.Timber
|
||||
|
||||
class ListServerFragment : RowsSupportFragment() {
|
||||
private companion object {
|
||||
private const val ADD_USER = 1
|
||||
private const val SELECT_USER = 2
|
||||
}
|
||||
|
||||
private val loginViewModel: LoginViewModel by sharedViewModel()
|
||||
|
||||
private val itemViewClickedListener = OnItemViewClickedListener() { _: Presenter.ViewHolder, item: Any, _: RowPresenter.ViewHolder, _: Row ->
|
||||
if (item is UserGridButton) {
|
||||
if (item.user.hasPassword) {
|
||||
// Open login fragment
|
||||
navigate(UserLoginFragment(
|
||||
user = item.user,
|
||||
onConfirmCallback = { username: String, password: String ->
|
||||
GlobalScope.launch {
|
||||
loginViewModel.login(server = item.server, username = username, password = password)
|
||||
}
|
||||
},
|
||||
onClose = { parentFragmentManager.popBackStack() }
|
||||
))
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
loginViewModel.login(item.server, item.user.name, "")
|
||||
}
|
||||
}
|
||||
} else if (item is AddUserGridButton) {
|
||||
// Open login fragment
|
||||
navigate(UserLoginFragment(
|
||||
onConfirmCallback = { username: String, password: String ->
|
||||
GlobalScope.launch {
|
||||
loginViewModel.login(server = item.server, username = username, password = password)
|
||||
}
|
||||
},
|
||||
onClose = { parentFragmentManager.popBackStack() }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
requireView().updatePadding(top = 20)
|
||||
|
||||
buildRows(emptyMap())
|
||||
|
||||
val serverObserver = Observer<ServerList> { serverList ->
|
||||
if (serverList.allServersUsers.isNotEmpty()) buildRows(serverList.allServersUsers)
|
||||
}
|
||||
|
||||
loginViewModel.serverList.observe(viewLifecycleOwner, serverObserver)
|
||||
|
||||
onItemViewClickedListener = itemViewClickedListener
|
||||
}
|
||||
|
||||
private fun buildRows(usersByServer: Map<Server, List<User>>) {
|
||||
val rowAdapter = ArrayObjectAdapter(ListRowPresenter())
|
||||
adapter = rowAdapter
|
||||
|
||||
usersByServer.forEach { (server: Server, userList: List<User>) ->
|
||||
Timber.d("Adding server row %s", server.name)
|
||||
|
||||
val userListAdapter = ArrayObjectAdapter(GridButtonPresenter())
|
||||
userList.forEach { user ->
|
||||
userListAdapter.add(UserGridButton(server, user, SELECT_USER, user.name, R.drawable.tile_port_person))
|
||||
}
|
||||
|
||||
userListAdapter.add(AddUserGridButton(server, ADD_USER, requireContext().getString(R.string.lbl_manual_login), R.drawable.tile_edit))
|
||||
|
||||
rowAdapter.add(ListRow(
|
||||
HeaderItem(usersByServer.keys.indexOf(server).toLong(),
|
||||
if (server.name.isNotBlank()) server.name else server.address),
|
||||
userListAdapter
|
||||
))
|
||||
}
|
||||
|
||||
// Ensure the server rows get focus
|
||||
requireView().requestFocus()
|
||||
}
|
||||
|
||||
private fun navigate(fragment: Fragment) {
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_view, fragment)
|
||||
.addToBackStack(this::class.simpleName)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private class AddUserGridButton(val server: Server, id: Int, text: String, @DrawableRes imageId: Int) : GridButton(id, text, imageId)
|
||||
|
||||
private class UserGridButton(val server: Server, val user: User, id: Int, text: String, @DrawableRes imageId: Int) : GridButton(id, text, imageId)
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
import org.jellyfin.androidtv.data.model.LoadingState
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.model.ServerList
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.data.repository.ServerRepository
|
||||
import org.jellyfin.androidtv.data.repository.UserRepository
|
||||
import org.jellyfin.androidtv.util.apiclient.toUser
|
||||
import timber.log.Timber
|
||||
|
||||
class LoginViewModel(
|
||||
private val serverRepository: ServerRepository,
|
||||
private val userRepository: UserRepository
|
||||
) : ViewModel() {
|
||||
private val _currentServer = MutableLiveData<Server>()
|
||||
val currentServer: LiveData<Server>
|
||||
get() = _currentServer
|
||||
|
||||
private val _currentUser = MutableLiveData<User>()
|
||||
val currentUser: LiveData<User>
|
||||
get() = _currentUser
|
||||
|
||||
private val _serverList = MediatorLiveData<ServerList>()
|
||||
val serverList: LiveData<ServerList>
|
||||
get() = _serverList
|
||||
|
||||
private val _loadingCurrentServer = MutableLiveData<LoadingState>()
|
||||
private val currentServerUsers = _currentServer.switchMap { server ->
|
||||
liveData {
|
||||
_loadingCurrentServer.value = LoadingState.LOADING
|
||||
emit(hashMapOf(server to userRepository.getUsers(server)))
|
||||
_loadingCurrentServer.value = LoadingState.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
private val _loadingDiscoveredServers = MutableLiveData<LoadingState>()
|
||||
private val discoveredServers = liveData {
|
||||
_loadingDiscoveredServers.value = LoadingState.LOADING
|
||||
emit(serverRepository.discoverServers()
|
||||
.associateWith { userRepository.getUsers(it) })
|
||||
_loadingDiscoveredServers.value = LoadingState.SUCCESS
|
||||
}
|
||||
|
||||
private val _loadingSavedServers = MutableLiveData<LoadingState>()
|
||||
private val savedServers = liveData {
|
||||
_loadingSavedServers.value = LoadingState.LOADING
|
||||
emit(serverRepository.getServers()
|
||||
.associateWith { userRepository.getUsers(it) })
|
||||
_loadingSavedServers.value = LoadingState.SUCCESS
|
||||
}
|
||||
|
||||
init {
|
||||
// Add all loading states
|
||||
_serverList.addSource(_loadingCurrentServer) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { currentServerUsersState = it }
|
||||
}
|
||||
_serverList.addSource(_loadingDiscoveredServers) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { discoveredServersUsersState = it }
|
||||
}
|
||||
_serverList.addSource(_loadingSavedServers) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { savedServersUsersState = it }
|
||||
}
|
||||
|
||||
// Add all the server data
|
||||
_serverList.addSource(currentServerUsers) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { currentServerUsers = it }
|
||||
}
|
||||
_serverList.addSource(discoveredServers) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { discoveredServersUsers = it }
|
||||
}
|
||||
_serverList.addSource(savedServers) {
|
||||
_serverList.value = (_serverList.value ?: ServerList()).apply { savedServersUsers = it }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun connect(address: String) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
Timber.d("Connecting to server %s", address)
|
||||
val connectResult = serverRepository.connect(address)
|
||||
Timber.d("Connected to server %s %s", connectResult.name, connectResult.address)
|
||||
_currentServer.postValue(connectResult)
|
||||
} catch (ex: RuntimeException) {
|
||||
Timber.w(ex, "Failed to connect to server $address")
|
||||
// TODO: Show error messaging
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(server: Server, username: String, password: String) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val authResult = userRepository.login(server, username, password)
|
||||
Timber.d("User authenticated %s %s", authResult.user.name, authResult.accessToken)
|
||||
_currentServer.postValue(server)
|
||||
_currentUser.postValue(authResult.user.toUser())
|
||||
} catch (ex: RuntimeException) {
|
||||
Timber.w(ex, "Failed to login as user $username")
|
||||
// TODO: Show error messaging
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity
|
||||
|
||||
class SelectServerActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(android.R.id.content, SelectServerFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
import androidx.leanback.widget.ListRow;
|
||||
import androidx.leanback.widget.OnItemViewClickedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.ui.shared.IKeyListener;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.androidtv.ui.browsing.CustomBrowseFragment;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SelectServerFragment extends CustomBrowseFragment {
|
||||
private static final int ENTER_MANUALLY = 0;
|
||||
private List<ServerInfo> mServers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
// TODO Don't read from extras but retrieve server list ourselves
|
||||
String[] passedItems = getActivity().getIntent().getStringArrayExtra("Servers");
|
||||
if (passedItems != null) {
|
||||
for (String json : passedItems) {
|
||||
//mServers.add(SerializerRepository.INSTANCE.getSerializer().DeserializeFromString(json, ServerInfo.class));
|
||||
}
|
||||
}
|
||||
|
||||
mActivity = (BaseActivity) getActivity();
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
super.addAdditionalRows(rowAdapter);
|
||||
|
||||
HeaderItem serverHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_select_server));
|
||||
ItemRowAdapter serverAdapter = new ItemRowAdapter(mServers.toArray(new ServerInfo[0]), new CardPresenter(), rowAdapter);
|
||||
serverAdapter.Retrieve();
|
||||
rowAdapter.add(new ListRow(serverHeader, serverAdapter));
|
||||
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_other_options));
|
||||
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
gridRowAdapter.add(new GridButton(ENTER_MANUALLY, mApplication.getString(R.string.lbl_enter_manually), R.drawable.tile_edit));
|
||||
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupEventListeners() {
|
||||
super.setupEventListeners();
|
||||
mClickedListener.registerListener(new ItemViewClickedListener());
|
||||
if (mActivity != null) {
|
||||
mActivity.registerKeyListener(new IKeyListener() {
|
||||
@Override
|
||||
public boolean onKeyUp(int key, KeyEvent event) {
|
||||
return KeyProcessor.HandleKey(key, mCurrentItem, mActivity);
|
||||
}
|
||||
});
|
||||
|
||||
mActivity.registerMessageListener(new IMessageListener() {
|
||||
@Override
|
||||
public void onMessageReceived(CustomMessage message) {
|
||||
switch (message) {
|
||||
case RemoveCurrentItem:
|
||||
((ItemRowAdapter) mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButton) {
|
||||
switch (((GridButton) item).getId()) {
|
||||
case ENTER_MANUALLY:
|
||||
AuthenticationHelper.enterManualServerAddress(getActivity());
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(getActivity(), item.toString(), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
class SelectUserActivity : FragmentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(android.R.id.content, SelectUserFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.leanback.widget.*
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.browsing.CustomBrowseFragment
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter
|
||||
import org.jellyfin.androidtv.ui.GridButton
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper
|
||||
import org.jellyfin.apiclient.interaction.EmptyResponse
|
||||
|
||||
class SelectUserFragment : CustomBrowseFragment() {
|
||||
override fun addAdditionalRows(rowAdapter: ArrayObjectAdapter) {
|
||||
super.addAdditionalRows(rowAdapter)
|
||||
val manualEntryRowItem = BaseRowItem(
|
||||
GridButton(ENTER_MANUALLY, getString(R.string.lbl_enter_manually), R.drawable.tile_edit)
|
||||
)
|
||||
|
||||
val usersHeader = HeaderItem(rowAdapter.size().toLong(), getString(R.string.lbl_select_user))
|
||||
val usersAdapter = ItemRowAdapter(CardPresenter(), rowAdapter)
|
||||
|
||||
usersAdapter.add(manualEntryRowItem)
|
||||
|
||||
usersAdapter.setRetrieveFinishedListener(
|
||||
UserRetrieveFinishedListener(usersAdapter, manualEntryRowItem)
|
||||
)
|
||||
|
||||
usersAdapter.Retrieve()
|
||||
|
||||
rowAdapter.add(ListRow(usersHeader, usersAdapter))
|
||||
val gridHeader = HeaderItem(rowAdapter.size().toLong(), getString(R.string.lbl_other_options))
|
||||
val mGridPresenter = GridButtonPresenter()
|
||||
val gridRowAdapter = ArrayObjectAdapter(mGridPresenter)
|
||||
|
||||
gridRowAdapter.add(
|
||||
GridButton(
|
||||
SWITCH_SERVER,
|
||||
getString(R.string.lbl_switch_server),
|
||||
R.drawable.tile_port_server
|
||||
)
|
||||
)
|
||||
|
||||
rowAdapter.add(ListRow(gridHeader, gridRowAdapter))
|
||||
}
|
||||
|
||||
override fun setupEventListeners() {
|
||||
super.setupEventListeners()
|
||||
mClickedListener.registerListener(ItemViewClickedListener())
|
||||
}
|
||||
|
||||
private inner class ItemViewClickedListener : OnItemViewClickedListener {
|
||||
override fun onItemClicked(itemViewHolder: Presenter.ViewHolder, item: Any,
|
||||
rowViewHolder: RowPresenter.ViewHolder, row: Row) {
|
||||
if (item is GridButton) {
|
||||
when (item.id) {
|
||||
SWITCH_SERVER -> {
|
||||
// Present server selection
|
||||
val serverIntent = Intent(activity, SelectServerActivity::class.java)
|
||||
serverIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
requireActivity().startActivity(serverIntent)
|
||||
}
|
||||
// Manual login
|
||||
ENTER_MANUALLY -> AuthenticationHelper.enterManualUser(activity)
|
||||
else -> Toast.makeText(activity, item.toString(), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else if (item is BaseRowItem) {
|
||||
if (item.gridButton != null &&
|
||||
item.gridButton.id == ENTER_MANUALLY) {
|
||||
// Manual login
|
||||
AuthenticationHelper.enterManualUser(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class UserRetrieveFinishedListener(
|
||||
private val usersAdapter: ItemRowAdapter,
|
||||
private val rowItem: BaseRowItem
|
||||
) : EmptyResponse() {
|
||||
override fun onResponse() {
|
||||
usersAdapter.remove(rowItem)
|
||||
usersAdapter.add(rowItem)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENTER_MANUALLY = 0
|
||||
private const val SWITCH_SERVER = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.jellyfin.androidtv.R
|
||||
|
||||
class SplashFragment : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_startup, container, false)
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.startup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.ui.browsing.MainActivity;
|
||||
import org.jellyfin.androidtv.ui.itemdetail.FullDetailsActivity;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
public class StartupActivity extends FragmentActivity {
|
||||
private static final int NETWORK_PERMISSION = 1;
|
||||
private TvApp application;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.fragment_startup);
|
||||
|
||||
application = (TvApp) getApplicationContext();
|
||||
|
||||
//Ensure basic permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED)) {
|
||||
Timber.i("Requesting network permissions");
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.INTERNET}, NETWORK_PERMISSION);
|
||||
} else {
|
||||
Timber.i("Basic network permissions are granted");
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (application.getCurrentUser() != null && MediaManager.isPlayingAudio()) {
|
||||
openNextActivity();
|
||||
} else {
|
||||
//clear audio queue in case left over from last run
|
||||
MediaManager.clearAudioQueue();
|
||||
MediaManager.clearVideoQueue();
|
||||
establishConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private void openNextActivity() {
|
||||
// workaround...
|
||||
Activity self = this;
|
||||
String itemId = getIntent().getStringExtra("ItemId");
|
||||
boolean itemIsUserView = getIntent().getBooleanExtra("ItemIsUserView", false);
|
||||
|
||||
if (itemId != null) {
|
||||
if (itemIsUserView) {
|
||||
get(ApiClient.class).GetItemAsync(itemId, get(ApiClient.class).getCurrentUserId(), new Response<BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto item) {
|
||||
ItemLauncher.launchUserView(item, self, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
// go straight into last connection
|
||||
Intent intent = new Intent(application, MainActivity.class);
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Can just go right into details
|
||||
Intent detailsIntent = new Intent(this, FullDetailsActivity.class);
|
||||
detailsIntent.putExtra("ItemId", getIntent().getStringExtra("ItemId"));
|
||||
startActivity(detailsIntent);
|
||||
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
// go straight into last connection
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == NETWORK_PERMISSION) {// If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// permission was granted
|
||||
start();
|
||||
} else {
|
||||
// permission denied! Disable the app.
|
||||
Utils.showToast(this, "Application cannot continue without network");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void establishConnection() {
|
||||
// Ask for server information
|
||||
AuthenticationHelper.enterManualServerAddress(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.TvApp
|
||||
import org.jellyfin.androidtv.data.model.LoadingState
|
||||
import org.jellyfin.androidtv.data.model.ServerList
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.ui.browsing.MainActivity
|
||||
import org.jellyfin.androidtv.ui.itemdetail.FullDetailsActivity
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.util.Utils
|
||||
import org.jellyfin.androidtv.util.apiclient.toUserDto
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import org.jellyfin.apiclient.interaction.Response
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import timber.log.Timber
|
||||
|
||||
class StartupActivity : FragmentActivity() {
|
||||
private companion object {
|
||||
private const val NETWORK_PERMISSION = 1
|
||||
private const val ITEM_ID = "ItemId"
|
||||
}
|
||||
|
||||
private var application: TvApp? = null
|
||||
private val loginViewModel: LoginViewModel by viewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.fragment_content_view)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_view, SplashFragment())
|
||||
.commit()
|
||||
application = applicationContext as TvApp
|
||||
|
||||
//Ensure basic permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED)) {
|
||||
Timber.i("Requesting network permissions")
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.INTERNET), NETWORK_PERMISSION)
|
||||
} else {
|
||||
Timber.i("Basic network permissions are granted")
|
||||
|
||||
val loadingObserver = Observer<ServerList> { serverList ->
|
||||
Timber.d("LoadingState: %s", serverList.state.toString())
|
||||
if (serverList.state == LoadingState.SUCCESS) start()
|
||||
}
|
||||
loginViewModel.serverList.observe(this, loadingObserver)
|
||||
}
|
||||
|
||||
// Navigate to home screen when user has logged in
|
||||
val currentUserObserver = Observer<User> { user: User? ->
|
||||
// TODO: This should be removed in favor of fragments getting the current user directly
|
||||
application!!.currentUser = user?.toUserDto()
|
||||
// User has been logged in continue to home screen
|
||||
// TODO: We should use a fragment for this, but the current fragment depends on BaseActivity
|
||||
val intent = Intent(application, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
loginViewModel.currentUser.observe(this, currentUserObserver)
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
if (application!!.currentUser != null && MediaManager.isPlayingAudio()) {
|
||||
openNextActivity()
|
||||
} else {
|
||||
//clear audio queue in case left over from last run
|
||||
MediaManager.clearAudioQueue()
|
||||
MediaManager.clearVideoQueue()
|
||||
establishConnection()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openNextActivity() {
|
||||
val itemId = intent.getStringExtra(ITEM_ID)
|
||||
val itemIsUserView = intent.getBooleanExtra("ItemIsUserView", false)
|
||||
if (itemId != null) {
|
||||
if (itemIsUserView) {
|
||||
get(ApiClient::class.java).GetItemAsync(itemId, get(ApiClient::class.java).currentUserId, object : Response<BaseItemDto?>() {
|
||||
override fun onResponse(item: BaseItemDto?) {
|
||||
ItemLauncher.launchUserView(item, this@StartupActivity, true)
|
||||
}
|
||||
|
||||
override fun onError(exception: Exception) {
|
||||
// go straight into last connection
|
||||
val intent = Intent(application, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//Can just go right into details
|
||||
val detailsIntent = Intent(this, FullDetailsActivity::class.java)
|
||||
detailsIntent.putExtra(ITEM_ID, intent.getStringExtra(ITEM_ID))
|
||||
startActivity(detailsIntent)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
// go straight into last connection
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
if (requestCode == NETWORK_PERMISSION) { // If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// permission was granted
|
||||
start()
|
||||
} else {
|
||||
// permission denied! Disable the app.
|
||||
Utils.showToast(this, "Application cannot continue without network")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun establishConnection() {
|
||||
// Ask for server information
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_view, StartupToolbarFragment(
|
||||
onAddServerClicked = {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_view, AddServerFragment(
|
||||
onConfirmCallback = {
|
||||
GlobalScope.launch { loginViewModel.connect(it) }
|
||||
},
|
||||
onClose = { supportFragmentManager.popBackStack() }
|
||||
))
|
||||
.addToBackStack(StartupToolbarFragment::class.simpleName)
|
||||
.commit()
|
||||
}
|
||||
))
|
||||
.add(R.id.content_view, ListServerFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.shared.ToolbarFragment
|
||||
|
||||
class StartupToolbarFragment(
|
||||
private val onAddServerClicked: () -> Unit = {}
|
||||
) : ToolbarFragment() {
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
requireActivity().findViewById<LinearLayout>(R.id.toolbar_start).apply {
|
||||
this.addView(Button(requireContext()).apply {
|
||||
text = context.getString(R.string.lbl_add_server)
|
||||
setOnClickListener { onAddServerClicked() }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.jellyfin.androidtv.ui.startup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.ui.shared.AlertFragment
|
||||
import org.jellyfin.androidtv.ui.shared.KeyboardFocusChangeListener
|
||||
|
||||
class UserLoginFragment(
|
||||
private val user: User? = null,
|
||||
private val onConfirmCallback: (username: String, password: String) -> Unit = { _: String, _: String -> },
|
||||
onCancelCallback: () -> Unit = {},
|
||||
private val onClose: () -> Unit = {}
|
||||
) : AlertFragment(
|
||||
title = R.string.lbl_sign_in,
|
||||
onCancelCallback = onCancelCallback,
|
||||
onClose = onClose
|
||||
) {
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val contentView = requireActivity().findViewById<LinearLayout>(R.id.content)
|
||||
|
||||
// Build the username field
|
||||
val username = EditText(activity)
|
||||
username.hint = getString(R.string.lbl_enter_user_name)
|
||||
username.isSingleLine = true
|
||||
if (user != null) {
|
||||
username.setText(user.name)
|
||||
username.isEnabled = false
|
||||
username.inputType = InputType.TYPE_NULL
|
||||
} else {
|
||||
username.onFocusChangeListener = KeyboardFocusChangeListener()
|
||||
username.requestFocus()
|
||||
}
|
||||
// Add the username field to the content view
|
||||
contentView.addView(username)
|
||||
|
||||
// Build the password field
|
||||
val password = EditText(activity)
|
||||
password.hint = getString(R.string.lbl_enter_user_pw)
|
||||
password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
password.isSingleLine = true
|
||||
password.onFocusChangeListener = KeyboardFocusChangeListener()
|
||||
password.nextFocusDownId = R.id.confirm
|
||||
if (user != null) password.requestFocus()
|
||||
// Add the password field to the content view
|
||||
contentView.addView(password)
|
||||
|
||||
// Override the default confirm button click listener to return the address field text
|
||||
val confirmButton = requireActivity().findViewById<Button>(R.id.confirm)
|
||||
confirmButton.setOnClickListener {
|
||||
if (username.text.isNotBlank()) {
|
||||
onConfirmCallback(username.text.toString(), password.text.toString())
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
package org.jellyfin.androidtv.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.media.AudioManager;
|
||||
import android.media.ToneGenerator;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -19,11 +13,6 @@ import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.preference.constant.AudioBehavior;
|
||||
import org.jellyfin.androidtv.ui.startup.DpadPwActivity;
|
||||
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.UserDto;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -180,34 +169,6 @@ public class Utils {
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
public static void processPasswordEntry(Activity activity, UserDto user) {
|
||||
processPasswordEntry(activity, user, null);
|
||||
}
|
||||
|
||||
public static void processPasswordEntry(final Activity activity, final UserDto user, final String directItemId) {
|
||||
if (get(UserPreferences.class).get(UserPreferences.Companion.getPasswordDPadEnabled())) {
|
||||
Intent pwIntent = new Intent(activity, DpadPwActivity.class);
|
||||
pwIntent.putExtra("User", get(GsonJsonSerializer.class).SerializeToString(user));
|
||||
pwIntent.putExtra("ItemId", directItemId);
|
||||
pwIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
activity.startActivity(pwIntent);
|
||||
} else {
|
||||
Timber.d("Requesting dialog...");
|
||||
final EditText password = new EditText(activity);
|
||||
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.password_prompt)
|
||||
.setMessage(TvApp.getApplication().getString(R.string.password_prompt_message, user.getName()))
|
||||
.setView(password)
|
||||
.setPositiveButton(R.string.lbl_ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
String pw = password.getText().toString();
|
||||
AuthenticationHelper.loginUser(user.getName(), pw, get(ApiClient.class), activity, directItemId);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean downMixAudio() {
|
||||
AudioManager am = (AudioManager) TvApp.getApplication().getSystemService(Context.AUDIO_SERVICE);
|
||||
if (am.isBluetoothA2dpOn()) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.jellyfin.androidtv.TvApp
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import org.jellyfin.apiclient.interaction.Response
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto
|
||||
import org.jellyfin.apiclient.model.dto.UserDto
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery
|
||||
import kotlin.coroutines.resume
|
||||
@@ -19,6 +20,16 @@ suspend fun ApiClient.getNextUpEpisodes(query: NextUpQuery): ItemsResult? = susp
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Coroutine capable version of the "getPublicUsers" function
|
||||
*/
|
||||
suspend fun ApiClient.getPublicUsers(): Array<UserDto>? = suspendCoroutine { continuation ->
|
||||
GetPublicUsersAsync(object : Response<Array<UserDto>>() {
|
||||
override fun onResponse(response: Array<UserDto>?) = continuation.resume(response!!)
|
||||
override fun onError(exception: Exception?) = continuation.resume(null)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Coroutine capable version of the "getUserViews" function
|
||||
* Uses the userId of the currently signed in user
|
||||
@@ -29,6 +40,7 @@ suspend fun ApiClient.getUserViews(): ItemsResult? = suspendCoroutine { continua
|
||||
override fun onError(exception: Exception?) = continuation.resume(null)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a coroutine capable version of the "GetItem" function
|
||||
* Uses the userId of the currently signed in user
|
||||
@@ -39,3 +51,10 @@ suspend fun ApiClient.getItem(id: String): BaseItemDto? = suspendCoroutine { con
|
||||
override fun onError(exception: Exception?) = continuation.resume(null)
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun <T : Any> callApi(init: (callback: Response<T>) -> Unit): T = suspendCoroutine { continuation ->
|
||||
init(object : Response<T>() {
|
||||
override fun onResponse(response: T) = continuation.resumeWith(Result.success(response))
|
||||
override fun onError(exception: Exception) = continuation.resumeWith(Result.failure(exception))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package org.jellyfin.androidtv.util.apiclient;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.text.InputType;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.data.model.LogonCredentials;
|
||||
import org.jellyfin.androidtv.ui.browsing.MainActivity;
|
||||
import org.jellyfin.androidtv.ui.itemdetail.FullDetailsActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.KeyboardFocusChangeListener;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo;
|
||||
import org.jellyfin.apiclient.model.users.AuthenticationResult;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
public class AuthenticationHelper {
|
||||
public static void enterManualServerAddress(final Activity activity) {
|
||||
final EditText address = new EditText(activity);
|
||||
address.setHint(activity.getString(R.string.lbl_ip_hint));
|
||||
address.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
address.setOnFocusChangeListener(new KeyboardFocusChangeListener());
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.lbl_enter_server_address))
|
||||
.setMessage(activity.getString(R.string.lbl_valid_server_address))
|
||||
.setView(address)
|
||||
.setNegativeButton(activity.getString(R.string.lbl_cancel), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
// Do nothing.
|
||||
}
|
||||
}).setPositiveButton(activity.getString(R.string.lbl_ok), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
String addressValue = address.getText().toString();
|
||||
Timber.d("Entered address: %s", addressValue);
|
||||
ServerInfo info = new ServerInfo();
|
||||
info.setAddress(addressValue);
|
||||
get(ApiClient.class).EnableAutomaticNetworking(info);
|
||||
AuthenticationHelper.enterManualUser(activity);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
public static void enterManualUser(final Activity activity) {
|
||||
final EditText userName = new EditText(activity);
|
||||
userName.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
userName.setOnFocusChangeListener(new KeyboardFocusChangeListener());
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.lbl_enter_user_name))
|
||||
.setView(userName)
|
||||
.setNegativeButton(activity.getString(R.string.lbl_cancel), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
// Do nothing.
|
||||
}
|
||||
}).setPositiveButton(activity.getString(R.string.lbl_ok), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
String userValue = userName.getText().toString();
|
||||
Timber.d("Entered user: %s", userValue);
|
||||
final EditText userPw = new EditText(activity);
|
||||
userPw.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
userPw.setOnFocusChangeListener(new KeyboardFocusChangeListener());
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.lbl_enter_user_pw))
|
||||
.setView(userPw)
|
||||
.setNegativeButton(activity.getString(R.string.lbl_cancel), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
// Do nothing.
|
||||
}
|
||||
}).setPositiveButton(activity.getString(R.string.lbl_ok), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
loginUser(userName.getText().toString(), userPw.getText().toString(), get(ApiClient.class), activity);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
public static void loginUser(String userName, String pw, ApiClient apiClient, final Activity activity) {
|
||||
loginUser(userName, pw, apiClient, activity, null);
|
||||
}
|
||||
|
||||
public static void loginUser(String userName, String pw, ApiClient apiClient, final Activity activity, final String directEntryItemId) {
|
||||
apiClient.AuthenticateUserAsync(userName, pw, new Response<AuthenticationResult>() {
|
||||
@Override
|
||||
public void onResponse(AuthenticationResult authenticationResult) {
|
||||
TvApp application = TvApp.getApplication();
|
||||
Timber.d("Signed in as %s", authenticationResult.getUser().getName());
|
||||
application.setCurrentUser(authenticationResult.getUser());
|
||||
if (directEntryItemId == null) {
|
||||
Intent intent = new Intent(activity, MainActivity.class);
|
||||
activity.startActivity(intent);
|
||||
} else {
|
||||
Intent intent = new Intent(activity, FullDetailsActivity.class);
|
||||
intent.putExtra("ItemId", directEntryItemId);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
super.onError(exception);
|
||||
Timber.e(exception, "Error logging in");
|
||||
Utils.showToast(activity, activity.getString(R.string.msg_invalid_id_pw));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void saveLoginCredentials(LogonCredentials creds, String fileName) throws IOException {
|
||||
TvApp app = TvApp.getApplication();
|
||||
OutputStream credsFile = app.openFileOutput(fileName, Context.MODE_PRIVATE);
|
||||
credsFile.write(get(GsonJsonSerializer.class).SerializeToString(creds).getBytes());
|
||||
credsFile.close();
|
||||
app.setConfiguredAutoCredentials(creds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.jellyfin.androidtv.util.apiclient
|
||||
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.data.model.UserConfiguration
|
||||
import org.jellyfin.androidtv.data.model.UserPolicy
|
||||
import org.jellyfin.apiclient.discovery.DiscoveryServerInfo
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo
|
||||
import org.jellyfin.apiclient.model.dto.UserDto
|
||||
import org.jellyfin.apiclient.model.system.PublicSystemInfo
|
||||
|
||||
fun DiscoveryServerInfo.toServerInfo(): ServerInfo = ServerInfo().apply {
|
||||
id = this@toServerInfo.id
|
||||
name = this@toServerInfo.name
|
||||
address = this@toServerInfo.address
|
||||
}
|
||||
|
||||
fun DiscoveryServerInfo.toServer(): Server = Server(id, name, address)
|
||||
|
||||
fun PublicSystemInfo.toServer(): Server = Server(id, serverName, localAddress)
|
||||
|
||||
fun ServerInfo.toServer(): Server = Server(id, name, address, userId, accessToken, dateLastAccessed)
|
||||
|
||||
fun Server.toServerInfo(): ServerInfo = ServerInfo().apply {
|
||||
id = this@toServerInfo.id
|
||||
name = this@toServerInfo.name
|
||||
address = this@toServerInfo.address
|
||||
userId = this@toServerInfo.userId
|
||||
accessToken = this@toServerInfo.accessToken
|
||||
dateLastAccessed = this@toServerInfo.dateLastAccessed
|
||||
}
|
||||
|
||||
fun UserDto.toUser(): User = User(
|
||||
id = id,
|
||||
name = name,
|
||||
serverId = serverId,
|
||||
primaryImageTag = primaryImageTag ?: "",
|
||||
hasPassword = hasPassword,
|
||||
hasConfiguredPassword = hasConfiguredPassword,
|
||||
hasConfiguredEasyPassword = hasConfiguredEasyPassword,
|
||||
configuration = UserConfiguration(
|
||||
latestItemsExcludes = configuration.latestItemsExcludes
|
||||
),
|
||||
policy = UserPolicy(
|
||||
enableLiveTvAccess = policy.enableLiveTvAccess,
|
||||
enableLiveTvManagement = policy.enableLiveTvManagement
|
||||
)
|
||||
)
|
||||
|
||||
fun User.toUserDto(): UserDto = UserDto().apply {
|
||||
id = this@toUserDto.id
|
||||
name = this@toUserDto.name
|
||||
serverId = this@toUserDto.serverId
|
||||
primaryImageTag = this@toUserDto.primaryImageTag
|
||||
hasPassword = this@toUserDto.hasPassword
|
||||
hasConfiguredPassword = this@toUserDto.hasConfiguredPassword
|
||||
hasConfiguredEasyPassword = this@toUserDto.hasConfiguredEasyPassword
|
||||
configuration = org.jellyfin.apiclient.model.configuration.UserConfiguration().apply {
|
||||
latestItemsExcludes = this@toUserDto.configuration.latestItemsExcludes
|
||||
}
|
||||
policy = org.jellyfin.apiclient.model.users.UserPolicy().apply {
|
||||
enableLiveTvAccess = this@toUserDto.policy.enableLiveTvAccess
|
||||
enableLiveTvManagement = this@toUserDto.policy.enableLiveTvManagement
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.startup.DpadPwActivity">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:id="@+id/imageView8"
|
||||
android:src="@drawable/dpad_description"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<TextView android:text="@string/enter_password" android:paddingLeft="20sp" android:paddingTop="20sp" android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/dpad_pw_text"
|
||||
android:textSize="32sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/dpad_entry_desc"
|
||||
android:paddingLeft="30sp"
|
||||
android:paddingRight="30sp"
|
||||
android:layout_marginTop="37dp"
|
||||
android:layout_below="@+id/dpad_pw_text"
|
||||
android:layout_alignParentStart="true" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/dpad_pw_cancel"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignStart="@+id/imageView8"
|
||||
android:layout_marginBottom="30sp"
|
||||
android:paddingLeft="30sp"
|
||||
android:maxWidth="450sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:text="000"
|
||||
android:id="@+id/dpad_pw_display"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="125dp"
|
||||
android:password="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="72sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
76
app/src/main/res/layout/fragment_alert_dialog.xml
Normal file
76
app/src/main/res/layout/fragment_alert_dialog.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="48dp"
|
||||
android:layout_marginVertical="27dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="220dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Leanback.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Alert Title" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@id/actions"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text="This is the description of the alert." />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/actions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/jellyfin_button"
|
||||
android:text="@string/lbl_cancel" />
|
||||
|
||||
<Space
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/jellyfin_button"
|
||||
android:text="@string/lbl_ok" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
9
app/src/main/res/layout/fragment_content_view.xml
Normal file
9
app/src/main/res/layout/fragment_content_view.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/content_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.startup.StartupActivity" />
|
||||
@@ -2,8 +2,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.startup.StartupActivity">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fragment_startup_logo"
|
||||
|
||||
53
app/src/main/res/layout/fragment_toolbar.xml
Normal file
53
app/src/main/res/layout/fragment_toolbar.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="95dp"
|
||||
android:background="?android:attr/windowBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbar_start"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginTop="27dp"
|
||||
android:layout_marginBottom="27dp"
|
||||
android:gravity="start"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toStartOf="@+id/toolbar_end"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/toolbar_logo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/app_logo_transparent" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbar_end"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="27dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:layout_marginBottom="27dp"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/toolbar_start">
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/toolbar_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:format12Hour="h:mm"
|
||||
android:format24Hour="k:mm"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -413,4 +413,7 @@
|
||||
<string name="play_with_exo_player">Play with ExoPlayer</string>
|
||||
<string name="play_with_external_app">Play with external app</string>
|
||||
<string name="play_with">Play with</string>
|
||||
<string name="lbl_add_server">Add Server</string>
|
||||
<string name="lbl_sign_in">Please Sign In</string>
|
||||
<string name="lbl_manual_login">Manual Login</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class ServerListTest {
|
||||
@Test
|
||||
fun defaultState() {
|
||||
val serverList = ServerList()
|
||||
assertTrue(serverList.currentServerUsers.isEmpty())
|
||||
assertEquals(LoadingState.PENDING.status, serverList.currentServerUsersState.status)
|
||||
assertTrue(serverList.discoveredServersUsers.isEmpty())
|
||||
assertEquals(LoadingState.PENDING.status, serverList.discoveredServersUsersState.status)
|
||||
assertTrue(serverList.savedServersUsers.isEmpty())
|
||||
assertEquals(LoadingState.PENDING.status, serverList.savedServersUsersState.status)
|
||||
|
||||
assertEquals(LoadingState.PENDING.status, serverList.state.status)
|
||||
assertTrue(serverList.allServersUsers.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorState() {
|
||||
val errorState = LoadingState.error("ERROR!")
|
||||
var serverList = ServerList(
|
||||
currentServerUsersState = errorState,
|
||||
discoveredServersUsersState = LoadingState.SUCCESS,
|
||||
savedServersUsersState = LoadingState.LOADING
|
||||
)
|
||||
assertEquals(errorState, serverList.state)
|
||||
serverList = ServerList(
|
||||
currentServerUsersState = LoadingState.SUCCESS,
|
||||
discoveredServersUsersState = errorState,
|
||||
savedServersUsersState = LoadingState.LOADING
|
||||
)
|
||||
assertEquals(errorState, serverList.state)
|
||||
serverList = ServerList(
|
||||
currentServerUsersState = LoadingState.LOADING,
|
||||
discoveredServersUsersState = LoadingState.SUCCESS,
|
||||
savedServersUsersState = errorState
|
||||
)
|
||||
assertEquals(errorState, serverList.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadingState() {
|
||||
var serverList = ServerList(
|
||||
currentServerUsersState = LoadingState.LOADING,
|
||||
discoveredServersUsersState = LoadingState.SUCCESS
|
||||
)
|
||||
assertEquals(LoadingState.LOADING, serverList.state)
|
||||
serverList = ServerList(
|
||||
currentServerUsersState = LoadingState.SUCCESS,
|
||||
discoveredServersUsersState = LoadingState.LOADING
|
||||
)
|
||||
assertEquals(LoadingState.LOADING, serverList.state)
|
||||
serverList = ServerList(
|
||||
currentServerUsersState = LoadingState.SUCCESS,
|
||||
savedServersUsersState = LoadingState.LOADING
|
||||
)
|
||||
assertEquals(LoadingState.LOADING, serverList.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successState() {
|
||||
var serverList = ServerList(currentServerUsersState = LoadingState.SUCCESS)
|
||||
assertEquals(LoadingState.SUCCESS, serverList.state)
|
||||
serverList = ServerList(discoveredServersUsersState = LoadingState.SUCCESS)
|
||||
assertEquals(LoadingState.SUCCESS, serverList.state)
|
||||
serverList = ServerList(savedServersUsersState = LoadingState.SUCCESS)
|
||||
assertEquals(LoadingState.SUCCESS, serverList.state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.jellyfin.androidtv.util.apiclient
|
||||
|
||||
import org.jellyfin.androidtv.data.model.Server
|
||||
import org.jellyfin.androidtv.data.model.User
|
||||
import org.jellyfin.androidtv.data.model.UserConfiguration
|
||||
import org.jellyfin.androidtv.data.model.UserPolicy
|
||||
import org.jellyfin.apiclient.discovery.DiscoveryServerInfo
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo
|
||||
import org.jellyfin.apiclient.model.dto.UserDto
|
||||
import org.jellyfin.apiclient.model.system.PublicSystemInfo
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
class ModelExtensionsTest {
|
||||
private companion object {
|
||||
private const val SERVER_ID = "id"
|
||||
private const val SERVER_URL = "https://example.com"
|
||||
private const val SERVER_NAME = "Test Server"
|
||||
private const val USER_ID = "userId"
|
||||
private const val USER_NAME = "User Name"
|
||||
private const val ACCESS_TOKEN = "token"
|
||||
private const val IMAGE_TAG = "tag"
|
||||
private val EXCLUDE_ITEMS = arrayOf("exclude")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discoveryServerInfoToServerInfo() {
|
||||
val serverInfo = DiscoveryServerInfo(SERVER_ID, SERVER_URL, SERVER_NAME, "").toServerInfo()
|
||||
assertEquals(SERVER_ID, serverInfo.id)
|
||||
assertEquals(SERVER_URL, serverInfo.address)
|
||||
assertEquals(SERVER_NAME, serverInfo.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discoverServerInfoToServer() {
|
||||
val server = DiscoveryServerInfo(SERVER_ID, SERVER_URL, SERVER_NAME, "").toServer()
|
||||
assertEquals(SERVER_ID, server.id)
|
||||
assertEquals(SERVER_URL, server.address)
|
||||
assertEquals(SERVER_NAME, server.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publicSystemInfoToServer() {
|
||||
val server = PublicSystemInfo().apply {
|
||||
id = SERVER_ID
|
||||
serverName = SERVER_NAME
|
||||
localAddress = SERVER_URL
|
||||
}.toServer()
|
||||
assertEquals(SERVER_ID, server.id)
|
||||
assertEquals(SERVER_URL, server.address)
|
||||
assertEquals(SERVER_NAME, server.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverInfoToServer() {
|
||||
val server = ServerInfo().apply {
|
||||
id = SERVER_ID
|
||||
address = SERVER_URL
|
||||
name = SERVER_NAME
|
||||
}.toServer()
|
||||
assertEquals(SERVER_ID, server.id)
|
||||
assertEquals(SERVER_URL, server.address)
|
||||
assertEquals(SERVER_NAME, server.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverToServerInfo() {
|
||||
val instant = LocalDate.of(2020, 10, 1)
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
val serverInfo = Server(
|
||||
SERVER_ID,
|
||||
SERVER_NAME,
|
||||
SERVER_URL,
|
||||
USER_ID,
|
||||
ACCESS_TOKEN,
|
||||
Date.from(instant)
|
||||
).toServerInfo()
|
||||
assertEquals(SERVER_ID, serverInfo.id)
|
||||
assertEquals(SERVER_NAME, serverInfo.name)
|
||||
assertEquals(SERVER_URL, serverInfo.address)
|
||||
assertEquals(USER_ID, serverInfo.userId)
|
||||
assertEquals(ACCESS_TOKEN, serverInfo.accessToken)
|
||||
assertEquals(instant, serverInfo.dateLastAccessed.toInstant())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userDtoToUser() {
|
||||
val user = UserDto().apply {
|
||||
id = USER_ID
|
||||
name = USER_NAME
|
||||
serverId = SERVER_ID
|
||||
primaryImageTag = IMAGE_TAG
|
||||
hasPassword = true
|
||||
hasConfiguredPassword = true
|
||||
hasConfiguredEasyPassword = false
|
||||
policy = org.jellyfin.apiclient.model.users.UserPolicy().apply {
|
||||
enableLiveTvAccess = false
|
||||
enableLiveTvManagement = false
|
||||
}
|
||||
configuration = org.jellyfin.apiclient.model.configuration.UserConfiguration().apply {
|
||||
latestItemsExcludes = EXCLUDE_ITEMS
|
||||
}
|
||||
}.toUser()
|
||||
assertEquals(USER_ID, user.id)
|
||||
assertEquals(USER_NAME, user.name)
|
||||
assertEquals(SERVER_ID, user.serverId)
|
||||
assertEquals(IMAGE_TAG, user.primaryImageTag)
|
||||
assertTrue(user.hasPassword)
|
||||
assertTrue(user.hasConfiguredPassword)
|
||||
assertFalse(user.hasConfiguredEasyPassword)
|
||||
assertFalse(user.policy.enableLiveTvAccess)
|
||||
assertFalse(user.policy.enableLiveTvManagement)
|
||||
assertTrue(EXCLUDE_ITEMS.contentEquals(user.configuration.latestItemsExcludes))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userToUserDto() {
|
||||
val userDto = User(
|
||||
id = USER_ID,
|
||||
name = USER_NAME,
|
||||
serverId = SERVER_ID,
|
||||
primaryImageTag = IMAGE_TAG,
|
||||
hasPassword = true,
|
||||
hasConfiguredPassword = true,
|
||||
hasConfiguredEasyPassword = false,
|
||||
configuration = UserConfiguration(
|
||||
latestItemsExcludes = EXCLUDE_ITEMS
|
||||
),
|
||||
policy = UserPolicy(
|
||||
enableLiveTvAccess = false,
|
||||
enableLiveTvManagement = false
|
||||
)
|
||||
).toUserDto()
|
||||
assertEquals(USER_ID, userDto.id)
|
||||
assertEquals(USER_NAME, userDto.name)
|
||||
assertEquals(SERVER_ID, userDto.serverId)
|
||||
assertEquals(IMAGE_TAG, userDto.primaryImageTag)
|
||||
assertTrue(userDto.hasPassword)
|
||||
assertTrue(userDto.hasConfiguredPassword)
|
||||
assertFalse(userDto.hasConfiguredEasyPassword)
|
||||
assertTrue(EXCLUDE_ITEMS.contentEquals(userDto.configuration.latestItemsExcludes))
|
||||
assertFalse(userDto.policy.enableLiveTvAccess)
|
||||
assertFalse(userDto.policy.enableLiveTvManagement)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user