blob: a16bc522bed50bc3bf35fa3bdef59395631d7265 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.tuner.tvinput;
import android.content.ComponentName;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.text.format.DateUtils;
import android.util.Log;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.ConvertUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages the channel info and EPG data through {@link TvInputManager}.
*/
public class ChannelDataManager implements Handler.Callback {
private static final String TAG = "ChannelDataManager";
private static final String[] ALL_PROGRAMS_SELECTION_ARGS = new String[] {
TvContract.Programs._ID,
TvContract.Programs.COLUMN_TITLE,
TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
TvContract.Programs.COLUMN_CONTENT_RATING,
TvContract.Programs.COLUMN_BROADCAST_GENRE,
TvContract.Programs.COLUMN_CANONICAL_GENRE,
TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
TvContract.Programs.COLUMN_VERSION_NUMBER };
private static final String[] CHANNEL_DATA_SELECTION_ARGS = new String[] {
TvContract.Channels._ID,
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1};
private static final int MSG_HANDLE_EVENTS = 1;
private static final int MSG_HANDLE_CHANNEL = 2;
private static final int MSG_BUILD_CHANNEL_MAP = 3;
private static final int MSG_REQUEST_PROGRAMS = 4;
private static final int MSG_CLEAR_CHANNELS = 6;
private static final int MSG_CHECK_VERSION = 7;
// Throttle the batch operations to avoid TransactionTooLargeException.
private static final int BATCH_OPERATION_COUNT = 100;
// At most 16 days of program information is delivered through an EIT,
// according to the Chapter 6.4 of ATSC Recommended Practice A/69.
private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16);
/**
* A version number to enforce consistency of the channel data.
*
* WARNING: If a change in the database serialization lead to breaking the backward
* compatibility, you must increment this value so that the old data are purged,
* and the user is requested to perform the auto-scan again to generate the new data set.
*/
private static final int VERSION = 6;
private final Context mContext;
private final String mInputId;
private ProgramInfoListener mListener;
private ChannelScanListener mChannelScanListener;
private Handler mChannelScanHandler;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap;
private final Uri mChannelsUri;
// Used for scanning
private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
private final AtomicBoolean mIsScanning;
private final AtomicBoolean scanCompleted = new AtomicBoolean();
public interface ProgramInfoListener {
/**
* Invoked when a request for getting programs of a channel has been processed and passes
* the requested channel and the programs retrieved from database to the listener.
*/
void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs);
/**
* Invoked when programs of a channel have been arrived and passes the arrived channel and
* programs to the listener.
*/
void onProgramsArrived(TunerChannel channel, List<EitItem> programs);
/**
* Invoked when a channel has been arrived and passes the arrived channel to the listener.
*/
void onChannelArrived(TunerChannel channel);
/**
* Invoked when the database schema has been changed and the old-format channels have been
* deleted. A receiver should notify to a user that re-scanning channels is necessary.
*/
void onRescanNeeded();
}
public interface ChannelScanListener {
/**
* Invoked when all pending channels have been handled.
*/
void onChannelHandlingDone();
}
public ChannelDataManager(Context context) {
mContext = context;
mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
TunerTvInputService.class.getName()));
mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
mTunerChannelMap = new ConcurrentHashMap<>();
mTunerChannelIdMap = new ConcurrentSkipListMap<>();
mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper(), this);
mIsScanning = new AtomicBoolean();
mScannedChannels = new ConcurrentSkipListSet<>();
mPreviousScannedChannels = new ConcurrentSkipListSet<>();
}
// Public methods
public void checkDataVersion(Context context) {
int version = TunerPreferences.getChannelDataVersion(context);
Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")");
if (version == VERSION) {
// Everything is awesome. Return and continue.
return;
}
setCurrentVersion(context);
if (version == TunerPreferences.CHANNEL_DATA_VERSION_NOT_SET) {
mHandler.sendEmptyMessage(MSG_CHECK_VERSION);
} else {
// The stored channel data seem outdated. Delete them all.
mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS);
}
}
public void setCurrentVersion(Context context) {
TunerPreferences.setChannelDataVersion(context, VERSION);
}
public void setListener(ProgramInfoListener listener) {
mListener = listener;
}
public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
mChannelScanListener = listener;
mChannelScanHandler = handler;
}
public void release() {
mHandler.removeCallbacksAndMessages(null);
mHandlerThread.quitSafely();
}
public void releaseSafely() {
mHandlerThread.quitSafely();
}
public TunerChannel getChannel(long channelId) {
TunerChannel channel = mTunerChannelMap.get(channelId);
if (channel != null) {
return channel;
}
mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
byte[] data = null;
try (Cursor cursor = mContext.getContentResolver().query(TvContract.buildChannelUri(
channelId), CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
data = cursor.getBlob(1);
}
}
if (data == null) {
return null;
}
channel = TunerChannel.parseFrom(data);
if (channel == null) {
return null;
}
channel.setChannelId(channelId);
return channel;
}
public void requestProgramsData(TunerChannel channel) {
mHandler.removeMessages(MSG_REQUEST_PROGRAMS);
mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget();
}
public void notifyEventDetected(TunerChannel channel, List<EitItem> items) {
mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget();
}
public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
if (mIsScanning.get()) {
// During scanning, channels should be handle first to improve scan time.
// EIT items can be handled in background after channel scan.
mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel));
} else {
mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
}
}
// For scanning process
/**
* Invoked when starting a scanning mode. This method gets the previous channels to detect the
* obsolete channels after scanning and initializes the variables used for scanning.
*/
public void notifyScanStarted() {
mScannedChannels.clear();
mPreviousScannedChannels.clear();
try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
long channelId = cursor.getLong(0);
byte[] data = cursor.getBlob(1);
TunerChannel channel = TunerChannel.parseFrom(data);
if (channel != null) {
channel.setChannelId(channelId);
mPreviousScannedChannels.add(channel);
}
} while (cursor.moveToNext());
}
}
mIsScanning.set(true);
}
/**
* Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler
* in order to wait for finishing the remaining messages in the handler queue. Then removes the
* obsolete channels, which are previously scanned but are not in the current scanned result.
*/
public void notifyScanCompleted() {
// Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue
// and avoid race conditions.
scanCompleted.set(true);
mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null));
}
public void scannedChannelHandlingCompleted() {
mIsScanning.set(false);
if (!mPreviousScannedChannels.isEmpty()) {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (TunerChannel channel : mPreviousScannedChannels) {
ops.add(ContentProviderOperation.newDelete(
TvContract.buildChannelUri(channel.getChannelId())).build());
}
try {
mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Error deleting obsolete channels", e);
}
}
if (mChannelScanListener != null && mChannelScanHandler != null) {
mChannelScanHandler.post(new Runnable() {
@Override
public void run() {
mChannelScanListener.onChannelHandlingDone();
}
});
} else {
Log.e(TAG, "Error. mChannelScanListener is null.");
}
}
/**
* Returns the number of scanned channels in the scanning mode.
*/
public int getScannedChannelCount() {
return mScannedChannels.size();
}
/**
* Removes all callbacks and messages in handler to avoid previous messages from last channel.
*/
public void removeAllCallbacksAndMessages() {
mHandler.removeCallbacksAndMessages(null);
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_HANDLE_EVENTS: {
ChannelEvent event = (ChannelEvent) msg.obj;
handleEvents(event.channel, event.eitItems);
return true;
}
case MSG_HANDLE_CHANNEL: {
TunerChannel channel = (TunerChannel) msg.obj;
if (channel != null) {
handleChannel(channel);
}
if (scanCompleted.get() && mIsScanning.get()
&& !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) {
// Complete the scan when all found channels have already been handled.
scannedChannelHandlingCompleted();
}
return true;
}
case MSG_BUILD_CHANNEL_MAP: {
mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP);
buildChannelMap();
return true;
}
case MSG_REQUEST_PROGRAMS: {
if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) {
return true;
}
TunerChannel channel = (TunerChannel) msg.obj;
if (mListener != null) {
mListener.onRequestProgramsResponse(channel, getAllProgramsForChannel(channel));
}
return true;
}
case MSG_CLEAR_CHANNELS: {
clearChannels();
return true;
}
case MSG_CHECK_VERSION: {
checkVersion();
return true;
}
}
return false;
}
// Private methods
private void handleEvents(TunerChannel channel, List<EitItem> items) {
long channelId = getChannelId(channel);
if (channelId <= 0) {
return;
}
channel.setChannelId(channelId);
// Schedule the audio and caption tracks of the current program and the programs being
// listed after the current one into TIS.
if (mListener != null) {
mListener.onProgramsArrived(channel, items);
}
long currentTime = System.currentTimeMillis();
List<EitItem> oldItems = getAllProgramsForChannel(channel, currentTime,
currentTime + PROGRAM_QUERY_DURATION);
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// TODO: Find a right way to check if the programs are added outside.
boolean addedOutside = false;
for (EitItem item : oldItems) {
if (item.getEventId() == 0) {
// The event has been added outside TV tuner.
addedOutside = true;
break;
}
}
// Inserting programs only when there is no overlapping with existing data assuming that:
// 1. external EPG is more accurate and rich and
// 2. the data we add here will be updated when we apply external EPG.
if (addedOutside) {
// oldItemCount cannot be 0 if addedOutside is true.
int oldItemCount = oldItems.size();
for (EitItem newItem : items) {
if (newItem.getEndTimeUtcMillis() < currentTime) {
continue;
}
long newItemStartTime = newItem.getStartTimeUtcMillis();
long newItemEndTime = newItem.getEndTimeUtcMillis();
if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) {
// Start time smaller than that of any old items. Insert if no overlap.
if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue;
} else if (newItemStartTime
> oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) {
// Start time larger than that of any old item. Insert if no overlap.
if (newItemStartTime
< oldItems.get(oldItemCount - 1).getEndTimeUtcMillis()) continue;
} else {
int pos = Collections.binarySearch(oldItems, newItem,
new Comparator<EitItem>() {
@Override
public int compare(EitItem lhs, EitItem rhs) {
return Long.compare(lhs.getStartTimeUtcMillis(),
rhs.getStartTimeUtcMillis());
}
});
if (pos >= 0) {
// Same start Time found. Overlapped.
continue;
}
int insertPoint = -1 - pos;
// Check the two adjacent items.
if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis()
|| newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) {
continue;
}
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId()));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
}
}
applyBatch(channel.getName(), ops);
return;
}
List<EitItem> outdatedOldItems = new ArrayList<>();
Map<Integer, EitItem> newEitItemMap = new HashMap<>();
for (EitItem item : items) {
newEitItemMap.put(item.getEventId(), item);
}
for (EitItem oldItem : oldItems) {
EitItem item = newEitItemMap.get(oldItem.getEventId());
if (item == null) {
outdatedOldItems.add(oldItem);
continue;
}
// Since program descriptions arrive at different time, the older one may have the
// correct program description while the newer one has no clue what value is.
if (oldItem.getDescription() != null && item.getDescription() == null
&& oldItem.getEventId() == item.getEventId()
&& oldItem.getStartTime() == item.getStartTime()
&& oldItem.getLengthInSecond() == item.getLengthInSecond()
&& Objects.equals(oldItem.getContentRating(), item.getContentRating())
&& Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre())
&& Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) {
item.setDescription(oldItem.getDescription());
}
if (item.compareTo(oldItem) != 0) {
ops.add(buildContentProviderOperation(ContentProviderOperation.newUpdate(
TvContract.buildProgramUri(oldItem.getProgramId())), item, null));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
}
}
newEitItemMap.remove(item.getEventId());
}
for (EitItem unverifiedOldItems : outdatedOldItems) {
if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) {
// The given new EIT item list covers partial time span of EPG. Here, we delete old
// item only when it has an overlapping with the new EIT item list.
long startTime = unverifiedOldItems.getStartTimeUtcMillis();
long endTime = unverifiedOldItems.getEndTimeUtcMillis();
for (EitItem item : newEitItemMap.values()) {
long newItemStartTime = item.getStartTimeUtcMillis();
long newItemEndTime = item.getEndTimeUtcMillis();
if ((startTime >= newItemStartTime && startTime < newItemEndTime)
|| (endTime > newItemStartTime && endTime <= newItemEndTime)) {
ops.add(ContentProviderOperation.newDelete(TvContract.buildProgramUri(
unverifiedOldItems.getProgramId())).build());
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
}
break;
}
}
}
}
for (EitItem item : newEitItemMap.values()) {
if (item.getEndTimeUtcMillis() < currentTime) {
continue;
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
TvContract.Programs.CONTENT_URI), item, channel.getChannelId()));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
}
}
applyBatch(channel.getName(), ops);
}
private ContentProviderOperation buildContentProviderOperation(
ContentProviderOperation.Builder builder, EitItem item, Long channelId) {
if (channelId != null) {
builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId);
}
if (item != null) {
builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
.withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
item.getStartTimeUtcMillis())
.withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
item.getEndTimeUtcMillis())
.withValue(TvContract.Programs.COLUMN_CONTENT_RATING,
item.getContentRating())
.withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE,
item.getAudioLanguage())
.withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
item.getDescription())
.withValue(TvContract.Programs.COLUMN_VERSION_NUMBER,
item.getEventId());
}
return builder.build();
}
private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) {
try {
mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Error updating EPG " + channelName, e);
}
}
private void handleChannel(TunerChannel channel) {
long channelId = getChannelId(channel);
ContentValues values = new ContentValues();
values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName());
values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName());
values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid());
values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber());
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray());
values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION);
if (channelId <= 0) {
values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId);
values.put(TvContract.Channels.COLUMN_TYPE, "QAM256".equals(channel.getModulation())
? TvContract.Channels.TYPE_ATSC_C : TvContract.Channels.TYPE_ATSC_T);
values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber());
// ATSC doesn't have original_network_id
values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency());
Uri channelUri = mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI,
values);
channelId = ContentUris.parseId(channelUri);
} else {
mContext.getContentResolver().update(
TvContract.buildChannelUri(channelId), values, null, null);
}
channel.setChannelId(channelId);
mTunerChannelMap.put(channelId, channel);
mTunerChannelIdMap.put(channel, channelId);
if (mIsScanning.get()) {
mScannedChannels.add(channel);
mPreviousScannedChannels.remove(channel);
}
if (mListener != null) {
mListener.onChannelArrived(channel);
}
}
private void clearChannels() {
int count = mContext.getContentResolver().delete(mChannelsUri, null, null);
if (count > 0) {
// We have just deleted obsolete data. Now tell the user that he or she needs
// to perform the auto-scan again.
if (mListener != null) {
mListener.onRescanNeeded();
}
}
}
private void checkVersion() {
String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
CHANNEL_DATA_SELECTION_ARGS, selection,
new String[] {Integer.toString(VERSION)}, null)) {
if (cursor != null && cursor.moveToFirst()) {
// The stored channel data seem outdated. Delete them all.
clearChannels();
}
}
}
private long getChannelId(TunerChannel channel) {
Long channelId = mTunerChannelIdMap.get(channel);
if (channelId != null) {
return channelId;
}
mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
channelId = cursor.getLong(0);
byte[] providerData = cursor.getBlob(1);
TunerChannel tunerChannel = TunerChannel.parseFrom(providerData);
if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) {
channel.setChannelId(channelId);
mTunerChannelIdMap.put(channel, channelId);
mTunerChannelMap.put(channelId, channel);
return channelId;
}
} while (cursor.moveToNext());
}
}
return -1;
}
private List<EitItem> getAllProgramsForChannel(TunerChannel channel) {
return getAllProgramsForChannel(channel, null, null);
}
private List<EitItem> getAllProgramsForChannel(TunerChannel channel, @Nullable Long startTimeMs,
@Nullable Long endTimeMs) {
List<EitItem> items = new ArrayList<>();
long channelId = channel.getChannelId();
Uri programsUri = (startTimeMs == null || endTimeMs == null) ?
TvContract.buildProgramsUriForChannel(channelId) :
TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs);
try (Cursor cursor = mContext.getContentResolver().query(programsUri,
ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
long id = cursor.getLong(0);
String titleText = cursor.getString(1);
long startTime = ConvertUtils.convertUnixEpochToGPSTime(
cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS);
long endTime = ConvertUtils.convertUnixEpochToGPSTime(
cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS);
int lengthInSecond = (int) (endTime - startTime);
String contentRating = cursor.getString(4);
String broadcastGenre = cursor.getString(5);
String canonicalGenre = cursor.getString(6);
String description = cursor.getString(7);
int eventId = cursor.getInt(8);
EitItem eitItem = new EitItem(id, eventId, titleText, startTime, lengthInSecond,
contentRating, null, null, broadcastGenre, canonicalGenre, description);
items.add(eitItem);
} while (cursor.moveToNext());
}
}
return items;
}
private void buildChannelMap() {
ArrayList<TunerChannel> channels = new ArrayList<>();
try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
long channelId = cursor.getLong(0);
byte[] data = cursor.getBlob(1);
TunerChannel channel = TunerChannel.parseFrom(data);
if (channel != null) {
channel.setChannelId(channelId);
channels.add(channel);
}
} while (cursor.moveToNext());
}
}
mTunerChannelMap.clear();
mTunerChannelIdMap.clear();
for (TunerChannel channel : channels) {
mTunerChannelMap.put(channel.getChannelId(), channel);
mTunerChannelIdMap.put(channel, channel.getChannelId());
}
}
private static class ChannelEvent {
public final TunerChannel channel;
public final List<EitItem> eitItems;
public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) {
this.channel = channel;
this.eitItems = eitItems;
}
}
}