aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java')
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java795
1 files changed, 795 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
new file mode 100644
index 00000000..c1d8f278
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -0,0 +1,795 @@
+/*
+ * 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.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.Build;
+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.common.BaseApplication;
+import com.android.tv.common.util.PermissionUtils;
+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_LOCKED,
+ 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.
+ *
+ * <p>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 = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
+ 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);
+ releaseSafely();
+ }
+
+ public void releaseSafely() {
+ mHandlerThread.quitSafely();
+ mListener = null;
+ mChannelScanListener = null;
+ mChannelScanHandler = null;
+ }
+
+ public TunerChannel getChannel(long channelId) {
+ TunerChannel channel = mTunerChannelMap.get(channelId);
+ if (channel != null) {
+ return channel;
+ }
+ mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
+ byte[] data = null;
+ boolean locked = false;
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ TvContract.buildChannelUri(channelId),
+ CHANNEL_DATA_SELECTION_ARGS,
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ locked = cursor.getInt(1) > 0;
+ data = cursor.getBlob(2);
+ }
+ }
+ if (data == null) {
+ return null;
+ }
+ channel = TunerChannel.parseFrom(data);
+ if (channel == null) {
+ return null;
+ }
+ channel.setLocked(locked);
+ 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 {
+ TunerChannel channel = TunerChannel.fromCursor(cursor);
+ if (channel != null) {
+ 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;
+ }
+ default: // fall out
+ Log.w(TAG, "unexpected case in handleMessage ( " + msg.what + " )");
+ }
+ 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));
+ 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));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+
+ applyBatch(channel.getName(), ops);
+ }
+
+ private ContentProviderOperation buildContentProviderOperation(
+ ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) {
+ if (channel != null) {
+ builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ builder.withValue(
+ TvContract.Programs.COLUMN_RECORDING_PROHIBITED,
+ channel.isRecordingProhibited() ? 1 : 0);
+ }
+ }
+ 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_VIDEO_FORMAT, channel.getVideoFormat());
+ values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION);
+ values.put(
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ channel.isRecordingProhibited() ? 1 : 0);
+
+ 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() {
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ 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();
+ }
+ }
+ } else {
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ mChannelsUri,
+ new String[] {
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
+ },
+ null,
+ null,
+ null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ int version = cursor.getInt(0);
+ if (version != VERSION) {
+ clearChannels();
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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 {
+ TunerChannel tunerChannel = TunerChannel.fromCursor(cursor);
+ if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) {
+ mTunerChannelIdMap.put(channel, tunerChannel.getChannelId());
+ mTunerChannelMap.put(tunerChannel.getChannelId(), channel);
+ return tunerChannel.getChannelId();
+ }
+ } 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 {
+ TunerChannel channel = TunerChannel.fromCursor(cursor);
+ if (channel != null) {
+ 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;
+ }
+ }
+}