Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,23 @@ public boolean isLeanbackSupported() {
return pm.hasSystemFeature(ANDROID_SOFTWARE_LEANBACK);
}

public boolean isTclAndroidTv(Context context) {
String manufacturer = android.os.Build.MANUFACTURER != null ?
android.os.Build.MANUFACTURER.toLowerCase() : "";
String brand = android.os.Build.BRAND != null ?
android.os.Build.BRAND.toLowerCase() : "";
String model = android.os.Build.MODEL != null ?
android.os.Build.MODEL.toLowerCase() : "";
public boolean isTclAndroidTv(Context context) {
String manufacturer =
android.os.Build.MANUFACTURER != null ? android.os.Build.MANUFACTURER.toLowerCase() : "";
String brand = android.os.Build.BRAND != null ? android.os.Build.BRAND.toLowerCase() : "";
String model = android.os.Build.MODEL != null ? android.os.Build.MODEL.toLowerCase() : "";

boolean isTcl = manufacturer.contains("tcl") || brand.contains("tcl");
boolean isTcl = manufacturer.contains("tcl") || brand.contains("tcl");

// Some older models report "beyondtv" in the model name
boolean isTclModel = model.contains("tcl") || model.contains("beyondtv");
// Some older models report "beyondtv" in the model name
boolean isTclModel = model.contains("tcl") || model.contains("beyondtv");

// Verify it is actually a TV device (Leanback)
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
// Verify it is actually a TV device (Leanback)
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
Comment on lines +122 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential NullPointerException: getSystemService() can return null.

context.getSystemService(Context.UI_MODE_SERVICE) may return null on certain devices or configurations. Calling getCurrentModeType() without a null check will cause a crash.

Proposed fix
     // Verify it is actually a TV device (Leanback)
     UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
-    boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+    boolean isTv = uiModeManager != null 
+        && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager != null
&& uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
🤖 Prompt for AI Agents
In
`@serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java`
around lines 122 - 123, The call to
context.getSystemService(Context.UI_MODE_SERVICE) can return null, so update the
AndroidHelper logic around uiModeManager and the isTv computation to null-check
the returned UiModeManager before calling getCurrentModeType(); if uiModeManager
is null, set isTv to false (or a safe default) and log or handle the missing
service. Locate the uiModeManager declaration and the isTv assignment in
AndroidHelper and guard the getCurrentModeType() call with an explicit null
check to avoid a NullPointerException.


return (isTcl || isTclModel) && isTv;
}
return (isTcl || isTclModel) && isTv;
}

@OptIn(markerClass = UnstableApi.class)
public boolean isAudioPassthroughSupported(String codec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,5 @@ class DataSourceFactoryProvider : Provider<DataSource.Factory> {
return CacheDataSource.Factory()
.setCache(SerenityApplication.simpleCache)
.setUpstreamDataSourceFactory(defaultDataSourceFactory)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ import us.nineworlds.serenity.common.rest.SerenityUser
class LoginRepository(private val client: SerenityClient) {

suspend fun loadAllUsers(): Result<List<SerenityUser>> = withContext(Dispatchers.IO) {
val users = client.allAvailableUsers()
Result.Success<List<SerenityUser>>(users)
try {
val users = client.allAvailableUsers()
Result.Success<List<SerenityUser>>(users)
} catch (e: Exception) {
Result.Error(e)
}
}

suspend fun authenticateUser(user: SerenityUser, password: String? = null): Result<SerenityUser> = withContext(Dispatchers.IO) {
val authenticatedUser = client.authenticateUser(user, password)
Result.Success<SerenityUser>(authenticatedUser)
try {
val authenticatedUser = client.authenticateUser(user, password)
Result.Success<SerenityUser>(authenticatedUser)
} catch (e: Exception) {
Result.Error(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ package us.nineworlds.serenity.core.repository

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton
import androidx.datastore.preferences.SharedPreferencesMigration

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "serenity_preferences",
Expand All @@ -19,9 +26,7 @@ val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
)

@Singleton
class SettingsRepository @Inject constructor(
private val context: Context
) {
class SettingsRepository @Inject constructor(private val context: Context) {

fun getString(key: String, defaultValue: String?): String? = runBlocking {
val prefKey = stringPreferencesKey(key)
Expand Down Expand Up @@ -72,12 +77,12 @@ class SettingsRepository @Inject constructor(
// DataStore doesn't have a direct 'contains' that is efficient without reading.
// We check if the key exists in the current preferences.
context.dataStore.data.map { preferences ->
preferences.contains(stringPreferencesKey(key)) ||
preferences.contains(booleanPreferencesKey(key)) ||
preferences.contains(intPreferencesKey(key)) ||
preferences.contains(longPreferencesKey(key)) ||
preferences.contains(floatPreferencesKey(key)) ||
preferences.contains(doublePreferencesKey(key))
preferences.contains(stringPreferencesKey(key)) ||
preferences.contains(booleanPreferencesKey(key)) ||
preferences.contains(intPreferencesKey(key)) ||
preferences.contains(longPreferencesKey(key)) ||
preferences.contains(floatPreferencesKey(key)) ||
preferences.contains(doublePreferencesKey(key))
}.first()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class LoginUserActivity :
server?.let { presenter.initPresenter(server) }
}

binding.retryButton.setOnClickListener {
progressBinding.dataLoadingContainer.visibility = VISIBLE
binding.retryButton.visibility = GONE
presenter.retrieveAllUsers()
}

presenter.retrieveAllUsers()
}

Expand All @@ -83,6 +89,7 @@ class LoginUserActivity :
override fun displayUsers(serenityUser: List<SerenityUser>) {
progressBinding.dataLoadingContainer.visibility = GONE
binding.apply {
retryButton.visibility = GONE
loginUserContainer.visibility = VISIBLE
adapter.loadUsers(serenityUser)
loginUserContainer.requestFocusFromTouch()
Expand All @@ -95,6 +102,13 @@ class LoginUserActivity :
finish()
}

override fun showError() {
progressBinding.dataLoadingContainer.visibility = GONE
binding.loginUserContainer.visibility = GONE
binding.retryButton.visibility = VISIBLE
binding.retryButton.requestFocus()
}

override fun onUserSelected(user: SerenityUser) {
if (user.hasPassword()) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ interface LoginUserContract {

@StateStrategyType(AddToEndSingleStrategy::class)
fun launchNextScreen()

@StateStrategyType(AddToEndSingleStrategy::class)
fun showError()
}

interface LoginUserPresnter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class LoginUserPresenter :
is Success<List<SerenityUser>> -> viewState.displayUsers(result.data)

else -> {
// Error state
viewState.showError()
}
}
}
Expand All @@ -66,7 +66,7 @@ class LoginUserPresenter :
is Success<SerenityUser> -> viewState.launchNextScreen()

else -> {
// Error State
viewState.showError()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ interface ExoplayerContract {

@StateStrategyType(AddToEndSingleStrategy::class)
fun showResumeDialog(video: VideoContentInfo)

@StateStrategyType(AddToEndSingleStrategy::class)
fun updateSerenityDebugInfo(isTranscoding: Boolean, videoCodec: String?, audioCodec: String?, bitrate: Int)

@StateStrategyType(AddToEndSingleStrategy::class)
fun toggleDebugView()
}

interface ExoplayerPresenter {
Expand All @@ -54,5 +60,7 @@ interface ExoplayerContract {
fun startPlaying()

fun playVideo()

fun toggleDebugMode()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ExoplayerPresenter :

private var onScreenControllerShowing: Boolean = false
private var playSessionId: String? = null
private var isTranscoding: Boolean = false

override fun attachView(view: ExoplayerView?) {
Toothpick.inject(this, Toothpick.openScope(InjectionConstants.APPLICATION_SCOPE))
Expand Down Expand Up @@ -160,6 +161,7 @@ class ExoplayerPresenter :
override fun playVideo() {
val videoUrl: String = transcoderUrl()
startPlaying()
viewState.updateSerenityDebugInfo(isTranscoding, video.videoCodec, video.audioCodec, 0)
viewState.initializePlayer(videoUrl, video.resumeOffset)
}

Expand All @@ -181,14 +183,20 @@ class ExoplayerPresenter :
logger.debug("ExoPlayerPresenter: Container: ${video.container} Audio: ${video.audioCodec}")
if (isDirectPlaySupportedForContainer(video)) {
logger.debug("ExoPlayerPresenter: Direct playing ${video.directPlayUrl}")
isTranscoding = false
return video.directPlayUrl.orEmpty()
}

val transcodingUrl = serenityClient.createTranscodeUrl(video.id().orEmpty(), video.resumeOffset)

logger.debug("ExoPlayerPresenter: Transcoding Url: $transcodingUrl")
isTranscoding = true
return transcodingUrl
}

private fun selectCodec(mimeType: String): Boolean = MediaCodecInfoUtil.isCodecSupported(mimeType)

override fun toggleDebugMode() {
viewState.toggleDebugView()
}
}
Loading