mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-05-09 04:13:28 -05:00
Merge pull request #14629 from Simonx22/android/convert-tvutil-to-kotlin
Android: Convert TvUtil to Kotlin
This commit is contained in:
commit
30a20d75d2
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user