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