From dbeca0d1342022050ffd56b6b4bcdcaf0728fcfd Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sun, 22 Mar 2026 13:25:18 -0400 Subject: [PATCH] Android: Refactor SyncProgramsJobService Changes: - Convert SyncProgramsJobService from Java to Kotlin. - Replace AsyncTask/executor lifecycle handling with coroutine-based job execution and cancellation. - Stop using restricted androidx.tvprovider builder APIs. --- .../services/SyncProgramsJobService.java | 164 ------------------ .../services/SyncProgramsJobService.kt | 156 +++++++++++++++++ 2 files changed, 156 insertions(+), 164 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.java deleted file mode 100644 index 7e4dacf9bc..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.java +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.services; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.PersistableBundle; -import android.util.Log; - -import androidx.tvprovider.media.tv.Channel; -import androidx.tvprovider.media.tv.PreviewProgram; -import androidx.tvprovider.media.tv.TvContractCompat; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.ui.platform.PlatformTab; -import org.dolphinemu.dolphinemu.utils.AppLinkHelper; -import org.dolphinemu.dolphinemu.utils.TvUtil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class SyncProgramsJobService extends JobService -{ - private static final String TAG = "SyncProgramsJobService"; - - private SyncProgramsTask mSyncProgramsTask; - - @Override - public boolean onStartJob(final JobParameters jobParameters) - { - Log.d(TAG, "onStartJob(): " + jobParameters); - final long channelId = getChannelId(jobParameters); - if (channelId == -1L) - { - Log.d(TAG, "Failed to find channel"); - return false; - } - - mSyncProgramsTask = - new SyncProgramsTask(getApplicationContext()) - { - @Override - protected void onPostExecute(Boolean finished) - { - super.onPostExecute(finished); - mSyncProgramsTask = null; - jobFinished(jobParameters, !finished); - } - }; - mSyncProgramsTask.execute(channelId); - return true; - } - - @Override - public boolean onStopJob(JobParameters jobParameters) - { - if (mSyncProgramsTask != null) - { - mSyncProgramsTask.cancel(true); - } - return true; - } - - private long getChannelId(JobParameters jobParameters) - { - PersistableBundle extras = jobParameters.getExtras(); - return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L); - } - - private static class SyncProgramsTask extends AsyncTask - { - private Context context; - private List updatePrograms; - - private SyncProgramsTask(Context context) - { - this.context = context; - updatePrograms = new ArrayList<>(); - } - - /** - * Determines which channel to update, get the game files for the channel, - * then updates the list - */ - @Override - protected Boolean doInBackground(Long... channelIds) - { - List params = Arrays.asList(channelIds); - if (!params.isEmpty()) - { - for (Long channelId : params) - { - Channel channel = TvUtil.getChannelById(context, channelId); - for (PlatformTab platformTab : PlatformTab.values()) - { - if (channel != null && - channel.getAppLinkIntentUri().equals(AppLinkHelper.buildBrowseUri(platformTab))) - { - getGamesByPlatform(platformTab); - syncPrograms(channelId); - } - } - } - } - return true; - } - - private void getGamesByPlatform(PlatformTab platformTab) - { - updatePrograms = GameFileCacheManager.getGameFilesForPlatformTab(platformTab); - } - - private void syncPrograms(long channelId) - { - Log.d(TAG, "Sync programs for channel: " + channelId); - deletePrograms(channelId); - createPrograms(channelId); - } - - private void createPrograms(long channelId) - { - for (GameFile game : updatePrograms) - { - PreviewProgram previewProgram = buildProgram(channelId, game); - - context.getContentResolver() - .insert( - TvContractCompat.PreviewPrograms.CONTENT_URI, - previewProgram.toContentValues()); - } - } - - private void deletePrograms(long channelId) - { - context.getContentResolver().delete( - TvContractCompat.buildPreviewProgramsUriForChannel(channelId), - null, - null); - } - - private PreviewProgram buildProgram(long channelId, GameFile game) - { - Uri appLinkUri = AppLinkHelper.buildGameUri(channelId, game.getGameId()); - Uri banner = TvUtil.buildBanner(game, context); - if (banner == null) - banner = TvUtil.getUriToResource(context, R.drawable.banner_tv); - - PreviewProgram.Builder builder = new PreviewProgram.Builder(); - builder.setChannelId(channelId) - .setType(TvContractCompat.PreviewProgramColumns.TYPE_GAME) - .setTitle(game.getTitle()) - .setDescription(game.getDescription()) - .setPosterArtUri(banner) - .setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_2_3) - .setIntentUri(appLinkUri); - return builder.build(); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt new file mode 100644 index 0000000000..2d9ad354fb --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.services + +import android.app.job.JobParameters +import android.app.job.JobService +import android.content.ContentValues +import android.content.Context +import android.media.tv.TvContract +import android.util.Log +import androidx.tvprovider.media.tv.TvContractCompat +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.ui.platform.PlatformTab +import org.dolphinemu.dolphinemu.utils.AppLinkHelper.buildBrowseUri +import org.dolphinemu.dolphinemu.utils.AppLinkHelper.buildGameUri +import org.dolphinemu.dolphinemu.utils.TvUtil + +class SyncProgramsJobService : JobService() { + private val syncProgramsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var syncProgramsJob: Job? = null + + override fun onStartJob(jobParameters: JobParameters): Boolean { + Log.d(TAG, "onStartJob(): $jobParameters") + val channelId = getChannelId(jobParameters) + if (channelId == -1L) { + Log.d(TAG, "Failed to find channel") + return false + } + + syncProgramsJob?.cancel() + syncProgramsJob = syncProgramsScope.launch { + val finished = try { + withContext(Dispatchers.IO) { + SyncProgramsTask(applicationContext).run(channelId) + } + } catch (_: CancellationException) { + return@launch + } catch (e: RuntimeException) { + Log.e(TAG, "Failed to sync programs", e) + false + } + + syncProgramsJob = null + jobFinished(jobParameters, !finished) + } + + return true + } + + override fun onStopJob(jobParameters: JobParameters): Boolean { + syncProgramsJob?.cancel() + syncProgramsJob = null + return true + } + + override fun onDestroy() { + syncProgramsScope.cancel() + super.onDestroy() + } + + private fun getChannelId(jobParameters: JobParameters): Long { + val extras = jobParameters.extras + return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L) + } + + private class SyncProgramsTask(private val context: Context) { + private var updatePrograms: List = emptyList() + + /** + * Determines which channel to update, get the game files for the channel, + * then updates the list + */ + suspend fun run(vararg channelIds: Long): Boolean { + for (channelId in channelIds) { + currentCoroutineContext().ensureActive() + + val channel = TvUtil.getChannelById(context, channelId) + for (platformTab in PlatformTab.entries) { + if (channel?.appLinkIntentUri == buildBrowseUri(platformTab)) { + updatePrograms = + GameFileCacheManager.getGameFilesForPlatformTab(platformTab) + if (!syncPrograms(channelId)) { + return false + } + } + } + } + + return true + } + + private suspend fun syncPrograms(channelId: Long): Boolean { + Log.d(TAG, "Sync programs for channel: $channelId") + deletePrograms(channelId) + return createPrograms(channelId) + } + + private suspend fun createPrograms(channelId: Long): Boolean { + for (game in updatePrograms) { + currentCoroutineContext().ensureActive() + + val previewProgram = buildProgram(channelId, game) + context.contentResolver.insert( + TvContractCompat.PreviewPrograms.CONTENT_URI, previewProgram + ) + } + + return true + } + + private fun deletePrograms(channelId: Long) { + context.contentResolver.delete( + TvContractCompat.buildPreviewProgramsUriForChannel(channelId), null, null + ) + } + + private fun buildProgram(channelId: Long, game: GameFile): ContentValues { + val appLinkUri = buildGameUri(channelId, game.getGameId()) + var banner = TvUtil.buildBanner(game, context) + if (banner == null) { + banner = TvUtil.getUriToResource(context, R.drawable.banner_tv) + } + + val values = ContentValues() + values.put(TvContractCompat.PreviewPrograms.COLUMN_CHANNEL_ID, channelId) + values.put( + TvContractCompat.PreviewPrograms.COLUMN_TYPE, + TvContractCompat.PreviewProgramColumns.TYPE_GAME + ) + values.put(TvContract.Programs.COLUMN_TITLE, game.getTitle()) + values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, game.getDescription()) + values.put(TvContract.Programs.COLUMN_POSTER_ART_URI, banner.toString()) + values.put( + TvContractCompat.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, + TvContractCompat.PreviewPrograms.ASPECT_RATIO_2_3 + ) + values.put(TvContractCompat.PreviewPrograms.COLUMN_INTENT_URI, appLinkUri.toString()) + return values + } + } + + companion object { + private const val TAG = "SyncProgramsJobService" + } +}