package com.limelight.utils; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentUris; import android.content.ContentValues; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.media.tv.TvContract; import android.net.Uri; import android.os.Build; import com.limelight.LimeLog; import com.limelight.PosterContentProvider; import com.limelight.R; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; import java.io.IOException; import java.io.OutputStream; public class TvChannelHelper { private static final int ASPECT_RATIO_MOVIE_POSTER = 5; private static final int TYPE_GAME = 12; private static final int INTERNAL_PROVIDER_ID_INDEX = 1; private static final int PROGRAM_BROWSABLE_INDEX = 2; private static final int ID_INDEX = 0; private Activity context; public TvChannelHelper(Activity context) { this.context = context; } void requestChannelOnHomeScreen(ComputerDetails computer) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!isAndroidTV()) { return; } Long channelId = getChannelId(computer.uuid); if (channelId == null) { return; } Intent intent = new Intent(TvContract.ACTION_REQUEST_CHANNEL_BROWSABLE); intent.putExtra(TvContract.EXTRA_CHANNEL_ID, getChannelId(computer.uuid)); try { context.startActivityForResult(intent, 0); } catch (ActivityNotFoundException e) { } } } void createTvChannel(ComputerDetails computer) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!isAndroidTV()) { return; } ChannelBuilder builder = new ChannelBuilder() .setType(TvContract.Channels.TYPE_PREVIEW) .setDisplayName(computer.name) .setInternalProviderId(computer.uuid) .setAppLinkIntent(ServerHelper.createPcShortcutIntent(context, computer)); Long channelId = getChannelId(computer.uuid); if (channelId != null) { context.getContentResolver().update(TvContract.buildChannelUri(channelId), builder.toContentValues(), null, null); return; } Uri channelUri = context.getContentResolver().insert( TvContract.Channels.CONTENT_URI, builder.toContentValues()); if (channelUri != null) { long id = ContentUris.parseId(channelUri); updateChannelIcon(id); } } } @TargetApi(Build.VERSION_CODES.O) private void updateChannelIcon(long channelId) { Bitmap logo = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel)); try { Uri localUri = TvContract.buildChannelLogoUri(channelId); try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) { logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream); outputStream.flush(); } catch (SQLiteException | IOException e) { LimeLog.warning("Failed to store the logo to the system content provider."); e.printStackTrace(); } } finally { logo.recycle(); } } private Bitmap drawableToBitmap(Drawable drawable) { int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width); int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } void addGameToChannel(ComputerDetails computer, NvApp app) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!isAndroidTV()) { return; } Long channelId = getChannelId(computer.uuid); if (channelId == null) { return; } PreviewProgramBuilder builder = new PreviewProgramBuilder() .setChannelId(channelId) .setType(TYPE_GAME) .setTitle(app.getAppName()) .setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER) .setPosterArtUri(PosterContentProvider.createBoxArtUri(computer.uuid, ""+app.getAppId())) .setIntent(ServerHelper.createAppShortcutIntent(context, computer, app)) .setInternalProviderId(""+app.getAppId()) // Weight should increase each time we run the game .setWeight((int)((System.currentTimeMillis() - 1500000000000L) / 1000)); Long programId = getProgramId(channelId, ""+app.getAppId()); if (programId != null) { context.getContentResolver().update(TvContract.buildPreviewProgramUri(programId), builder.toContentValues(), null, null); return; } context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI, builder.toContentValues()); TvContract.requestChannelBrowsable(context, channelId); } } void deleteChannel(ComputerDetails computer) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!isAndroidTV()) { return; } Long channelId = getChannelId(computer.uuid); if (channelId == null) { return; } context.getContentResolver().delete(TvContract.buildChannelUri(channelId), null, null); } } void deleteProgram(ComputerDetails computer, NvApp app) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!isAndroidTV()) { return; } Long channelId = getChannelId(computer.uuid); if (channelId == null) { return; } Long programId = getProgramId(channelId, ""+app.getAppId()); if (programId == null) { return; } context.getContentResolver().delete(TvContract.buildPreviewProgramUri(programId), null, null); } } @TargetApi(Build.VERSION_CODES.O) private Long getChannelId(String computerUuid) { try (Cursor cursor = context.getContentResolver().query( TvContract.Channels.CONTENT_URI, new String[] {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID}, null, null, null)) { if (cursor == null || cursor.getCount() == 0) { return null; } while (cursor.moveToNext()) { String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX); if (computerUuid.equals(internalProviderId)) { return cursor.getLong(ID_INDEX); } } return null; } } @TargetApi(Build.VERSION_CODES.O) private Long getProgramId(long channelId, String appId) { try (Cursor cursor = context.getContentResolver().query( TvContract.buildPreviewProgramsUriForChannel(channelId), new String[] {TvContract.PreviewPrograms._ID, TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, TvContract.PreviewPrograms.COLUMN_BROWSABLE}, null, null, null)) { if (cursor == null || cursor.getCount() == 0) { return null; } while (cursor.moveToNext()) { String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX); if (appId.equals(internalProviderId)) { long id = cursor.getLong(ID_INDEX); int browsable = cursor.getInt(PROGRAM_BROWSABLE_INDEX); if (browsable != 0) { return id; } else { int countDeleted = context.getContentResolver().delete(TvContract.buildPreviewProgramUri(id), null, null); if (countDeleted > 0) { LimeLog.info("Preview program has been deleted"); } else { LimeLog.warning("Preview program has not been deleted"); } } } } return null; } } private static String toValueString(T value) { return value == null ? null : value.toString(); } private static String toUriString(Intent intent) { return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME); } @TargetApi(Build.VERSION_CODES.O) private boolean isAndroidTV() { PackageManager pm = context.getPackageManager(); // HarmonyOS devices report PackageManager.FEATURE_LEANBACK yet // when we attempt to actually use TvContract, it will die with // an IllegalArgumentException because TvContract.Channels.CONTENT_URI // is an unknown URL. I don't know if this is a perfect check for // HarmonyOS-powered TVs (excluding any Android TVs), but it will // suffice for now. if (pm.hasSystemFeature("com.huawei.software.features.tv")) { return false; } return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } @TargetApi(Build.VERSION_CODES.O) private static class PreviewProgramBuilder { private ContentValues mValues = new ContentValues(); public PreviewProgramBuilder setChannelId(Long channelId) { mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId); return this; } public PreviewProgramBuilder setType(int type) { mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type); return this; } public PreviewProgramBuilder setTitle(String title) { mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title); return this; } public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) { mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio); return this; } public PreviewProgramBuilder setIntent(Intent intent) { mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent)); return this; } public PreviewProgramBuilder setIntentUri(Uri uri) { mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri)); return this; } public PreviewProgramBuilder setInternalProviderId(String id) { mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id); return this; } public PreviewProgramBuilder setPosterArtUri(Uri uri) { mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri)); return this; } public PreviewProgramBuilder setWeight(int weight) { mValues.put(TvContract.PreviewPrograms.COLUMN_WEIGHT, weight); return this; } public ContentValues toContentValues() { return new ContentValues(mValues); } } @TargetApi(Build.VERSION_CODES.O) private static class ChannelBuilder { private ContentValues mValues = new ContentValues(); public ChannelBuilder setType(String type) { mValues.put(TvContract.Channels.COLUMN_TYPE, type); return this; } public ChannelBuilder setDisplayName(String displayName) { mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName); return this; } public ChannelBuilder setInternalProviderId(String internalProviderId) { mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId); return this; } public ChannelBuilder setAppLinkIntent(Intent intent) { mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent)); return this; } public ContentValues toContentValues() { return new ContentValues(mValues); } } }