summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimonx22 <simon@oatmealdome.me>2026-04-26 12:52:56 -0400
committerSimonx22 <simon@oatmealdome.me>2026-04-30 16:23:32 -0400
commita7e98c776d90938a066b0eb2603ff7b69a5164b5 (patch)
treec87638f2549b0676bfaf067f7c6e8eaab4244a6c
parentaf15769d57ddaf4b3f79801a2174c67a19150050 (diff)
Android: Convert TvUtil to Kotlin
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java296
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.kt247
2 files changed, 247 insertions, 296 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java
deleted file mode 100644
index 37ef3eca62..0000000000
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java
+++ /dev/null
@@ -1,296 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.dolphinemu.dolphinemu.utils;
-
-import android.annotation.TargetApi;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.VectorDrawable;
-import android.net.Uri;
-import android.os.Build;
-import android.os.PersistableBundle;
-import android.util.Log;
-
-import androidx.annotation.AnyRes;
-import androidx.annotation.NonNull;
-import androidx.tvprovider.media.tv.Channel;
-import androidx.tvprovider.media.tv.TvContractCompat;
-
-import org.dolphinemu.dolphinemu.model.GameFile;
-import org.dolphinemu.dolphinemu.model.HomeScreenChannel;
-import org.dolphinemu.dolphinemu.services.SyncChannelJobService;
-import org.dolphinemu.dolphinemu.services.SyncProgramsJobService;
-import org.dolphinemu.dolphinemu.ui.platform.PlatformTab;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.List;
-
-import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
-import static androidx.core.content.FileProvider.getUriForFile;
-
-/**
- * Assists in TV related services, e.g., home screen channels
- */
-public class TvUtil
-{
- private static final String TAG = "TvUtil";
- private static final long CHANNEL_JOB_ID_OFFSET = 1000;
-
- private static final String[] CHANNELS_PROJECTION = {
- TvContractCompat.Channels._ID,
- TvContractCompat.Channels.COLUMN_DISPLAY_NAME,
- TvContractCompat.Channels.COLUMN_BROWSABLE,
- TvContractCompat.Channels.COLUMN_APP_LINK_INTENT_URI
- };
- private static final String LEANBACK_PACKAGE = "com.google.android.tvlauncher";
-
- public static int getNumberOfChannels(Context context)
- {
- Cursor cursor =
- context.getContentResolver()
- .query(
- TvContractCompat.Channels.CONTENT_URI,
- CHANNELS_PROJECTION,
- null,
- null,
- null);
- return cursor != null ? cursor.getCount() : 0;
- }
-
- public static List<Channel> getAllChannels(Context context)
- {
- List<Channel> channels = new ArrayList<>();
- Cursor cursor =
- context.getContentResolver()
- .query(
- TvContractCompat.Channels.CONTENT_URI,
- CHANNELS_PROJECTION,
- null,
- null,
- null);
- if (cursor != null && cursor.moveToFirst())
- {
- do
- {
- channels.add(Channel.fromCursor(cursor));
- }
- while (cursor.moveToNext());
- }
- return channels;
- }
-
- public static Channel getChannelById(Context context, long channelId)
- {
- for (Channel channel : getAllChannels(context))
- {
- if (channel.getId() == channelId)
- {
- return channel;
- }
- }
- return null;
- }
-
- /**
- * Updates all Leanback homescreen channels
- */
- public static void updateAllChannels(Context context)
- {
- if (Build.VERSION.SDK_INT < 26)
- return;
- for (Channel channel : getAllChannels(context))
- {
- context.getContentResolver()
- .update(
- TvContractCompat.buildChannelUri(channel.getId()),
- channel.toContentValues(),
- null,
- null);
- }
- }
-
- public static Uri getUriToResource(Context context, @AnyRes int resId)
- throws Resources.NotFoundException
- {
- Resources res = context.getResources();
- return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
- "://" + res.getResourcePackageName(resId)
- + '/' + res.getResourceTypeName(resId)
- + '/' + res.getResourceEntryName(resId));
- }
-
- /**
- * Converts a resource into a {@link Bitmap}. If the resource is a vector drawable, it will be
- * drawn into a new Bitmap. Otherwise the {@link BitmapFactory} will decode the resource.
- */
- @NonNull
- public static Bitmap convertToBitmap(Context context, int resourceId)
- {
- Drawable drawable = context.getDrawable(resourceId);
- if (drawable instanceof VectorDrawable)
- {
- Bitmap bitmap =
- Bitmap.createBitmap(
- drawable.getIntrinsicWidth(),
- drawable.getIntrinsicHeight(),
- Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
- return bitmap;
- }
- return BitmapFactory.decodeResource(context.getResources(), resourceId);
- }
-
- /**
- * Leanback launcher requires a uri for poster art so we create a contentUri and
- * pass that to LEANBACK_PACKAGE
- */
- public static Uri buildBanner(GameFile game, Context context)
- {
- Uri contentUri = null;
- File cover;
-
- try
- {
- String customCoverPath = game.getCustomCoverPath();
-
- if (ContentHandler.isContentUri(customCoverPath))
- {
- try
- {
- contentUri = ContentHandler.unmangle(customCoverPath);
- }
- catch (FileNotFoundException | SecurityException ignored)
- {
- // Let contentUri remain null
- }
- }
- else
- {
- if ((cover = new File(customCoverPath)).exists())
- {
- contentUri = getUriForFile(context, getFileProvider(context), cover);
- }
- }
-
- if (contentUri == null)
- {
- contentUri = Uri.parse(CoverHelper.buildGameTDBUrl(game, CoverHelper.getRegion(game)));
- }
-
- context.grantUriPermission(LEANBACK_PACKAGE, contentUri, FLAG_GRANT_READ_URI_PERMISSION);
- }
- catch (Exception e)
- {
- Log.e(TAG, "Failed to create banner");
- Log.e(TAG, e.getMessage());
- }
-
- return contentUri;
- }
-
- /**
- * Needed since debug builds append '.debug' to the end of the package
- */
- private static String getFileProvider(Context context)
- {
- return context.getPackageName() + ".filesprovider";
- }
-
- /**
- * Schedules syncing channels via a {@link JobScheduler}.
- *
- * @param context for accessing the {@link JobScheduler}.
- */
- public static void scheduleSyncingChannel(Context context)
- {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
- return;
-
- ComponentName componentName = new ComponentName(context, SyncChannelJobService.class);
- JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
- builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
-
- JobScheduler scheduler =
- (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
-
- Log.d(TAG, "Scheduled channel creation.");
- scheduler.schedule(builder.build());
- }
-
- /**
- * Schedulers syncing programs for a channel. The scheduler will listen to a {@link Uri} for a
- * particular channel.
- *
- * @param context for accessing the {@link JobScheduler}.
- * @param channelId for the channel to listen for changes.
- */
- @TargetApi(Build.VERSION_CODES.O)
- public static void scheduleSyncingProgramsForChannel(Context context, long channelId)
- {
- Log.d(TAG, "ProgramsRefresh job");
- ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
- JobInfo.Builder builder =
- new JobInfo.Builder(getJobIdForChannelId(channelId), componentName);
- JobInfo.TriggerContentUri triggerContentUri =
- new JobInfo.TriggerContentUri(
- TvContractCompat.buildChannelUri(channelId),
- JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
- builder.addTriggerContentUri(triggerContentUri);
- builder.setTriggerContentMaxDelay(0L);
- builder.setTriggerContentUpdateDelay(0L);
-
- PersistableBundle bundle = new PersistableBundle();
- bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
- builder.setExtras(bundle);
-
- JobScheduler scheduler =
- (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- scheduler.cancel(getJobIdForChannelId(channelId));
- scheduler.schedule(builder.build());
- }
-
- private static int getJobIdForChannelId(long channelId)
- {
- return (int) (CHANNEL_JOB_ID_OFFSET + channelId);
- }
-
- /**
- * Generates all subscriptions for homescreen channels.
- */
- public static List<HomeScreenChannel> createUniversalSubscriptions(Context context)
- {
- return new ArrayList<>(createPlatformSubscriptions(context));
- }
-
- private static List<HomeScreenChannel> createPlatformSubscriptions(Context context)
- {
- List<HomeScreenChannel> subs = new ArrayList<>();
- for (PlatformTab platformTab : PlatformTab.values())
- {
- subs.add(new HomeScreenChannel(
- context.getString(platformTab.getHeaderName()),
- context.getString(platformTab.getHeaderName()),
- AppLinkHelper.buildBrowseUri(platformTab)));
- }
- return subs;
- }
-
- public static Boolean isLeanback(Context context)
- {
- return (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK));
- }
-}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.kt
new file mode 100644
index 0000000000..6af8612822
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.kt
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.utils
+
+import android.app.job.JobInfo
+import android.app.job.JobScheduler
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.drawable.VectorDrawable
+import android.net.Uri
+import android.os.Build
+import android.os.PersistableBundle
+import android.util.Log
+import androidx.annotation.AnyRes
+import androidx.annotation.RequiresApi
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.FileProvider.getUriForFile
+import androidx.core.graphics.createBitmap
+import androidx.core.net.toUri
+import androidx.tvprovider.media.tv.Channel
+import androidx.tvprovider.media.tv.TvContractCompat
+import org.dolphinemu.dolphinemu.model.GameFile
+import org.dolphinemu.dolphinemu.model.HomeScreenChannel
+import org.dolphinemu.dolphinemu.services.SyncChannelJobService
+import org.dolphinemu.dolphinemu.services.SyncProgramsJobService
+import org.dolphinemu.dolphinemu.ui.platform.PlatformTab
+import java.io.File
+import java.io.FileNotFoundException
+
+/**
+ * Assists in TV related services, e.g., home screen channels
+ */
+object TvUtil {
+ private const val TAG = "TvUtil"
+ private const val CHANNEL_JOB_ID_OFFSET = 1000L
+ private const val LEANBACK_PACKAGE = "com.google.android.tvlauncher"
+
+ private val channelsProjection = arrayOf(
+ TvContractCompat.Channels._ID,
+ TvContractCompat.Channels.COLUMN_DISPLAY_NAME,
+ TvContractCompat.Channels.COLUMN_BROWSABLE,
+ TvContractCompat.Channels.COLUMN_APP_LINK_INTENT_URI
+ )
+
+ @JvmStatic
+ fun getAllChannels(context: Context): List<Channel> {
+ val channels = ArrayList<Channel>()
+ val cursor = context.contentResolver.query(
+ TvContractCompat.Channels.CONTENT_URI, channelsProjection, null, null, null
+ )
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ channels.add(Channel.fromCursor(cursor))
+ } while (cursor.moveToNext())
+ }
+ return channels
+ }
+
+ @JvmStatic
+ fun getChannelById(context: Context, channelId: Long): Channel? {
+ for (channel in getAllChannels(context)) {
+ if (channel.id == channelId) {
+ return channel
+ }
+ }
+ return null
+ }
+
+ /**
+ * Updates all Leanback homescreen channels
+ */
+ @JvmStatic
+ fun updateAllChannels(context: Context) {
+ if (Build.VERSION.SDK_INT < 26) {
+ return
+ }
+
+ for (channel in getAllChannels(context)) {
+ context.contentResolver.update(
+ TvContractCompat.buildChannelUri(channel.id), channel.toContentValues(), null, null
+ )
+ }
+ }
+
+ @JvmStatic
+ fun getUriToResource(context: Context, @AnyRes resId: Int): Uri {
+ val res = context.resources
+ return (ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + res.getResourcePackageName(resId) + '/' + res.getResourceTypeName(
+ resId
+ ) + '/' + res.getResourceEntryName(resId)).toUri()
+ }
+
+ /**
+ * Converts a resource into a [Bitmap]. If the resource is a vector drawable, it will be
+ * drawn into a new Bitmap. Otherwise, the [BitmapFactory] will decode the resource.
+ */
+ @JvmStatic
+ fun convertToBitmap(context: Context, resourceId: Int): Bitmap {
+ val drawable = AppCompatResources.getDrawable(context, resourceId)
+ if (drawable is VectorDrawable) {
+ val bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight)
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+ return bitmap
+ }
+ return BitmapFactory.decodeResource(context.resources, resourceId)
+ }
+
+ /**
+ * Leanback launcher requires a uri for poster art so we create a contentUri and
+ * pass that to LEANBACK_PACKAGE
+ */
+ @JvmStatic
+ fun buildBanner(game: GameFile, context: Context): Uri? {
+ var contentUri: Uri? = null
+
+ try {
+ val customCoverPath = game.customCoverPath
+
+ if (ContentHandler.isContentUri(customCoverPath)) {
+ try {
+ contentUri = ContentHandler.unmangle(customCoverPath)
+ } catch (_: FileNotFoundException) {
+ // Let contentUri remain null
+ } catch (_: SecurityException) {
+ // Let contentUri remain null
+ }
+ } else {
+ val cover = File(customCoverPath)
+ if (cover.exists()) {
+ contentUri = getUriForFile(context, getFileProvider(context), cover)
+ }
+ }
+
+ val resolvedContentUri = contentUri ?: (CoverHelper.buildGameTDBUrl(
+ game, CoverHelper.getRegion(game)
+ )).toUri()
+
+ context.grantUriPermission(
+ LEANBACK_PACKAGE,
+ resolvedContentUri,
+ android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ contentUri = resolvedContentUri
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to create banner")
+ Log.e(TAG, e.message ?: "null")
+ }
+
+ return contentUri
+ }
+
+ /**
+ * Needed since debug builds append '.debug' to the end of the package
+ */
+ private fun getFileProvider(context: Context): String {
+ return context.packageName + ".filesprovider"
+ }
+
+ /**
+ * Schedules syncing channels via a [JobScheduler].
+ *
+ * @param context for accessing the [JobScheduler].
+ */
+ @JvmStatic
+ fun scheduleSyncingChannel(context: Context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+
+ val componentName = ComponentName(context, SyncChannelJobService::class.java)
+ val builder = JobInfo.Builder(1, componentName)
+ builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+
+ val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+
+ Log.d(TAG, "Scheduled channel creation.")
+ scheduler.schedule(builder.build())
+ }
+
+ /**
+ * Schedulers syncing programs for a channel. The scheduler will listen to a [Uri] for a
+ * particular channel.
+ *
+ * @param context for accessing the [JobScheduler].
+ * @param channelId for the channel to listen for changes.
+ */
+ @RequiresApi(Build.VERSION_CODES.O)
+ @JvmStatic
+ fun scheduleSyncingProgramsForChannel(context: Context, channelId: Long) {
+ Log.d(TAG, "ProgramsRefresh job")
+ val componentName = ComponentName(context, SyncProgramsJobService::class.java)
+ val builder = JobInfo.Builder(getJobIdForChannelId(channelId), componentName)
+ val triggerContentUri = JobInfo.TriggerContentUri(
+ TvContractCompat.buildChannelUri(channelId),
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
+ )
+ builder.addTriggerContentUri(triggerContentUri)
+ builder.setTriggerContentMaxDelay(0L)
+ builder.setTriggerContentUpdateDelay(0L)
+
+ val bundle = PersistableBundle()
+ bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId)
+ builder.setExtras(bundle)
+
+ val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ scheduler.cancel(getJobIdForChannelId(channelId))
+ scheduler.schedule(builder.build())
+ }
+
+ private fun getJobIdForChannelId(channelId: Long): Int {
+ return (CHANNEL_JOB_ID_OFFSET + channelId).toInt()
+ }
+
+ /**
+ * Generates all subscriptions for homescreen channels.
+ */
+ @JvmStatic
+ fun createUniversalSubscriptions(context: Context): List<HomeScreenChannel> {
+ return ArrayList(createPlatformSubscriptions(context))
+ }
+
+ private fun createPlatformSubscriptions(context: Context): List<HomeScreenChannel> {
+ val subscriptions = ArrayList<HomeScreenChannel>()
+ for (platformTab in PlatformTab.entries) {
+ subscriptions.add(
+ HomeScreenChannel(
+ context.getString(platformTab.headerName),
+ context.getString(platformTab.headerName),
+ AppLinkHelper.buildBrowseUri(platformTab)
+ )
+ )
+ }
+ return subscriptions
+ }
+
+ @JvmStatic
+ fun isLeanback(context: Context): Boolean {
+ return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+ }
+}