最近在做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 <application>节点下申明
<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. 上图啦
Google官方参考:https://developer.android.google.cn/training/tv/tif/channel.html#update