Home | History | Annotate | Download | only in tvinput
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.tuner.tvinput;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.OperationApplicationException;
     24 import android.database.Cursor;
     25 import android.media.tv.TvContract;
     26 import android.net.Uri;
     27 import android.os.Build;
     28 import android.os.Handler;
     29 import android.os.HandlerThread;
     30 import android.os.Message;
     31 import android.os.RemoteException;
     32 import android.support.annotation.Nullable;
     33 import android.text.format.DateUtils;
     34 import android.util.Log;
     35 import com.android.tv.common.BaseApplication;
     36 import com.android.tv.common.util.PermissionUtils;
     37 import com.android.tv.tuner.TunerPreferences;
     38 import com.android.tv.tuner.data.PsipData.EitItem;
     39 import com.android.tv.tuner.data.TunerChannel;
     40 import com.android.tv.tuner.util.ConvertUtils;
     41 import java.util.ArrayList;
     42 import java.util.Collections;
     43 import java.util.Comparator;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.Objects;
     48 import java.util.concurrent.ConcurrentHashMap;
     49 import java.util.concurrent.ConcurrentSkipListMap;
     50 import java.util.concurrent.ConcurrentSkipListSet;
     51 import java.util.concurrent.TimeUnit;
     52 import java.util.concurrent.atomic.AtomicBoolean;
     53 
     54 /** Manages the channel info and EPG data through {@link TvInputManager}. */
     55 public class ChannelDataManager implements Handler.Callback {
     56     private static final String TAG = "ChannelDataManager";
     57 
     58     private static final String[] ALL_PROGRAMS_SELECTION_ARGS =
     59             new String[] {
     60                 TvContract.Programs._ID,
     61                 TvContract.Programs.COLUMN_TITLE,
     62                 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
     63                 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
     64                 TvContract.Programs.COLUMN_CONTENT_RATING,
     65                 TvContract.Programs.COLUMN_BROADCAST_GENRE,
     66                 TvContract.Programs.COLUMN_CANONICAL_GENRE,
     67                 TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
     68                 TvContract.Programs.COLUMN_VERSION_NUMBER
     69             };
     70     private static final String[] CHANNEL_DATA_SELECTION_ARGS =
     71             new String[] {
     72                 TvContract.Channels._ID,
     73                 TvContract.Channels.COLUMN_LOCKED,
     74                 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
     75                 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
     76             };
     77 
     78     private static final int MSG_HANDLE_EVENTS = 1;
     79     private static final int MSG_HANDLE_CHANNEL = 2;
     80     private static final int MSG_BUILD_CHANNEL_MAP = 3;
     81     private static final int MSG_REQUEST_PROGRAMS = 4;
     82     private static final int MSG_CLEAR_CHANNELS = 6;
     83     private static final int MSG_CHECK_VERSION = 7;
     84 
     85     // Throttle the batch operations to avoid TransactionTooLargeException.
     86     private static final int BATCH_OPERATION_COUNT = 100;
     87     // At most 16 days of program information is delivered through an EIT,
     88     // according to the Chapter 6.4 of ATSC Recommended Practice A/69.
     89     private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16);
     90 
     91     /**
     92      * A version number to enforce consistency of the channel data.
     93      *
     94      * <p>WARNING: If a change in the database serialization lead to breaking the backward
     95      * compatibility, you must increment this value so that the old data are purged, and the user is
     96      * requested to perform the auto-scan again to generate the new data set.
     97      */
     98     private static final int VERSION = 6;
     99 
    100     private final Context mContext;
    101     private final String mInputId;
    102     private ProgramInfoListener mListener;
    103     private ChannelScanListener mChannelScanListener;
    104     private Handler mChannelScanHandler;
    105     private final HandlerThread mHandlerThread;
    106     private final Handler mHandler;
    107     private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
    108     private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap;
    109     private final Uri mChannelsUri;
    110 
    111     // Used for scanning
    112     private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
    113     private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
    114     private final AtomicBoolean mIsScanning;
    115     private final AtomicBoolean scanCompleted = new AtomicBoolean();
    116 
    117     public interface ProgramInfoListener {
    118 
    119         /**
    120          * Invoked when a request for getting programs of a channel has been processed and passes
    121          * the requested channel and the programs retrieved from database to the listener.
    122          */
    123         void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs);
    124 
    125         /**
    126          * Invoked when programs of a channel have been arrived and passes the arrived channel and
    127          * programs to the listener.
    128          */
    129         void onProgramsArrived(TunerChannel channel, List<EitItem> programs);
    130 
    131         /**
    132          * Invoked when a channel has been arrived and passes the arrived channel to the listener.
    133          */
    134         void onChannelArrived(TunerChannel channel);
    135 
    136         /**
    137          * Invoked when the database schema has been changed and the old-format channels have been
    138          * deleted. A receiver should notify to a user that re-scanning channels is necessary.
    139          */
    140         void onRescanNeeded();
    141     }
    142 
    143     public interface ChannelScanListener {
    144         /** Invoked when all pending channels have been handled. */
    145         void onChannelHandlingDone();
    146     }
    147 
    148     public ChannelDataManager(Context context) {
    149         mContext = context;
    150         mInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
    151         mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
    152         mTunerChannelMap = new ConcurrentHashMap<>();
    153         mTunerChannelIdMap = new ConcurrentSkipListMap<>();
    154         mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread");
    155         mHandlerThread.start();
    156         mHandler = new Handler(mHandlerThread.getLooper(), this);
    157         mIsScanning = new AtomicBoolean();
    158         mScannedChannels = new ConcurrentSkipListSet<>();
    159         mPreviousScannedChannels = new ConcurrentSkipListSet<>();
    160     }
    161 
    162     // Public methods
    163     public void checkDataVersion(Context context) {
    164         int version = TunerPreferences.getChannelDataVersion(context);
    165         Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")");
    166         if (version == VERSION) {
    167             // Everything is awesome. Return and continue.
    168             return;
    169         }
    170         setCurrentVersion(context);
    171 
    172         if (version == TunerPreferences.CHANNEL_DATA_VERSION_NOT_SET) {
    173             mHandler.sendEmptyMessage(MSG_CHECK_VERSION);
    174         } else {
    175             // The stored channel data seem outdated. Delete them all.
    176             mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS);
    177         }
    178     }
    179 
    180     public void setCurrentVersion(Context context) {
    181         TunerPreferences.setChannelDataVersion(context, VERSION);
    182     }
    183 
    184     public void setListener(ProgramInfoListener listener) {
    185         mListener = listener;
    186     }
    187 
    188     public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
    189         mChannelScanListener = listener;
    190         mChannelScanHandler = handler;
    191     }
    192 
    193     public void release() {
    194         mHandler.removeCallbacksAndMessages(null);
    195         releaseSafely();
    196     }
    197 
    198     public void releaseSafely() {
    199         mHandlerThread.quitSafely();
    200         mListener = null;
    201         mChannelScanListener = null;
    202         mChannelScanHandler = null;
    203     }
    204 
    205     public TunerChannel getChannel(long channelId) {
    206         TunerChannel channel = mTunerChannelMap.get(channelId);
    207         if (channel != null) {
    208             return channel;
    209         }
    210         mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
    211         byte[] data = null;
    212         boolean locked = false;
    213         try (Cursor cursor =
    214                 mContext.getContentResolver()
    215                         .query(
    216                                 TvContract.buildChannelUri(channelId),
    217                                 CHANNEL_DATA_SELECTION_ARGS,
    218                                 null,
    219                                 null,
    220                                 null)) {
    221             if (cursor != null && cursor.moveToFirst()) {
    222                 locked = cursor.getInt(1) > 0;
    223                 data = cursor.getBlob(2);
    224             }
    225         }
    226         if (data == null) {
    227             return null;
    228         }
    229         channel = TunerChannel.parseFrom(data);
    230         if (channel == null) {
    231             return null;
    232         }
    233         channel.setLocked(locked);
    234         channel.setChannelId(channelId);
    235         return channel;
    236     }
    237 
    238     public void requestProgramsData(TunerChannel channel) {
    239         mHandler.removeMessages(MSG_REQUEST_PROGRAMS);
    240         mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget();
    241     }
    242 
    243     public void notifyEventDetected(TunerChannel channel, List<EitItem> items) {
    244         mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget();
    245     }
    246 
    247     public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
    248         if (mIsScanning.get()) {
    249             // During scanning, channels should be handle first to improve scan time.
    250             // EIT items can be handled in background after channel scan.
    251             mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel));
    252         } else {
    253             mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
    254         }
    255     }
    256 
    257     // For scanning process
    258     /**
    259      * Invoked when starting a scanning mode. This method gets the previous channels to detect the
    260      * obsolete channels after scanning and initializes the variables used for scanning.
    261      */
    262     public void notifyScanStarted() {
    263         mScannedChannels.clear();
    264         mPreviousScannedChannels.clear();
    265         try (Cursor cursor =
    266                 mContext.getContentResolver()
    267                         .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
    268             if (cursor != null && cursor.moveToFirst()) {
    269                 do {
    270                     TunerChannel channel = TunerChannel.fromCursor(cursor);
    271                     if (channel != null) {
    272                         mPreviousScannedChannels.add(channel);
    273                     }
    274                 } while (cursor.moveToNext());
    275             }
    276         }
    277         mIsScanning.set(true);
    278     }
    279 
    280     /**
    281      * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler
    282      * in order to wait for finishing the remaining messages in the handler queue. Then removes the
    283      * obsolete channels, which are previously scanned but are not in the current scanned result.
    284      */
    285     public void notifyScanCompleted() {
    286         // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue
    287         // and avoid race conditions.
    288         scanCompleted.set(true);
    289         mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null));
    290     }
    291 
    292     public void scannedChannelHandlingCompleted() {
    293         mIsScanning.set(false);
    294         if (!mPreviousScannedChannels.isEmpty()) {
    295             ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    296             for (TunerChannel channel : mPreviousScannedChannels) {
    297                 ops.add(
    298                         ContentProviderOperation.newDelete(
    299                                         TvContract.buildChannelUri(channel.getChannelId()))
    300                                 .build());
    301             }
    302             try {
    303                 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
    304             } catch (RemoteException | OperationApplicationException e) {
    305                 Log.e(TAG, "Error deleting obsolete channels", e);
    306             }
    307         }
    308         if (mChannelScanListener != null && mChannelScanHandler != null) {
    309             mChannelScanHandler.post(
    310                     new Runnable() {
    311                         @Override
    312                         public void run() {
    313                             mChannelScanListener.onChannelHandlingDone();
    314                         }
    315                     });
    316         } else {
    317             Log.e(TAG, "Error. mChannelScanListener is null.");
    318         }
    319     }
    320 
    321     /** Returns the number of scanned channels in the scanning mode. */
    322     public int getScannedChannelCount() {
    323         return mScannedChannels.size();
    324     }
    325 
    326     /**
    327      * Removes all callbacks and messages in handler to avoid previous messages from last channel.
    328      */
    329     public void removeAllCallbacksAndMessages() {
    330         mHandler.removeCallbacksAndMessages(null);
    331     }
    332 
    333     @Override
    334     public boolean handleMessage(Message msg) {
    335         switch (msg.what) {
    336             case MSG_HANDLE_EVENTS:
    337                 {
    338                     ChannelEvent event = (ChannelEvent) msg.obj;
    339                     handleEvents(event.channel, event.eitItems);
    340                     return true;
    341                 }
    342             case MSG_HANDLE_CHANNEL:
    343                 {
    344                     TunerChannel channel = (TunerChannel) msg.obj;
    345                     if (channel != null) {
    346                         handleChannel(channel);
    347                     }
    348                     if (scanCompleted.get()
    349                             && mIsScanning.get()
    350                             && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) {
    351                         // Complete the scan when all found channels have already been handled.
    352                         scannedChannelHandlingCompleted();
    353                     }
    354                     return true;
    355                 }
    356             case MSG_BUILD_CHANNEL_MAP:
    357                 {
    358                     mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP);
    359                     buildChannelMap();
    360                     return true;
    361                 }
    362             case MSG_REQUEST_PROGRAMS:
    363                 {
    364                     if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) {
    365                         return true;
    366                     }
    367                     TunerChannel channel = (TunerChannel) msg.obj;
    368                     if (mListener != null) {
    369                         mListener.onRequestProgramsResponse(
    370                                 channel, getAllProgramsForChannel(channel));
    371                     }
    372                     return true;
    373                 }
    374             case MSG_CLEAR_CHANNELS:
    375                 {
    376                     clearChannels();
    377                     return true;
    378                 }
    379             case MSG_CHECK_VERSION:
    380                 {
    381                     checkVersion();
    382                     return true;
    383                 }
    384             default: // fall out
    385                 Log.w(TAG, "unexpected case in handleMessage ( " + msg.what + " )");
    386         }
    387         return false;
    388     }
    389 
    390     // Private methods
    391     private void handleEvents(TunerChannel channel, List<EitItem> items) {
    392         long channelId = getChannelId(channel);
    393         if (channelId <= 0) {
    394             return;
    395         }
    396         channel.setChannelId(channelId);
    397 
    398         // Schedule the audio and caption tracks of the current program and the programs being
    399         // listed after the current one into TIS.
    400         if (mListener != null) {
    401             mListener.onProgramsArrived(channel, items);
    402         }
    403 
    404         long currentTime = System.currentTimeMillis();
    405         List<EitItem> oldItems =
    406                 getAllProgramsForChannel(
    407                         channel, currentTime, currentTime + PROGRAM_QUERY_DURATION);
    408         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    409         // TODO: Find a right way to check if the programs are added outside.
    410         boolean addedOutside = false;
    411         for (EitItem item : oldItems) {
    412             if (item.getEventId() == 0) {
    413                 // The event has been added outside TV tuner.
    414                 addedOutside = true;
    415                 break;
    416             }
    417         }
    418 
    419         // Inserting programs only when there is no overlapping with existing data assuming that:
    420         // 1. external EPG is more accurate and rich and
    421         // 2. the data we add here will be updated when we apply external EPG.
    422         if (addedOutside) {
    423             // oldItemCount cannot be 0 if addedOutside is true.
    424             int oldItemCount = oldItems.size();
    425             for (EitItem newItem : items) {
    426                 if (newItem.getEndTimeUtcMillis() < currentTime) {
    427                     continue;
    428                 }
    429                 long newItemStartTime = newItem.getStartTimeUtcMillis();
    430                 long newItemEndTime = newItem.getEndTimeUtcMillis();
    431                 if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) {
    432                     // Start time smaller than that of any old items. Insert if no overlap.
    433                     if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue;
    434                 } else if (newItemStartTime
    435                         > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) {
    436                     // Start time larger than that of any old item. Insert if no overlap.
    437                     if (newItemStartTime < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis())
    438                         continue;
    439                 } else {
    440                     int pos =
    441                             Collections.binarySearch(
    442                                     oldItems,
    443                                     newItem,
    444                                     new Comparator<EitItem>() {
    445                                         @Override
    446                                         public int compare(EitItem lhs, EitItem rhs) {
    447                                             return Long.compare(
    448                                                     lhs.getStartTimeUtcMillis(),
    449                                                     rhs.getStartTimeUtcMillis());
    450                                         }
    451                                     });
    452                     if (pos >= 0) {
    453                         // Same start Time found. Overlapped.
    454                         continue;
    455                     }
    456                     int insertPoint = -1 - pos;
    457                     // Check the two adjacent items.
    458                     if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis()
    459                             || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) {
    460                         continue;
    461                     }
    462                 }
    463                 ops.add(
    464                         buildContentProviderOperation(
    465                                 ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI),
    466                                 newItem,
    467                                 channel));
    468                 if (ops.size() >= BATCH_OPERATION_COUNT) {
    469                     applyBatch(channel.getName(), ops);
    470                     ops.clear();
    471                 }
    472             }
    473             applyBatch(channel.getName(), ops);
    474             return;
    475         }
    476 
    477         List<EitItem> outdatedOldItems = new ArrayList<>();
    478         Map<Integer, EitItem> newEitItemMap = new HashMap<>();
    479         for (EitItem item : items) {
    480             newEitItemMap.put(item.getEventId(), item);
    481         }
    482         for (EitItem oldItem : oldItems) {
    483             EitItem item = newEitItemMap.get(oldItem.getEventId());
    484             if (item == null) {
    485                 outdatedOldItems.add(oldItem);
    486                 continue;
    487             }
    488 
    489             // Since program descriptions arrive at different time, the older one may have the
    490             // correct program description while the newer one has no clue what value is.
    491             if (oldItem.getDescription() != null
    492                     && item.getDescription() == null
    493                     && oldItem.getEventId() == item.getEventId()
    494                     && oldItem.getStartTime() == item.getStartTime()
    495                     && oldItem.getLengthInSecond() == item.getLengthInSecond()
    496                     && Objects.equals(oldItem.getContentRating(), item.getContentRating())
    497                     && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre())
    498                     && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) {
    499                 item.setDescription(oldItem.getDescription());
    500             }
    501             if (item.compareTo(oldItem) != 0) {
    502                 ops.add(
    503                         buildContentProviderOperation(
    504                                 ContentProviderOperation.newUpdate(
    505                                         TvContract.buildProgramUri(oldItem.getProgramId())),
    506                                 item,
    507                                 null));
    508                 if (ops.size() >= BATCH_OPERATION_COUNT) {
    509                     applyBatch(channel.getName(), ops);
    510                     ops.clear();
    511                 }
    512             }
    513             newEitItemMap.remove(item.getEventId());
    514         }
    515         for (EitItem unverifiedOldItems : outdatedOldItems) {
    516             if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) {
    517                 // The given new EIT item list covers partial time span of EPG. Here, we delete old
    518                 // item only when it has an overlapping with the new EIT item list.
    519                 long startTime = unverifiedOldItems.getStartTimeUtcMillis();
    520                 long endTime = unverifiedOldItems.getEndTimeUtcMillis();
    521                 for (EitItem item : newEitItemMap.values()) {
    522                     long newItemStartTime = item.getStartTimeUtcMillis();
    523                     long newItemEndTime = item.getEndTimeUtcMillis();
    524                     if ((startTime >= newItemStartTime && startTime < newItemEndTime)
    525                             || (endTime > newItemStartTime && endTime <= newItemEndTime)) {
    526                         ops.add(
    527                                 ContentProviderOperation.newDelete(
    528                                                 TvContract.buildProgramUri(
    529                                                         unverifiedOldItems.getProgramId()))
    530                                         .build());
    531                         if (ops.size() >= BATCH_OPERATION_COUNT) {
    532                             applyBatch(channel.getName(), ops);
    533                             ops.clear();
    534                         }
    535                         break;
    536                     }
    537                 }
    538             }
    539         }
    540         for (EitItem item : newEitItemMap.values()) {
    541             if (item.getEndTimeUtcMillis() < currentTime) {
    542                 continue;
    543             }
    544             ops.add(
    545                     buildContentProviderOperation(
    546                             ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI),
    547                             item,
    548                             channel));
    549             if (ops.size() >= BATCH_OPERATION_COUNT) {
    550                 applyBatch(channel.getName(), ops);
    551                 ops.clear();
    552             }
    553         }
    554 
    555         applyBatch(channel.getName(), ops);
    556     }
    557 
    558     private ContentProviderOperation buildContentProviderOperation(
    559             ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) {
    560         if (channel != null) {
    561             builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId());
    562             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    563                 builder.withValue(
    564                         TvContract.Programs.COLUMN_RECORDING_PROHIBITED,
    565                         channel.isRecordingProhibited() ? 1 : 0);
    566             }
    567         }
    568         if (item != null) {
    569             builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
    570                     .withValue(
    571                             TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
    572                             item.getStartTimeUtcMillis())
    573                     .withValue(
    574                             TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
    575                             item.getEndTimeUtcMillis())
    576                     .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, item.getContentRating())
    577                     .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, item.getAudioLanguage())
    578                     .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, item.getDescription())
    579                     .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, item.getEventId());
    580         }
    581         return builder.build();
    582     }
    583 
    584     private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) {
    585         try {
    586             mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations);
    587         } catch (RemoteException | OperationApplicationException e) {
    588             Log.e(TAG, "Error updating EPG " + channelName, e);
    589         }
    590     }
    591 
    592     private void handleChannel(TunerChannel channel) {
    593         long channelId = getChannelId(channel);
    594         ContentValues values = new ContentValues();
    595         values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName());
    596         values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName());
    597         values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid());
    598         values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber());
    599         values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName());
    600         values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray());
    601         values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription());
    602         values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat());
    603         values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION);
    604         values.put(
    605                 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
    606                 channel.isRecordingProhibited() ? 1 : 0);
    607 
    608         if (channelId <= 0) {
    609             values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId);
    610             values.put(
    611                     TvContract.Channels.COLUMN_TYPE,
    612                     "QAM256".equals(channel.getModulation())
    613                             ? TvContract.Channels.TYPE_ATSC_C
    614                             : TvContract.Channels.TYPE_ATSC_T);
    615             values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber());
    616 
    617             // ATSC doesn't have original_network_id
    618             values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency());
    619 
    620             Uri channelUri =
    621                     mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
    622             channelId = ContentUris.parseId(channelUri);
    623         } else {
    624             mContext.getContentResolver()
    625                     .update(TvContract.buildChannelUri(channelId), values, null, null);
    626         }
    627         channel.setChannelId(channelId);
    628         mTunerChannelMap.put(channelId, channel);
    629         mTunerChannelIdMap.put(channel, channelId);
    630         if (mIsScanning.get()) {
    631             mScannedChannels.add(channel);
    632             mPreviousScannedChannels.remove(channel);
    633         }
    634         if (mListener != null) {
    635             mListener.onChannelArrived(channel);
    636         }
    637     }
    638 
    639     private void clearChannels() {
    640         int count = mContext.getContentResolver().delete(mChannelsUri, null, null);
    641         if (count > 0) {
    642             // We have just deleted obsolete data. Now tell the user that he or she needs
    643             // to perform the auto-scan again.
    644             if (mListener != null) {
    645                 mListener.onRescanNeeded();
    646             }
    647         }
    648     }
    649 
    650     private void checkVersion() {
    651         if (PermissionUtils.hasAccessAllEpg(mContext)) {
    652             String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
    653             try (Cursor cursor =
    654                     mContext.getContentResolver()
    655                             .query(
    656                                     mChannelsUri,
    657                                     CHANNEL_DATA_SELECTION_ARGS,
    658                                     selection,
    659                                     new String[] {Integer.toString(VERSION)},
    660                                     null)) {
    661                 if (cursor != null && cursor.moveToFirst()) {
    662                     // The stored channel data seem outdated. Delete them all.
    663                     clearChannels();
    664                 }
    665             }
    666         } else {
    667             try (Cursor cursor =
    668                     mContext.getContentResolver()
    669                             .query(
    670                                     mChannelsUri,
    671                                     new String[] {
    672                                         TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
    673                                     },
    674                                     null,
    675                                     null,
    676                                     null)) {
    677                 if (cursor != null) {
    678                     while (cursor.moveToNext()) {
    679                         int version = cursor.getInt(0);
    680                         if (version != VERSION) {
    681                             clearChannels();
    682                             break;
    683                         }
    684                     }
    685                 }
    686             }
    687         }
    688     }
    689 
    690     private long getChannelId(TunerChannel channel) {
    691         Long channelId = mTunerChannelIdMap.get(channel);
    692         if (channelId != null) {
    693             return channelId;
    694         }
    695         mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
    696         try (Cursor cursor =
    697                 mContext.getContentResolver()
    698                         .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
    699             if (cursor != null && cursor.moveToFirst()) {
    700                 do {
    701                     TunerChannel tunerChannel = TunerChannel.fromCursor(cursor);
    702                     if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) {
    703                         mTunerChannelIdMap.put(channel, tunerChannel.getChannelId());
    704                         mTunerChannelMap.put(tunerChannel.getChannelId(), channel);
    705                         return tunerChannel.getChannelId();
    706                     }
    707                 } while (cursor.moveToNext());
    708             }
    709         }
    710         return -1;
    711     }
    712 
    713     private List<EitItem> getAllProgramsForChannel(TunerChannel channel) {
    714         return getAllProgramsForChannel(channel, null, null);
    715     }
    716 
    717     private List<EitItem> getAllProgramsForChannel(
    718             TunerChannel channel, @Nullable Long startTimeMs, @Nullable Long endTimeMs) {
    719         List<EitItem> items = new ArrayList<>();
    720         long channelId = channel.getChannelId();
    721         Uri programsUri =
    722                 (startTimeMs == null || endTimeMs == null)
    723                         ? TvContract.buildProgramsUriForChannel(channelId)
    724                         : TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs);
    725         try (Cursor cursor =
    726                 mContext.getContentResolver()
    727                         .query(programsUri, ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) {
    728             if (cursor != null && cursor.moveToFirst()) {
    729                 do {
    730                     long id = cursor.getLong(0);
    731                     String titleText = cursor.getString(1);
    732                     long startTime =
    733                             ConvertUtils.convertUnixEpochToGPSTime(
    734                                     cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS);
    735                     long endTime =
    736                             ConvertUtils.convertUnixEpochToGPSTime(
    737                                     cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS);
    738                     int lengthInSecond = (int) (endTime - startTime);
    739                     String contentRating = cursor.getString(4);
    740                     String broadcastGenre = cursor.getString(5);
    741                     String canonicalGenre = cursor.getString(6);
    742                     String description = cursor.getString(7);
    743                     int eventId = cursor.getInt(8);
    744                     EitItem eitItem =
    745                             new EitItem(
    746                                     id,
    747                                     eventId,
    748                                     titleText,
    749                                     startTime,
    750                                     lengthInSecond,
    751                                     contentRating,
    752                                     null,
    753                                     null,
    754                                     broadcastGenre,
    755                                     canonicalGenre,
    756                                     description);
    757                     items.add(eitItem);
    758                 } while (cursor.moveToNext());
    759             }
    760         }
    761         return items;
    762     }
    763 
    764     private void buildChannelMap() {
    765         ArrayList<TunerChannel> channels = new ArrayList<>();
    766         try (Cursor cursor =
    767                 mContext.getContentResolver()
    768                         .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
    769             if (cursor != null && cursor.moveToFirst()) {
    770                 do {
    771                     TunerChannel channel = TunerChannel.fromCursor(cursor);
    772                     if (channel != null) {
    773                         channels.add(channel);
    774                     }
    775                 } while (cursor.moveToNext());
    776             }
    777         }
    778         mTunerChannelMap.clear();
    779         mTunerChannelIdMap.clear();
    780         for (TunerChannel channel : channels) {
    781             mTunerChannelMap.put(channel.getChannelId(), channel);
    782             mTunerChannelIdMap.put(channel, channel.getChannelId());
    783         }
    784     }
    785 
    786     private static class ChannelEvent {
    787         public final TunerChannel channel;
    788         public final List<EitItem> eitItems;
    789 
    790         public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) {
    791             this.channel = channel;
    792             this.eitItems = eitItems;
    793         }
    794     }
    795 }
    796