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.usbtuner.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.text.format.DateUtils;
33 import android.util.Log;
34 
35 import com.android.usbtuner.UsbTunerPreferences;
36 import com.android.usbtuner.data.PsipData.EitItem;
37 import com.android.usbtuner.data.TunerChannel;
38 import com.android.usbtuner.util.ConvertUtils;
39 import com.android.usbtuner.util.TisConfiguration;
40 
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.concurrent.ConcurrentHashMap;
47 import java.util.concurrent.ConcurrentSkipListMap;
48 import java.util.concurrent.ConcurrentSkipListSet;
49 import java.util.concurrent.CountDownLatch;
50 import java.util.concurrent.atomic.AtomicBoolean;
51 
52 /**
53  * Manages the channel info and EPG data through {@link TvInputManager}.
54  */
55 public class ChannelDataManager implements Handler.Callback {
56     private static final String TAG = "ChannelDataManager";
57 
58     private static final String[] ALL_PROGRAMS_SELECTION_ARGS = 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     private static final String[] CHANNEL_DATA_SELECTION_ARGS = new String[] {
69             TvContract.Channels._ID,
70             TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA };
71 
72     private static final int MSG_HANDLE_EVENTS = 1;
73     private static final int MSG_HANDLE_CHANNEL = 2;
74     private static final int MSG_BUILD_CHANNEL_MAP = 3;
75     private static final int MSG_REQUEST_PROGRAMS = 4;
76     private static final int MSG_CLEAR_CHANNELS = 6;
77     private static final int MSG_SCAN_COMPLETED = 7;
78 
79     /**
80      * A version number to enforce consistency of the channel data.
81      *
82      * WARNING: If a change in the database serialization lead to breaking the backward
83      * compatibility, you must increment this value so that the old data are purged,
84      * and the user is requested to perform the auto-scan again to generate the new data set.
85      */
86     private static final int VERSION = 6;
87 
88     private final Context mContext;
89     private String mInputId;
90     private ProgramInfoListener mListener;
91     private HandlerThread mHandlerThread;
92     private Handler mHandler;
93     private ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
94     private ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap;
95     private final Uri mChannelsUri;
96 
97     // Used for scanning
98     private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
99     private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
100     private AtomicBoolean mIsScanning;
101     private CountDownLatch mScanLatch;
102 
103     public interface ProgramInfoListener {
104 
105         /**
106          * Invoked when a request for getting programs of a channel has been processed and passes
107          * the requested channel and the programs retrieved from database to the listener.
108          */
onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs)109         void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs);
110 
111         /**
112          * Invoked when programs of a channel have been arrived and passes the arrived channel and
113          * programs to the listener.
114          */
onProgramsArrived(TunerChannel channel, List<EitItem> programs)115         void onProgramsArrived(TunerChannel channel, List<EitItem> programs);
116 
117         /**
118          * Invoked when a channel has been arrived and passes the arrived channel to the listener.
119          */
onChannelArrived(TunerChannel channel)120         void onChannelArrived(TunerChannel channel);
121 
122         /**
123          * Invoked when the database schema has been changed and the old-format channels have been
124          * deleted. A receiver should notify to a user that re-scanning channels is necessary.
125          */
onRescanNeeded()126         void onRescanNeeded();
127     }
128 
ChannelDataManager(Context context)129     public ChannelDataManager(Context context) {
130         mContext = context;
131         if (TisConfiguration.isInternalTunerTvInput(context)) {
132             mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
133                     InternalTunerTvInputService.class.getName())) + "/HW" +
134                     TisConfiguration.getTunerHwDeviceId(context);
135         } else {
136             mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
137                     UsbTunerTvInputService.class.getName()));
138         }
139         mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
140         mTunerChannelMap = new ConcurrentHashMap<>();
141         mTunerChannelIdMap = new ConcurrentSkipListMap<>();
142         mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread");
143         mHandlerThread.start();
144         mHandler = new Handler(mHandlerThread.getLooper(), this);
145         mIsScanning = new AtomicBoolean();
146         mScannedChannels = new ConcurrentSkipListSet<>();
147         mPreviousScannedChannels = new ConcurrentSkipListSet<>();
148     }
149 
150     // Public methods
checkDataVersion(Context context)151     public void checkDataVersion(Context context) {
152         int version = UsbTunerPreferences.getChannelDataVersion(context);
153         Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")");
154         if (version == VERSION) {
155             // Everything is awesome. Return and continue.
156             return;
157         }
158         setCurrentVersion(context);
159 
160         // The stored channel data seem outdated. Delete them all.
161         mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS);
162     }
163 
setCurrentVersion(Context context)164     public void setCurrentVersion(Context context) {
165         UsbTunerPreferences.setChannelDataVersion(context, VERSION);
166     }
167 
setListener(ProgramInfoListener listener)168     public void setListener(ProgramInfoListener listener) {
169         mListener = listener;
170     }
171 
release()172     public void release() {
173         mHandler.removeCallbacksAndMessages(null);
174         mHandlerThread.quitSafely();
175     }
176 
getChannel(long channelId)177     public TunerChannel getChannel(long channelId) {
178         TunerChannel channel = mTunerChannelMap.get(channelId);
179         if (channel != null) {
180             return channel;
181         }
182         mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
183         byte[] data = null;
184         try (Cursor cursor = mContext.getContentResolver().query(TvContract.buildChannelUri(
185                 channelId), CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
186             if (cursor != null && cursor.moveToFirst()) {
187                 data = cursor.getBlob(1);
188             }
189         }
190         if (data == null) {
191             return null;
192         }
193         channel = TunerChannel.parseFrom(data);
194         if (channel == null) {
195             return null;
196         }
197         channel.setChannelId(channelId);
198         return channel;
199     }
200 
requestProgramsData(TunerChannel channel)201     public void requestProgramsData(TunerChannel channel) {
202         mHandler.removeMessages(MSG_REQUEST_PROGRAMS);
203         mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget();
204     }
205 
notifyEventDetected(TunerChannel channel, List<EitItem> items)206     public void notifyEventDetected(TunerChannel channel, List<EitItem> items) {
207         mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget();
208     }
209 
notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)210     public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
211         mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
212     }
213 
214     // For scanning process
215     /**
216      * Invoked when starting a scanning mode. This method gets the previous channels to detect the
217      * obsolete channels after scanning and initializes the variables used for scanning.
218      */
notifyScanStarted()219     public void notifyScanStarted() {
220         mScannedChannels.clear();
221         mPreviousScannedChannels.clear();
222         try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
223                 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
224             if (cursor != null && cursor.moveToFirst()) {
225                 do {
226                     long channelId = cursor.getLong(0);
227                     byte[] data = cursor.getBlob(1);
228                     TunerChannel channel = TunerChannel.parseFrom(data);
229                     if (channel != null) {
230                         channel.setChannelId(channelId);
231                         mPreviousScannedChannels.add(channel);
232                     }
233                 } while (cursor.moveToNext());
234             }
235         }
236         mIsScanning.set(true);
237     }
238 
239     /**
240      * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler
241      * in order to wait for finish the remaining messages in the handler queue. Then removes the
242      * obsolete channels, which are previous scanned but are in the scanned result.
243      */
notifyScanCompleted()244     public void notifyScanCompleted() {
245         mScanLatch = new CountDownLatch(1);
246         mHandler.sendEmptyMessage(MSG_SCAN_COMPLETED);
247         try {
248             mScanLatch.await();
249         } catch (InterruptedException e) {
250             Log.e(TAG, "Scanning process could not finish", e);
251         }
252         mIsScanning.set(false);
253         if (mPreviousScannedChannels.isEmpty()) {
254             return;
255         }
256         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
257         for (TunerChannel channel : mPreviousScannedChannels) {
258             ops.add(ContentProviderOperation.newDelete(
259                     TvContract.buildChannelUri(channel.getChannelId())).build());
260         }
261         try {
262             mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
263         } catch (RemoteException | OperationApplicationException e) {
264             Log.e(TAG, "Error deleting obsolete channels", e);
265         }
266     }
267 
268     /**
269      * Returns the number of scanned channels in the scanning mode.
270      */
getScannedChannelCount()271     public int getScannedChannelCount() {
272         return mScannedChannels.size();
273     }
274 
275     @Override
handleMessage(Message msg)276     public boolean handleMessage(Message msg) {
277         switch (msg.what) {
278             case MSG_HANDLE_EVENTS: {
279                 ChannelEvent event = (ChannelEvent) msg.obj;
280                 handleEvents(event.channel, event.eitItems);
281                 return true;
282             }
283             case MSG_HANDLE_CHANNEL: {
284                 TunerChannel channel = (TunerChannel) msg.obj;
285                 handleChannel(channel);
286                 return true;
287             }
288             case MSG_BUILD_CHANNEL_MAP: {
289                 mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP);
290                 buildChannelMap();
291                 return true;
292             }
293             case MSG_REQUEST_PROGRAMS: {
294                 if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) {
295                     return true;
296                 }
297                 TunerChannel channel = (TunerChannel) msg.obj;
298                 if (mListener != null) {
299                     mListener.onRequestProgramsResponse(channel, getAllProgramsForChannel(channel));
300                 }
301                 return true;
302             }
303             case MSG_CLEAR_CHANNELS: {
304                 clearChannels();
305                 return true;
306             }
307             case MSG_SCAN_COMPLETED: {
308                 mScanLatch.countDown();
309                 return true;
310             }
311         }
312         return false;
313     }
314 
315     // Private methods
handleEvents(TunerChannel channel, List<EitItem> items)316     private void handleEvents(TunerChannel channel, List<EitItem> items) {
317         long channelId = getChannelId(channel);
318         if (channelId <= 0) {
319             return;
320         }
321         channel.setChannelId(channelId);
322         long currentTime = System.currentTimeMillis();
323         List<EitItem> oldItems = getAllProgramsForChannel(channel);
324         // TODO: Find a right to check if the programs are added outside.
325         for (EitItem item : oldItems) {
326             if (item.getEventId() == 0) {
327                 // The event has been added outside TV tuner. Do not update programs.
328                 return;
329             }
330         }
331         List<EitItem> outdatedOldItems = new ArrayList<>();
332         List<EitItem> programsAddedToEPG = new ArrayList<>();
333         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
334         Map<Integer, EitItem> eitItemMap = new HashMap<>();
335         for (EitItem item : items) {
336             eitItemMap.put(item.getEventId(), item);
337         }
338         for (EitItem oldItem : oldItems) {
339             EitItem item = eitItemMap.get(oldItem.getEventId());
340             if (item == null) {
341                 outdatedOldItems.add(oldItem);
342                 continue;
343             }
344             items.remove(item);
345             programsAddedToEPG.add(item);
346 
347             // Since program descriptions arrive at different time, the older one may have the
348             // correct program description while the newer one has no clue what value is.
349             if (oldItem.getDescription() != null && item.getDescription() == null
350                     && oldItem.getEventId() == item.getEventId()
351                     && oldItem.getStartTime() == item.getStartTime()
352                     && oldItem.getLengthInSecond() == item.getLengthInSecond()
353                     && Objects.equals(oldItem.getContentRating(), item.getContentRating())
354                     && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre())
355                     && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) {
356                 item.setDescription(oldItem.getDescription());
357             }
358             if (item.compareTo(oldItem) != 0) {
359                 ops.add(ContentProviderOperation.newUpdate(
360                         TvContract.buildProgramUri(oldItem.getProgramId()))
361                         .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
362                         .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
363                                 item.getStartTimeUtcMillis())
364                         .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
365                                 item.getEndTimeUtcMillis())
366                         .withValue(TvContract.Programs.COLUMN_CONTENT_RATING,
367                                 item.getContentRating())
368                         .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE,
369                                 item.getAudioLanguage())
370                         .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
371                                 item.getDescription())
372                         .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER,
373                                 item.getEventId())
374                         .build());
375             }
376         }
377         for (EitItem outdatedOldItem : outdatedOldItems) {
378             if (outdatedOldItem.getStartTimeUtcMillis() > currentTime) {
379                 ops.add(ContentProviderOperation.newDelete(
380                         TvContract.buildProgramUri(outdatedOldItem.getProgramId())).build());
381             }
382         }
383         for (EitItem item : items) {
384             ops.add(ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI)
385                     .withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId())
386                     .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
387                     .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
388                             item.getStartTimeUtcMillis())
389                     .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
390                             item.getEndTimeUtcMillis())
391                     .withValue(TvContract.Programs.COLUMN_CONTENT_RATING,
392                             item.getContentRating())
393                     .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE,
394                             item.getAudioLanguage())
395                     .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
396                             item.getDescription())
397                     .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER,
398                             item.getEventId())
399                     .build());
400             programsAddedToEPG.add(item);
401         }
402 
403         try {
404             mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
405         } catch (RemoteException | OperationApplicationException e) {
406             Log.e(TAG, "Error updating EPG " + channel.getName(), e);
407         }
408 
409         // Schedule the audio and caption tracks of the current program and the programs being
410         // listed after the current one into TIS.
411         if (mListener != null) {
412             mListener.onProgramsArrived(channel, programsAddedToEPG);
413         }
414     }
415 
handleChannel(TunerChannel channel)416     private void handleChannel(TunerChannel channel) {
417         long channelId = getChannelId(channel);
418         ContentValues values = new ContentValues();
419         values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName());
420         values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName());
421         values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid());
422         values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber());
423         values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName());
424         values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray());
425         values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription());
426         if (channelId <= 0) {
427             values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId);
428             values.put(TvContract.Channels.COLUMN_TYPE, "QAM256".equals(channel.getModulation())
429                     ? TvContract.Channels.TYPE_ATSC_C : TvContract.Channels.TYPE_ATSC_T);
430             values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber());
431 
432             // ATSC doesn't have original_network_id
433             values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency());
434 
435             Uri channelUri = mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI,
436                     values);
437             channelId = ContentUris.parseId(channelUri);
438         } else {
439             mContext.getContentResolver().update(
440                     TvContract.buildChannelUri(channelId), values, null, null);
441         }
442         channel.setChannelId(channelId);
443         mTunerChannelMap.put(channelId, channel);
444         mTunerChannelIdMap.put(channel, channelId);
445         if (mIsScanning.get()) {
446             mScannedChannels.add(channel);
447             mPreviousScannedChannels.remove(channel);
448         }
449         if (mListener != null) {
450             mListener.onChannelArrived(channel);
451         }
452     }
453 
clearChannels()454     private void clearChannels() {
455         int count = mContext.getContentResolver().delete(mChannelsUri, null, null);
456         if (count > 0) {
457             // We have just deleted obsolete data. Now tell the user that he or she needs
458             // to perform the auto-scan again.
459             if (mListener != null) {
460                 mListener.onRescanNeeded();
461             }
462         }
463     }
464 
getChannelId(TunerChannel channel)465     private long getChannelId(TunerChannel channel) {
466         Long channelId = mTunerChannelIdMap.get(channel);
467         if (channelId != null) {
468             return channelId;
469         }
470         mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP);
471         try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
472                 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
473             if (cursor != null && cursor.moveToFirst()) {
474                 do {
475                     channelId = cursor.getLong(0);
476                     byte[] providerData = cursor.getBlob(1);
477                     TunerChannel tunerChannel = TunerChannel.parseFrom(providerData);
478                     if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) {
479                         channel.setChannelId(channelId);
480                         mTunerChannelIdMap.put(channel, channelId);
481                         mTunerChannelMap.put(channelId, channel);
482                         return channelId;
483                     }
484                 } while (cursor.moveToNext());
485             }
486         }
487         return -1;
488     }
489 
getAllProgramsForChannel(TunerChannel channel)490     private List<EitItem> getAllProgramsForChannel(TunerChannel channel) {
491         List<EitItem> items = new ArrayList<>();
492         try (Cursor cursor = mContext.getContentResolver().query(
493                 TvContract.buildProgramsUriForChannel(channel.getChannelId()),
494                 ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) {
495             if (cursor != null && cursor.moveToFirst()) {
496                 do {
497                     long id = cursor.getLong(0);
498                     String titleText = cursor.getString(1);
499                     long startTime = ConvertUtils.convertUnixEpochToGPSTime(
500                             cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS);
501                     long endTime = ConvertUtils.convertUnixEpochToGPSTime(
502                             cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS);
503                     int lengthInSecond = (int) (endTime - startTime);
504                     String contentRating = cursor.getString(4);
505                     String broadcastGenre = cursor.getString(5);
506                     String canonicalGenre = cursor.getString(6);
507                     String description = cursor.getString(7);
508                     int eventId = cursor.getInt(8);
509                     EitItem eitItem = new EitItem(id, eventId, titleText, startTime, lengthInSecond,
510                             contentRating, null, null, broadcastGenre, canonicalGenre, description);
511                     items.add(eitItem);
512                 } while (cursor.moveToNext());
513             }
514         }
515         return items;
516     }
517 
buildChannelMap()518     private void buildChannelMap() {
519         ArrayList<TunerChannel> channels = new ArrayList<>();
520         try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
521                 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) {
522             if (cursor != null && cursor.moveToFirst()) {
523                 do {
524                     long channelId = cursor.getLong(0);
525                     byte[] data = cursor.getBlob(1);
526                     TunerChannel channel = TunerChannel.parseFrom(data);
527                     if (channel != null) {
528                         channel.setChannelId(channelId);
529                         channels.add(channel);
530                     }
531                 } while (cursor.moveToNext());
532             }
533         }
534         mTunerChannelMap.clear();
535         mTunerChannelIdMap.clear();
536         for (TunerChannel channel : channels) {
537             mTunerChannelMap.put(channel.getChannelId(), channel);
538             mTunerChannelIdMap.put(channel, channel.getChannelId());
539         }
540     }
541 
542     private static class ChannelEvent {
543         public final TunerChannel channel;
544         public final List<EitItem> eitItems;
545 
ChannelEvent(TunerChannel channel, List<EitItem> eitItems)546         public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) {
547             this.channel = channel;
548             this.eitItems = eitItems;
549         }
550     }
551 }
552