如何在Android TV 桌面新增自定義頻道/節目
阿新 • • 發佈:2019-02-13
最近在做Android TV O的專案,需要在TV 桌面新增自定義頻道/節目,節目的背景圖片要顯示為SD卡或者快取目錄裡面的圖片。
- 新增自定義頻道
- 節目背景顯示本地目錄的圖片
一、新增頻道
1. 首先新建頻道、節目實體類,屬性如下。
public class MediaChannel { private final String mName; private final String mDescription; private final String mMediaUri; private final String mBgImage; private final String mTitle; private final String mMediaChannelId; private List<MediaProgram> mPrograms; private boolean mChannelPublished; private long mChannelId; MediaChannel(String name, List<MediaProgram> programs, String mediaChannelId) { mName = name; mTitle = "playlist title"; mDescription = "playlist description"; mMediaUri = "dsf"; mBgImage = "asdf"; mPrograms = programs; mMediaChannelId = mediaChannelId; } // 省略 set get toString } public class MediaProgram implements Parcelable { private final String mMediaProgramId; private final String mContentId; private final String mTitle; private final String mDescription; private final String mBgImageUrl; private final String mCardImageUrl; private final String mMediaUrl; private final String mPreviewMediaUrl; private final String mCategory; private long mProgramId; private int mViewCount; MediaProgram(String title, String description, String bgImageUrl, String cardImageUrl, String category, String mediaProgramId, String contentId) { mMediaProgramId = mediaProgramId; mContentId = contentId; mTitle = title; mDescription = description; mBgImageUrl = bgImageUrl; mCardImageUrl = cardImageUrl; mMediaUrl = ""; mPreviewMediaUrl = ""; mCategory = category; } // 省略 set get toString }
2. 初始化頻道、節目資訊
private void initChannel() { Uri usbUri = getUSBCardImageFileUri(); Uri pvrUri = getPVRCardImageFileUri(); grantUriPermissionToApp("com.google.android.tvlauncher", usbUri); grantUriPermissionToApp("com.google.android.tvlauncher", pvrUri); String bgImageUrl = ""; String usbCardImageUrl = getUSBCardImageFileUri().toString(); String pvrCardImageUrl = getPVRCardImageFileUri().toString(); int mediaProgramId = 1; int contentId = 0; MediaProgram usbProgram = new MediaProgram("USB", "usb description", bgImageUrl, usbCardImageUrl, "USB category", Integer.toString(mediaProgramId), Integer.toString(contentId ++)); MediaProgram pvrProgram = new MediaProgram("PVR", "pvr description", bgImageUrl, pvrCardImageUrl, "PVR category", Integer.toString(mediaProgramId), Integer.toString(contentId ++)); List<MediaProgram> programs = new ArrayList<>(); programs.add(usbProgram); programs.add(pvrProgram); mChannelId = LocalDataManager.getChannelId(this); mChannel = new MediaChannel("MediaChannel", programs, Long.toString(mChannelId)); } private Uri getUSBCardImageFileUri() { String sdPath = Environment.getExternalStorageDirectory().getPath(); File file = new File(sdPath + "/Pictures/mediachannel/usb_thumbnail.jpg"); Uri uri = FileProvider.getUriForFile(this, "com.rogera.mediaplaychannel.fileprovider", file); Log.v(TAG, "uri:" + uri.toString()); return uri; } private Uri getPVRCardImageFileUri() { String sdPath = Environment.getExternalStorageDirectory().getPath(); File file = new File(sdPath + "/Pictures/mediachannel/pvr_thumbnail.jpg"); Uri uri = FileProvider.getUriForFile(this, "com.rogera.mediaplaychannel.fileprovider", file); Log.v(TAG, "uri:" + uri.toString()); return uri; } private void grantUriPermissionToApp(String packageName, Uri uri) { grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); }
3.新增頻道、節目
mChannelId = MediaTVProvider.addChannel(MainActivity.this, mChannel);
mChannel 為initChannel() 方法裡面初始化的實體類
貼出核心類MediaTVProvider.java
package com.rogera.mediaplaychannel; import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.WorkerThread; import android.support.media.tv.Channel; import android.support.media.tv.ChannelLogoUtils; import android.support.media.tv.PreviewProgram; import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Log; import java.util.List; /** * Created by rogera on 2017/12/30. */ public class MediaTVProvider { private static final String TAG = "MediaTVProvider"; private static final String SCHEME = "tvmediachannels"; private static final String APPS_LAUNCH_HOST = "com.google.android.tvmediachannels"; private static final String PLAY_MEDIA_ACTION_PATH = "playMedia"; private static final String START_APP_ACTION_PATH = "startApp"; private static final Uri PREVIEW_PROGRAMS_CONTENT_URI = Uri.parse("content://android.media.tv/preview_program"); static private String createInputId(Context context) { ComponentName cName = new ComponentName(context, MainActivity.class.getName()); return TvContractCompat.buildInputId(cName); } @WorkerThread static long addChannel(Context context, MediaChannel mediaChannel) { String channelInputId = createInputId(context); Channel channel = new Channel.Builder() .setDisplayName(mediaChannel.getName()) .setDescription(mediaChannel.getDescription()) .setType(TvContractCompat.Channels.TYPE_PREVIEW) .setInputId(channelInputId) .setAppLinkIntentUri(Uri.parse(SCHEME + "://" + APPS_LAUNCH_HOST + "/" + START_APP_ACTION_PATH)) .setInternalProviderId(mediaChannel.getMediaChannelId()) .build(); Uri channelUri = context.getContentResolver().insert(TvContractCompat.Channels.CONTENT_URI, channel.toContentValues()); if (channelUri == null || channelUri.equals(Uri.EMPTY)) { Log.e(TAG, "addChannel Insert channel failed"); return 0; } long channelId = ContentUris.parseId(channelUri); mediaChannel.setChannelPublishedId(channelId); writeChannelLogo(context, channelId, R.drawable.media_logo); List<MediaProgram> programs = mediaChannel.getMediaPrograms(); int weight = programs.size(); for (int i = 0; i < programs.size(); ++i, --weight) { MediaProgram mp = programs.get(i); final String mediaProgramId = mp.getMediaProgramId(); final String contentId = mp.getContentId(); PreviewProgram program = new PreviewProgram.Builder() .setChannelId(channelId) .setTitle(mp.getTitle()) .setDescription(mp.getDescription()) .setPosterArtUri(Uri.parse(mp.getCardImageUrl())) .setIntentUri(Uri.parse(SCHEME + "://" + APPS_LAUNCH_HOST + "/" + PLAY_MEDIA_ACTION_PATH + "/" + mediaProgramId)) //.setPreviewVideoUri(Uri.parse(mp.getPreviewMediaUrl())) .setInternalProviderId(mediaProgramId) .setContentId(contentId) .setWeight(weight) .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .build(); Uri programUri = context.getContentResolver().insert(PREVIEW_PROGRAMS_CONTENT_URI, program.toContentValues()); if (programUri == null || programUri.equals(Uri.EMPTY)) { Log.e(TAG, "addChannel Insert program failed"); } else { mp.setProgramId(ContentUris.parseId(programUri)); } } return channelId; } @WorkerThread static void deleteChannel(Context context, long channelId) { int rowsDeleted = context.getContentResolver().delete( TvContractCompat.buildChannelUri(channelId), null, null); if (rowsDeleted < 1) { Log.e(TAG, "Delete channel failed"); } } @WorkerThread public static void deleteProgram(Context context, MediaProgram program) { deleteProgram(context, program.getProgramId()); } @WorkerThread static void deleteProgram(Context context, long programId) { int rowsDeleted = context.getContentResolver().delete( TvContractCompat.buildPreviewProgramUri(programId), null, null); if (rowsDeleted < 1) { Log.e(TAG, "Delete program failed"); } } /** * Writes a drawable as the channel logo. * * @param channelId identifies the channel to write the logo. * @param drawableId resource to write as the channel logo. This must be a bitmap and not, say * a vector drawable. */ @WorkerThread static private void writeChannelLogo(Context context, long channelId, @DrawableRes int drawableId) { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), drawableId); ChannelLogoUtils.storeChannelLogo(context, channelId, bitmap); } @WorkerThread static void updateMediaProgram(Context context, MediaProgram mediaProgram) { long programId = mediaProgram.getProgramId(); Uri programUri = TvContractCompat.buildPreviewProgramUri(programId); try (Cursor cursor = context.getContentResolver().query(programUri, null, null, null, null)) { if (!cursor.moveToFirst()) { Log.e(TAG, "Update program failed"); } PreviewProgram porgram = PreviewProgram.fromCursor(cursor); PreviewProgram.Builder builder = new PreviewProgram.Builder(porgram) .setTitle(mediaProgram.getTitle()); int rowsUpdated = context.getContentResolver().update(programUri, builder.build().toContentValues(), null, null); if (rowsUpdated < 1) { Log.e(TAG, "Update program failed"); } } } static void publishProgram(Context context, MediaProgram mediaProgram, long channelId, int weight) { final String mediaProgramId = mediaProgram.getMediaProgramId(); PreviewProgram program = new PreviewProgram.Builder() .setChannelId(channelId) .setTitle(mediaProgram.getTitle()) .setDescription(mediaProgram.getDescription()) .setPosterArtUri(Uri.parse(mediaProgram.getCardImageUrl())) .setIntentUri(Uri.parse(SCHEME + "://" + APPS_LAUNCH_HOST + "/" + PLAY_MEDIA_ACTION_PATH + "/" + mediaProgramId)) .setPreviewVideoUri(Uri.parse(mediaProgram.getPreviewMediaUrl())) .setInternalProviderId(mediaProgramId) .setWeight(weight) .setType(TvContractCompat.PreviewPrograms.TYPE_MOVIE) .build(); Uri programUri = context.getContentResolver().insert(PREVIEW_PROGRAMS_CONTENT_URI, program.toContentValues()); if (programUri == null || programUri.equals(Uri.EMPTY)) { Log.e(TAG, "Insert program failed"); return; } mediaProgram.setProgramId(ContentUris.parseId(programUri)); } @WorkerThread static void setProgramViewCount(Context context, long programId, int numberOfViews) { Uri programUri = TvContractCompat.buildPreviewProgramUri(programId); try (Cursor cursor = context.getContentResolver().query(programUri, null, null, null, null)) { if (!cursor.moveToFirst()) { return; } PreviewProgram existingProgram = PreviewProgram.fromCursor(cursor); PreviewProgram.Builder builder = new PreviewProgram.Builder(existingProgram) .setInteractionCount(numberOfViews) .setInteractionType(TvContractCompat.PreviewProgramColumns .INTERACTION_TYPE_VIEWS); int rowsUpdated = context.getContentResolver().update( TvContractCompat.buildPreviewProgramUri(programId), builder.build().toContentValues(), null, null); if (rowsUpdated != 1) { Log.e(TAG, "Update program failed"); } } } }
二、節目背景顯示本地目錄的圖片
對於顯示本地圖片,需要使用FileProvider 獲取圖片檔案的uri然後設定給節目。如果使用 Uri.fromFile(new File(filePath) 這種方式,就會報Permission問題:
class java.io.FileNotFoundException: /storage/emulated/0/Pictures/mediachannel/usb_thumbnail.jpg (Permission denied)
使用FileProvider分享檔案給其他應用需要給對應的應用賦予讀許可權,可以通過如下兩種方式:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
grantUriPermissionToApp("com.google.android.tvlauncher", usbUri);
這裡只能使用第二種方式了。com.google.android.tvlauncher 為TV launcher的包名。
1. 使用FileProvider首先需要在AndroidManifest.xml 節點下申明
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.rogera.mediaplaychannel.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
2. 在res下xml資料夾下新建filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path path="Pictures" name="pictures" />
</paths>
3. 獲取檔案URI
File file = new File(sdPath + "/Pictures/mediachannel/usb_thumbnail.jpg");
Uri uri = FileProvider.getUriForFile(this, "com.rogera.mediaplaychannel.fileprovider", file);
三、其他說明
1、sdcard裡面的檔案是push進去的,是假設應用獲取U盤裡面的電影/圖片/音樂 生成的縮圖。點選桌面的usb節目就會播放相應的電影/圖片/音樂。
2. 在桌面新增頻道、節目需要申請EPG許可權,SD需要申請storage許可權
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
3. 同時需要在gradle新增如下依賴
implementation 'com.android.support:leanback-v17:26.1.0'
implementation 'com.android.support:support-tv-provider:26.1.0'
4. 上圖啦