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