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