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