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.
This commit is contained in:
-164
@@ -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<Long, Void, Boolean>
|
|
||||||
{
|
|
||||||
private Context context;
|
|
||||||
private List<GameFile> 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<Long> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+156
@@ -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<GameFile> = 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user