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