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
17package com.android.usbtuner.tvinput;
18
19import android.content.ComponentName;
20import android.content.ContentProviderOperation;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.OperationApplicationException;
25import android.database.Cursor;
26import android.media.tv.TvContract;
27import android.net.Uri;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.Message;
31import android.os.RemoteException;
32import android.text.format.DateUtils;
33import android.util.Log;
34
35import com.android.usbtuner.UsbTunerPreferences;
36import com.android.usbtuner.data.PsipData.EitItem;
37import com.android.usbtuner.data.TunerChannel;
38import com.android.usbtuner.util.ConvertUtils;
39import com.android.usbtuner.util.TisConfiguration;
40
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.List;
44import java.util.Map;
45import java.util.Objects;
46import java.util.concurrent.ConcurrentHashMap;
47import java.util.concurrent.ConcurrentSkipListMap;
48import java.util.concurrent.ConcurrentSkipListSet;
49import java.util.concurrent.CountDownLatch;
50import java.util.concurrent.atomic.AtomicBoolean;
51
52/**
53 * Manages the channel info and EPG data through {@link TvInputManager}.
54 */
55public 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         */
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         */
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         */
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         */
126        void onRescanNeeded();
127    }
128
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
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
164    public void setCurrentVersion(Context context) {
165        UsbTunerPreferences.setChannelDataVersion(context, VERSION);
166    }
167
168    public void setListener(ProgramInfoListener listener) {
169        mListener = listener;
170    }
171
172    public void release() {
173        mHandler.removeCallbacksAndMessages(null);
174        mHandlerThread.quitSafely();
175    }
176
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
201    public void requestProgramsData(TunerChannel channel) {
202        mHandler.removeMessages(MSG_REQUEST_PROGRAMS);
203        mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget();
204    }
205
206    public void notifyEventDetected(TunerChannel channel, List<EitItem> items) {
207        mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget();
208    }
209
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     */
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     */
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     */
271    public int getScannedChannelCount() {
272        return mScannedChannels.size();
273    }
274
275    @Override
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
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
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
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
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
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
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
546        public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) {
547            this.channel = channel;
548            this.eitItems = eitItems;
549        }
550    }
551}
552