From cef3eb1aa7c0aa175c364ce8469913194a9332d2 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 13 Sep 2022 13:03:53 +0200 Subject: [PATCH 1/3] Rename .java to .kt --- .../jellyfin/androidtv/util/{DeviceUtils.java => DeviceUtils.kt} | 0 .../java/org/jellyfin/androidtv/util/{Utils.java => Utils.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/jellyfin/androidtv/util/{DeviceUtils.java => DeviceUtils.kt} (100%) rename app/src/main/java/org/jellyfin/androidtv/util/{Utils.java => Utils.kt} (100%) diff --git a/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.java b/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt similarity index 100% rename from app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.java rename to app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt diff --git a/app/src/main/java/org/jellyfin/androidtv/util/Utils.java b/app/src/main/java/org/jellyfin/androidtv/util/Utils.kt similarity index 100% rename from app/src/main/java/org/jellyfin/androidtv/util/Utils.java rename to app/src/main/java/org/jellyfin/androidtv/util/Utils.kt From 4a77646141c4cf0457d8cad7c666271ea2d87f9f Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 13 Sep 2022 13:03:54 +0200 Subject: [PATCH 2/3] Rewrite DeviceUtils in Kotlin --- .../androidtv/preference/UserPreferences.kt | 4 +- .../preference/constant/AudioBehavior.kt | 2 +- .../jellyfin/androidtv/util/DeviceUtils.kt | 128 +++++++----------- .../androidtv/util/profile/ProfileHelper.kt | 6 +- app/src/test/kotlin/util/DeviceUtilsTests.kt | 36 ++--- 5 files changed, 76 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt index cbb6005f38..6f1a0a5260 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt @@ -114,7 +114,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore( /** * Enable AC3 */ - var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1()) + var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1) /** * Default audio delay in milliseconds for libVLC @@ -218,7 +218,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore( putInt("libvlc_audio_delay", it.getLong("libvlc_audio_delay", 0).toInt()) // Disable AC3 (Dolby Digital) on Fire Stick Gen 1 devices - if (DeviceUtils.isFireTvStickGen1()) putBoolean("pref_bitstream_ac3", false) + if (DeviceUtils.isFireTvStickGen1) putBoolean("pref_bitstream_ac3", false) } // v0.13.3 to v0.13.4 diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioBehavior.kt b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioBehavior.kt index 7f37853c64..3c9b3d8267 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioBehavior.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioBehavior.kt @@ -18,5 +18,5 @@ enum class AudioBehavior { DOWNMIX_TO_STEREO } -val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV()) AudioBehavior.DOWNMIX_TO_STEREO +val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV) AudioBehavior.DOWNMIX_TO_STEREO else AudioBehavior.DIRECT_STREAM diff --git a/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt b/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt index 9897fcbe4d..09ad9c3e5f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt @@ -1,77 +1,53 @@ -package org.jellyfin.androidtv.util; - -import android.os.Build; - -import androidx.annotation.NonNull; - -import java.util.Arrays; - -public class DeviceUtils { - // Chromecast with Google TV - private static final String CHROMECAST_GOOGLE_TV = "Chromecast"; - - private static final String FIRE_TV_PREFIX = "AFT"; - // Fire TV Stick Models - private static final String FIRE_STICK_MODEL_GEN_1 = "AFTM"; - private static final String FIRE_STICK_MODEL_GEN_2 = "AFTT"; - private static final String FIRE_STICK_MODEL_GEN_3 = "AFTSSS"; - private static final String FIRE_STICK_LITE_MODEL = "AFTSS"; - private static final String FIRE_STICK_4K_MODEL = "AFTMM"; - private static final String FIRE_STICK_4K_MAX_MODEL = "AFTKA"; - // Fire TV Cube Models - private static final String FIRE_CUBE_MODEL_GEN_1 = "AFTA"; - private static final String FIRE_CUBE_MODEL_GEN_2 = "AFTR"; - // Fire TV (Box) Models - private static final String FIRE_TV_MODEL_GEN_1 = "AFTB"; - private static final String FIRE_TV_MODEL_GEN_2 = "AFTS"; - private static final String FIRE_TV_MODEL_GEN_3 = "AFTN"; - // Nvidia Shield TV Model - private static final String SHIELD_TV_MODEL = "SHIELD Android TV"; - - private static final String UNKNOWN = "Unknown"; - - @NonNull - static String getBuildModel() { - // Stub to allow for mock injection - return Build.MODEL != null ? Build.MODEL : UNKNOWN; - } - - public static boolean isChromecastWithGoogleTV() { - return getBuildModel().equals(CHROMECAST_GOOGLE_TV); - } - - public static boolean isFireTv() { - return getBuildModel().startsWith(FIRE_TV_PREFIX); - } - - public static boolean isFireTvStickGen1() { - return getBuildModel().equals(FIRE_STICK_MODEL_GEN_1); - } - - public static boolean isFireTvStick4k() { - return Arrays.asList(FIRE_STICK_4K_MODEL, FIRE_STICK_4K_MAX_MODEL) - .contains(getBuildModel()); - } - - public static boolean isShieldTv() { - return getBuildModel().equals(SHIELD_TV_MODEL); - } - - public static boolean has4kVideoSupport() { - String buildModel = getBuildModel(); - - return !Arrays.asList( - // These devices only support a max video resolution of 1080p - FIRE_STICK_MODEL_GEN_1, - FIRE_STICK_MODEL_GEN_2, - FIRE_STICK_MODEL_GEN_3, - FIRE_STICK_LITE_MODEL, - FIRE_TV_MODEL_GEN_1, - FIRE_TV_MODEL_GEN_2 - ).contains(buildModel) && !buildModel.equals(UNKNOWN); - } - - public static boolean is60() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - } +package org.jellyfin.androidtv.util + +import android.os.Build + +object DeviceUtils { + // Chromecast with Google TV + private const val CHROMECAST_GOOGLE_TV = "Chromecast" + private const val FIRE_TV_PREFIX = "AFT" + + // Fire TV Stick Models + private const val FIRE_STICK_MODEL_GEN_1 = "AFTM" + private const val FIRE_STICK_MODEL_GEN_2 = "AFTT" + private const val FIRE_STICK_MODEL_GEN_3 = "AFTSSS" + private const val FIRE_STICK_LITE_MODEL = "AFTSS" + private const val FIRE_STICK_4K_MODEL = "AFTMM" + private const val FIRE_STICK_4K_MAX_MODEL = "AFTKA" + + // Fire TV Cube Models + private const val FIRE_CUBE_MODEL_GEN_1 = "AFTA" + private const val FIRE_CUBE_MODEL_GEN_2 = "AFTR" + + // Fire TV (Box) Models + private const val FIRE_TV_MODEL_GEN_1 = "AFTB" + private const val FIRE_TV_MODEL_GEN_2 = "AFTS" + private const val FIRE_TV_MODEL_GEN_3 = "AFTN" + + // Nvidia Shield TV Model + private const val SHIELD_TV_MODEL = "SHIELD Android TV" + private const val UNKNOWN = "Unknown" + + // Stub to allow for mock injection + fun getBuildModel(): String = Build.MODEL ?: UNKNOWN + + @JvmStatic val isChromecastWithGoogleTV: Boolean get() = getBuildModel() == CHROMECAST_GOOGLE_TV + @JvmStatic val isFireTv: Boolean get() = getBuildModel().startsWith(FIRE_TV_PREFIX) + @JvmStatic val isFireTvStickGen1: Boolean get() = getBuildModel() == FIRE_STICK_MODEL_GEN_1 + @JvmStatic val isFireTvStick4k: Boolean get() = getBuildModel() in listOf(FIRE_STICK_4K_MODEL, FIRE_STICK_4K_MAX_MODEL) + @JvmStatic val isShieldTv: Boolean get() = getBuildModel() == SHIELD_TV_MODEL + + @JvmStatic + fun has4kVideoSupport(): Boolean = getBuildModel() != UNKNOWN && getBuildModel() !in listOf( + // These devices only support a max video resolution of 1080p + FIRE_STICK_MODEL_GEN_1, + FIRE_STICK_MODEL_GEN_2, + FIRE_STICK_MODEL_GEN_3, + FIRE_STICK_LITE_MODEL, + FIRE_TV_MODEL_GEN_1, + FIRE_TV_MODEL_GEN_2 + ) + + @JvmStatic + fun is60(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } diff --git a/app/src/main/java/org/jellyfin/androidtv/util/profile/ProfileHelper.kt b/app/src/main/java/org/jellyfin/androidtv/util/profile/ProfileHelper.kt index 64365a2065..66a325a47f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/profile/ProfileHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/profile/ProfileHelper.kt @@ -69,9 +69,9 @@ object ProfileHelper { ProfileConditionValue.VideoLevel, when { // https://developer.amazon.com/docs/fire-tv/device-specifications.html - DeviceUtils.isFireTvStick4k() -> H264_LEVEL_5_2 - DeviceUtils.isFireTv() -> H264_LEVEL_4_1 - DeviceUtils.isShieldTv() -> H264_LEVEL_5_2 + DeviceUtils.isFireTvStick4k -> H264_LEVEL_5_2 + DeviceUtils.isFireTv -> H264_LEVEL_4_1 + DeviceUtils.isShieldTv -> H264_LEVEL_5_2 else -> H264_LEVEL_5_1 } ) diff --git a/app/src/test/kotlin/util/DeviceUtilsTests.kt b/app/src/test/kotlin/util/DeviceUtilsTests.kt index d542264617..c598f099db 100644 --- a/app/src/test/kotlin/util/DeviceUtilsTests.kt +++ b/app/src/test/kotlin/util/DeviceUtilsTests.kt @@ -3,8 +3,8 @@ package org.jellyfin.androidtv.util import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic +import io.mockk.mockkObject +import io.mockk.unmockkObject class DeviceUtilsTests : FunSpec({ test("DeviceUtils.getBuildModel() is unknown") { @@ -12,56 +12,56 @@ class DeviceUtilsTests : FunSpec({ } test("DeviceUtils methods support unknown as model") { - DeviceUtils.isChromecastWithGoogleTV() shouldBe false - DeviceUtils.isFireTv() shouldBe false - DeviceUtils.isFireTvStickGen1() shouldBe false - DeviceUtils.isFireTvStick4k() shouldBe false - DeviceUtils.isShieldTv() shouldBe false + DeviceUtils.isChromecastWithGoogleTV shouldBe false + DeviceUtils.isFireTv shouldBe false + DeviceUtils.isFireTvStickGen1 shouldBe false + DeviceUtils.isFireTvStick4k shouldBe false + DeviceUtils.isShieldTv shouldBe false DeviceUtils.has4kVideoSupport() shouldBe false } fun withBuildModel(buildModel: String, block: () -> Unit) { - mockkStatic(DeviceUtils::class) + mockkObject(DeviceUtils) every { DeviceUtils.getBuildModel() } returns buildModel block() - unmockkStatic(DeviceUtils::class) + unmockkObject(DeviceUtils) } test("DeviceUtils.isChromecastWithGoogleTV() works correctly") { withBuildModel("Chromecast") { - DeviceUtils.isChromecastWithGoogleTV() shouldBe true + DeviceUtils.isChromecastWithGoogleTV shouldBe true } } test("DeviceUtils.isFireTv() works correctly") { arrayOf("AFT", "AFT_foo", "AFT ", "AFT2").forEach { input -> withBuildModel(input) { - DeviceUtils.isFireTv() shouldBe true + DeviceUtils.isFireTv shouldBe true } } } test("DeviceUtils.isFireTvStickGen1() works correctly") { withBuildModel("AFTM") { - DeviceUtils.isFireTv() shouldBe true - DeviceUtils.isFireTvStickGen1() shouldBe true - DeviceUtils.isFireTvStick4k() shouldBe false + DeviceUtils.isFireTv shouldBe true + DeviceUtils.isFireTvStickGen1 shouldBe true + DeviceUtils.isFireTvStick4k shouldBe false } } test("DeviceUtils.isFireTvStick4k() works correctly") { arrayOf("AFTMM", "AFTKA").forEach { input -> withBuildModel(input) { - DeviceUtils.isFireTv() shouldBe true - DeviceUtils.isFireTvStick4k() shouldBe true - DeviceUtils.isFireTvStickGen1() shouldBe false + DeviceUtils.isFireTv shouldBe true + DeviceUtils.isFireTvStick4k shouldBe true + DeviceUtils.isFireTvStickGen1 shouldBe false } } } test("DeviceUtils.isShieldTv() works correctly") { withBuildModel("SHIELD Android TV") { - DeviceUtils.isShieldTv() shouldBe true + DeviceUtils.isShieldTv shouldBe true } } From a98c73a2856b4ec4c7d1ce1c7e19172e677ff58d Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 13 Sep 2022 13:19:22 +0200 Subject: [PATCH 3/3] Rewrite Utils in Kotlin --- .../java/org/jellyfin/androidtv/util/Utils.kt | 249 ++++++++---------- 1 file changed, 112 insertions(+), 137 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/util/Utils.kt b/app/src/main/java/org/jellyfin/androidtv/util/Utils.kt index b62fb98937..896d09c6b9 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/Utils.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/Utils.kt @@ -1,142 +1,117 @@ -package org.jellyfin.androidtv.util; - -import android.content.Context; -import android.content.res.TypedArray; -import android.media.AudioManager; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -import org.jellyfin.androidtv.preference.UserPreferences; -import org.jellyfin.androidtv.preference.constant.AudioBehavior; -import org.jellyfin.sdk.model.api.UserDto; -import org.koin.java.KoinJavaComponent; - -import java.util.Arrays; -import java.util.Iterator; - -import timber.log.Timber; +package org.jellyfin.androidtv.util + +import android.content.Context +import android.media.AudioManager +import android.widget.Toast +import org.jellyfin.androidtv.preference.UserPreferences +import org.jellyfin.androidtv.preference.UserPreferences.Companion.audioBehaviour +import org.jellyfin.androidtv.preference.constant.AudioBehavior +import org.jellyfin.sdk.model.api.UserDto +import org.koin.java.KoinJavaComponent.get +import timber.log.Timber +import kotlin.math.roundToInt /** * A collection of utility methods, all static. */ -public class Utils { - /** - * Shows a (long) toast - * - * @param context - * @param msg - */ - public static void showToast(Context context, String msg) { - Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); - } - - /** - * Shows a (long) toast. - * - * @param context - * @param resourceId - */ - public static void showToast(Context context, int resourceId) { - Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show(); - } - - public static int convertDpToPixel(@NonNull Context ctx, int dp) { - return convertDpToPixel(ctx, (float) dp); - } - - public static int convertDpToPixel(@NonNull Context ctx, float dp) { - float density = ctx.getResources().getDisplayMetrics().density; - return Math.round(dp * density); - } - - public static boolean isTrue(Boolean value) { - return value != null && value; - } - - /** - * A null safe version of {@code String.equalsIgnoreCase}. - */ - public static boolean equalsIgnoreCase(String str1, String str2) { - if (str1 == null && str2 == null) { - return true; - } - if (str1 == null || str2 == null) { - return false; - } - return str1.equalsIgnoreCase(str2); - } - - public static T getSafeValue(T value, T defaultValue) { - if (value == null) return defaultValue; - return value; - } - - public static boolean isEmpty(String value) { - return value == null || value.equals(""); - } - - public static boolean isNonEmpty(String value) { - return value != null && !value.equals(""); - } - - public static String join(String separator, Iterable items) { - StringBuilder builder = new StringBuilder(); - - Iterator iterator = items.iterator(); - while (iterator.hasNext()) { - builder.append(iterator.next()); - - if (iterator.hasNext()) { - builder.append(separator); - } - } - - return builder.toString(); - } - - public static String join(String separator, String... items) { - return join(separator, Arrays.asList(items)); - } - - public static int getMaxBitrate() { - String maxRate = KoinJavaComponent.get(UserPreferences.class).get(UserPreferences.Companion.getMaxBitrate()); - Long autoRate = KoinJavaComponent.get(AutoBitrate.class).getBitrate(); - if (maxRate.equals(UserPreferences.MAX_BITRATE_AUTO) && autoRate != null) { - return autoRate.intValue(); - } else { - return (int) (Float.parseFloat(maxRate) * 1_000_000); - } - } - - public static int getThemeColor(@NonNull Context context, int resourceId) { - TypedArray styledAttributes = context.getTheme() - .obtainStyledAttributes(new int[]{resourceId}); - int themeColor = styledAttributes.getColor(0, 0); - styledAttributes.recycle(); - - return themeColor; - } - - public static boolean downMixAudio(@NonNull Context context) { - AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - if (am.isBluetoothA2dpOn()) { - Timber.i("Downmixing audio due to wired headset"); - return true; - } - - return KoinJavaComponent.get(UserPreferences.class).get(UserPreferences.Companion.getAudioBehaviour()) == AudioBehavior.DOWNMIX_TO_STEREO; - } - - public static long getSafeSeekPosition(long position, long duration) { - if (position < 0 || duration < 0) - return 0; - if (position >= duration) - return Math.max(duration - 1000, 0); - return position; - } - - public static boolean canManageRecordings(UserDto user) { - return user != null && user.getPolicy().getEnableLiveTvManagement(); - } +object Utils { + /** + * Shows a (long) toast + */ + @JvmStatic + @Deprecated( + message = "Use Toast.makeText", + replaceWith = ReplaceWith( + expression = "Toast.makeText(context, msg, Toast.LENGTH_LONG).show()", + imports = ["android.widget.Toast"] + ) + ) + fun showToast(context: Context?, msg: String?) { + Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + } + + /** + * Shows a (long) toast. + */ + @JvmStatic + @Deprecated( + message = "Use Toast.makeText", + replaceWith = ReplaceWith( + expression = "Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show()", + imports = ["android.widget.Toast"] + ) + ) + fun showToast(context: Context, resourceId: Int) { + Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show() + } + + @JvmStatic + fun convertDpToPixel(ctx: Context, dp: Int): Int = (dp * ctx.resources.displayMetrics.density).roundToInt() + + @JvmStatic + fun isTrue(value: Boolean?): Boolean = value == true + + /** + * A null safe version of `String.equalsIgnoreCase`. + */ + @JvmStatic + fun equalsIgnoreCase(str1: String?, str2: String?): Boolean = when { + str1 == null && str2 == null -> true + str1 == null || str2 == null -> false + else -> str1.equals(str2, ignoreCase = true) + } + + @JvmStatic + fun getSafeValue(value: T?, defaultValue: T): T = value ?: defaultValue + + @JvmStatic + fun isEmpty(value: String?): Boolean = value.isNullOrEmpty() + + @JvmStatic + fun isNonEmpty(value: String?): Boolean = !value.isNullOrEmpty() + + @JvmStatic + fun join(separator: String, items: Iterable): String = items.joinToString(separator = separator) + + @JvmStatic + fun join(separator: String, vararg items: String?): String = join(separator, items.toList()) + + @JvmStatic + fun getMaxBitrate(): Int { + val maxRate = get(UserPreferences::class.java)[UserPreferences.maxBitrate] + val autoRate = get(AutoBitrate::class.java).bitrate + + return when { + maxRate == UserPreferences.MAX_BITRATE_AUTO && autoRate != null -> autoRate.toInt() + else -> (maxRate.toFloat() * 1000000).toInt() + } + } + + @JvmStatic + fun getThemeColor(context: Context, resourceId: Int): Int { + val styledAttributes = context.theme.obtainStyledAttributes(intArrayOf(resourceId)) + val themeColor = styledAttributes.getColor(0, 0) + styledAttributes.recycle() + return themeColor + } + + @JvmStatic + fun downMixAudio(context: Context): Boolean { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + if (am.isBluetoothA2dpOn) { + Timber.i("Downmixing audio due to wired headset") + return true + } + + return get(UserPreferences::class.java)[audioBehaviour] === AudioBehavior.DOWNMIX_TO_STEREO + } + + @JvmStatic + fun getSafeSeekPosition(position: Long, duration: Long): Long = when { + position >= duration -> (duration - 1000).coerceAtLeast(0) + else -> position.coerceAtLeast(0) + } + + @JvmStatic + fun canManageRecordings(user: UserDto?): Boolean = user?.policy?.enableLiveTvManagement == true }