blob: 57affe221365b38ac042bbca0c66ea54221bff38 [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.usbtuner.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.text.format.DateUtils;
import android.util.Log;
import com.android.usbtuner.UsbTunerPreferences;
import com.android.usbtuner.data.PsipData.EitItem;
import com.android.usbtuner.data.TunerChannel;
import com.android.usbtuner.util.ConvertUtils;
import com.android.usbtuner.util.TisConfiguration;
import java.util.ArrayList;
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.CountDownLatch;
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 };
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_SCAN_COMPLETED = 7;
/**
* 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 String mInputId;
private ProgramInfoListener mListener;
private HandlerThread mHandlerThread;
private Handler mHandler;
private ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
private ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap;
private final Uri mChannelsUri;
// Used for scanning
private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
private AtomicBoolean mIsScanning;
private CountDownLatch mScanLatch;
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 ChannelDataManager(Context context) {
mContext = context;
if (TisConfiguration.isInternalTunerTvInput(context)) {
mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
InternalTunerTvInputService.class.getName())) + "/HW" +
TisConfiguration.getTunerHwDeviceId(context);
} else {
mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
UsbTunerTvInputService.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 = UsbTunerPreferences.getChannelDataVersion(context);
Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")");
if (version == VERSION) {
// Everything is awesome. Return and continue.
return;
}
setCurrentVersion(context);
// The stored channel data seem outdated. Delete them all.
mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS);
}
public void setCurrentVersion(Context context) {
UsbTunerPreferences.setChannelDataVersion(context, VERSION);
}
public void setListener(ProgramInfoListener listener) {
mListener = listener;
}
public void release() {
mHandler.removeCallbacksAndMessages(null);
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) {
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 finish the remaining messages in the handler queue. Then removes the
* obsolete channels, which are previous scanned but are in the scanned result.
*/
public void notifyScanCompleted() {
mScanLatch = new CountDownLatch(1);
mHandler.sendEmptyMessage(MSG_SCAN_COMPLETED);
try {
mScanLatch.await();
} catch (InterruptedException e) {
Log.e(TAG, "Scanning process could not finish", e);
}
mIsScanning.set(false);
if (mPreviousScannedChannels.isEmpty()) {
return;
}
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);
}
}
/**
* Returns the number of scanned channels in the scanning mode.
*/
public int getScannedChannelCount() {
return mScannedChannels.size();
}
@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;
handleChannel(channel);
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_SCAN_COMPLETED: {
mScanLatch.countDown();
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);
long currentTime = System.currentTimeMillis();
List<EitItem> oldItems = getAllProgramsForChannel(channel);
// TODO: Find a right to check if the programs are added outside.
for (EitItem item : oldItems) {
if (item.getEventId() == 0) {
// The event has been added outside TV tuner. Do not update programs.
return;
}
}
List<EitItem> outdatedOldItems = new ArrayList<>();
List<EitItem> programsAddedToEPG = new ArrayList<>();
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
Map<Integer, EitItem> eitItemMap = new HashMap<>();
for (EitItem item : items) {
eitItemMap.put(item.getEventId(), item);
}
for (EitItem oldItem : oldItems) {
EitItem item = eitItemMap.get(oldItem.getEventId());
if (item == null) {
outdatedOldItems.add(oldItem);
continue;
}
items.remove(item);
programsAddedToEPG.add(item);
// 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(ContentProviderOperation.newUpdate(
TvContract.buildProgramUri(oldItem.getProgramId()))
.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())
.build());
}
}
for (EitItem outdatedOldItem : outdatedOldItems) {
if (outdatedOldItem.getStartTimeUtcMillis() > currentTime) {
ops.add(ContentProviderOperation.newDelete(
TvContract.buildProgramUri(outdatedOldItem.getProgramId())).build());
}
}
for (EitItem item : items) {
ops.add(ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI)
.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId())
.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())
.build());
programsAddedToEPG.add(item);
}
try {
mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Error updating EPG " + channel.getName(), e);
}
// 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, programsAddedToEPG);
}
}
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());
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 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) {
List<EitItem> items = new ArrayList<>();
try (Cursor cursor = mContext.getContentResolver().query(
TvContract.buildProgramsUriForChannel(channel.getChannelId()),
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;
}
}
}