1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner.tvinput; 18 19 import android.content.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.Nullable; 33 import android.text.format.DateUtils; 34 import android.util.Log; 35 import com.android.tv.common.BaseApplication; 36 import com.android.tv.common.util.PermissionUtils; 37 import com.android.tv.tuner.TunerPreferences; 38 import com.android.tv.tuner.data.PsipData.EitItem; 39 import com.android.tv.tuner.data.TunerChannel; 40 import com.android.tv.tuner.util.ConvertUtils; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.concurrent.ConcurrentHashMap; 49 import java.util.concurrent.ConcurrentSkipListMap; 50 import java.util.concurrent.ConcurrentSkipListSet; 51 import java.util.concurrent.TimeUnit; 52 import java.util.concurrent.atomic.AtomicBoolean; 53 54 /** Manages the channel info and EPG data through {@link TvInputManager}. */ 55 public class ChannelDataManager implements Handler.Callback { 56 private static final String TAG = "ChannelDataManager"; 57 58 private static final String[] ALL_PROGRAMS_SELECTION_ARGS = 59 new String[] { 60 TvContract.Programs._ID, 61 TvContract.Programs.COLUMN_TITLE, 62 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 63 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 64 TvContract.Programs.COLUMN_CONTENT_RATING, 65 TvContract.Programs.COLUMN_BROADCAST_GENRE, 66 TvContract.Programs.COLUMN_CANONICAL_GENRE, 67 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 68 TvContract.Programs.COLUMN_VERSION_NUMBER 69 }; 70 private static final String[] CHANNEL_DATA_SELECTION_ARGS = 71 new String[] { 72 TvContract.Channels._ID, 73 TvContract.Channels.COLUMN_LOCKED, 74 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, 75 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 76 }; 77 78 private static final int MSG_HANDLE_EVENTS = 1; 79 private static final int MSG_HANDLE_CHANNEL = 2; 80 private static final int MSG_BUILD_CHANNEL_MAP = 3; 81 private static final int MSG_REQUEST_PROGRAMS = 4; 82 private static final int MSG_CLEAR_CHANNELS = 6; 83 private static final int MSG_CHECK_VERSION = 7; 84 85 // Throttle the batch operations to avoid TransactionTooLargeException. 86 private static final int BATCH_OPERATION_COUNT = 100; 87 // At most 16 days of program information is delivered through an EIT, 88 // according to the Chapter 6.4 of ATSC Recommended Practice A/69. 89 private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16); 90 91 /** 92 * A version number to enforce consistency of the channel data. 93 * 94 * <p>WARNING: If a change in the database serialization lead to breaking the backward 95 * compatibility, you must increment this value so that the old data are purged, and the user is 96 * requested to perform the auto-scan again to generate the new data set. 97 */ 98 private static final int VERSION = 6; 99 100 private final Context mContext; 101 private final String mInputId; 102 private ProgramInfoListener mListener; 103 private ChannelScanListener mChannelScanListener; 104 private Handler mChannelScanHandler; 105 private final HandlerThread mHandlerThread; 106 private final Handler mHandler; 107 private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap; 108 private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap; 109 private final Uri mChannelsUri; 110 111 // Used for scanning 112 private final ConcurrentSkipListSet<TunerChannel> mScannedChannels; 113 private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels; 114 private final AtomicBoolean mIsScanning; 115 private final AtomicBoolean scanCompleted = new AtomicBoolean(); 116 117 public interface ProgramInfoListener { 118 119 /** 120 * Invoked when a request for getting programs of a channel has been processed and passes 121 * the requested channel and the programs retrieved from database to the listener. 122 */ 123 void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs); 124 125 /** 126 * Invoked when programs of a channel have been arrived and passes the arrived channel and 127 * programs to the listener. 128 */ 129 void onProgramsArrived(TunerChannel channel, List<EitItem> programs); 130 131 /** 132 * Invoked when a channel has been arrived and passes the arrived channel to the listener. 133 */ 134 void onChannelArrived(TunerChannel channel); 135 136 /** 137 * Invoked when the database schema has been changed and the old-format channels have been 138 * deleted. A receiver should notify to a user that re-scanning channels is necessary. 139 */ 140 void onRescanNeeded(); 141 } 142 143 public interface ChannelScanListener { 144 /** Invoked when all pending channels have been handled. */ 145 void onChannelHandlingDone(); 146 } 147 148 public ChannelDataManager(Context context) { 149 mContext = context; 150 mInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); 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 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 180 public void setCurrentVersion(Context context) { 181 TunerPreferences.setChannelDataVersion(context, VERSION); 182 } 183 184 public void setListener(ProgramInfoListener listener) { 185 mListener = listener; 186 } 187 188 public void setChannelScanListener(ChannelScanListener listener, Handler handler) { 189 mChannelScanListener = listener; 190 mChannelScanHandler = handler; 191 } 192 193 public void release() { 194 mHandler.removeCallbacksAndMessages(null); 195 releaseSafely(); 196 } 197 198 public void releaseSafely() { 199 mHandlerThread.quitSafely(); 200 mListener = null; 201 mChannelScanListener = null; 202 mChannelScanHandler = null; 203 } 204 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 238 public void requestProgramsData(TunerChannel channel) { 239 mHandler.removeMessages(MSG_REQUEST_PROGRAMS); 240 mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget(); 241 } 242 243 public void notifyEventDetected(TunerChannel channel, List<EitItem> items) { 244 mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget(); 245 } 246 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 */ 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 */ 285 public void notifyScanCompleted() { 286 // Send a dummy 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 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 (mChannelScanListener != null && mChannelScanHandler != null) { 309 mChannelScanHandler.post( 310 new Runnable() { 311 @Override 312 public void run() { 313 mChannelScanListener.onChannelHandlingDone(); 314 } 315 }); 316 } else { 317 Log.e(TAG, "Error. mChannelScanListener is null."); 318 } 319 } 320 321 /** Returns the number of scanned channels in the scanning mode. */ 322 public int getScannedChannelCount() { 323 return mScannedChannels.size(); 324 } 325 326 /** 327 * Removes all callbacks and messages in handler to avoid previous messages from last channel. 328 */ 329 public void removeAllCallbacksAndMessages() { 330 mHandler.removeCallbacksAndMessages(null); 331 } 332 333 @Override 334 public boolean handleMessage(Message msg) { 335 switch (msg.what) { 336 case MSG_HANDLE_EVENTS: 337 { 338 ChannelEvent event = (ChannelEvent) msg.obj; 339 handleEvents(event.channel, event.eitItems); 340 return true; 341 } 342 case MSG_HANDLE_CHANNEL: 343 { 344 TunerChannel channel = (TunerChannel) msg.obj; 345 if (channel != null) { 346 handleChannel(channel); 347 } 348 if (scanCompleted.get() 349 && mIsScanning.get() 350 && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) { 351 // Complete the scan when all found channels have already been handled. 352 scannedChannelHandlingCompleted(); 353 } 354 return true; 355 } 356 case MSG_BUILD_CHANNEL_MAP: 357 { 358 mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP); 359 buildChannelMap(); 360 return true; 361 } 362 case MSG_REQUEST_PROGRAMS: 363 { 364 if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) { 365 return true; 366 } 367 TunerChannel channel = (TunerChannel) msg.obj; 368 if (mListener != null) { 369 mListener.onRequestProgramsResponse( 370 channel, getAllProgramsForChannel(channel)); 371 } 372 return true; 373 } 374 case MSG_CLEAR_CHANNELS: 375 { 376 clearChannels(); 377 return true; 378 } 379 case MSG_CHECK_VERSION: 380 { 381 checkVersion(); 382 return true; 383 } 384 default: // fall out 385 Log.w(TAG, "unexpected case in handleMessage ( " + msg.what + " )"); 386 } 387 return false; 388 } 389 390 // Private methods 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 new Comparator<EitItem>() { 445 @Override 446 public int compare(EitItem lhs, EitItem rhs) { 447 return Long.compare( 448 lhs.getStartTimeUtcMillis(), 449 rhs.getStartTimeUtcMillis()); 450 } 451 }); 452 if (pos >= 0) { 453 // Same start Time found. Overlapped. 454 continue; 455 } 456 int insertPoint = -1 - pos; 457 // Check the two adjacent items. 458 if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis() 459 || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) { 460 continue; 461 } 462 } 463 ops.add( 464 buildContentProviderOperation( 465 ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI), 466 newItem, 467 channel)); 468 if (ops.size() >= BATCH_OPERATION_COUNT) { 469 applyBatch(channel.getName(), ops); 470 ops.clear(); 471 } 472 } 473 applyBatch(channel.getName(), ops); 474 return; 475 } 476 477 List<EitItem> outdatedOldItems = new ArrayList<>(); 478 Map<Integer, EitItem> newEitItemMap = new HashMap<>(); 479 for (EitItem item : items) { 480 newEitItemMap.put(item.getEventId(), item); 481 } 482 for (EitItem oldItem : oldItems) { 483 EitItem item = newEitItemMap.get(oldItem.getEventId()); 484 if (item == null) { 485 outdatedOldItems.add(oldItem); 486 continue; 487 } 488 489 // Since program descriptions arrive at different time, the older one may have the 490 // correct program description while the newer one has no clue what value is. 491 if (oldItem.getDescription() != null 492 && item.getDescription() == null 493 && oldItem.getEventId() == item.getEventId() 494 && oldItem.getStartTime() == item.getStartTime() 495 && oldItem.getLengthInSecond() == item.getLengthInSecond() 496 && Objects.equals(oldItem.getContentRating(), item.getContentRating()) 497 && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre()) 498 && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) { 499 item.setDescription(oldItem.getDescription()); 500 } 501 if (item.compareTo(oldItem) != 0) { 502 ops.add( 503 buildContentProviderOperation( 504 ContentProviderOperation.newUpdate( 505 TvContract.buildProgramUri(oldItem.getProgramId())), 506 item, 507 null)); 508 if (ops.size() >= BATCH_OPERATION_COUNT) { 509 applyBatch(channel.getName(), ops); 510 ops.clear(); 511 } 512 } 513 newEitItemMap.remove(item.getEventId()); 514 } 515 for (EitItem unverifiedOldItems : outdatedOldItems) { 516 if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) { 517 // The given new EIT item list covers partial time span of EPG. Here, we delete old 518 // item only when it has an overlapping with the new EIT item list. 519 long startTime = unverifiedOldItems.getStartTimeUtcMillis(); 520 long endTime = unverifiedOldItems.getEndTimeUtcMillis(); 521 for (EitItem item : newEitItemMap.values()) { 522 long newItemStartTime = item.getStartTimeUtcMillis(); 523 long newItemEndTime = item.getEndTimeUtcMillis(); 524 if ((startTime >= newItemStartTime && startTime < newItemEndTime) 525 || (endTime > newItemStartTime && endTime <= newItemEndTime)) { 526 ops.add( 527 ContentProviderOperation.newDelete( 528 TvContract.buildProgramUri( 529 unverifiedOldItems.getProgramId())) 530 .build()); 531 if (ops.size() >= BATCH_OPERATION_COUNT) { 532 applyBatch(channel.getName(), ops); 533 ops.clear(); 534 } 535 break; 536 } 537 } 538 } 539 } 540 for (EitItem item : newEitItemMap.values()) { 541 if (item.getEndTimeUtcMillis() < currentTime) { 542 continue; 543 } 544 ops.add( 545 buildContentProviderOperation( 546 ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI), 547 item, 548 channel)); 549 if (ops.size() >= BATCH_OPERATION_COUNT) { 550 applyBatch(channel.getName(), ops); 551 ops.clear(); 552 } 553 } 554 555 applyBatch(channel.getName(), ops); 556 } 557 558 private ContentProviderOperation buildContentProviderOperation( 559 ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { 560 if (channel != null) { 561 builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); 562 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 563 builder.withValue( 564 TvContract.Programs.COLUMN_RECORDING_PROHIBITED, 565 channel.isRecordingProhibited() ? 1 : 0); 566 } 567 } 568 if (item != null) { 569 builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) 570 .withValue( 571 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 572 item.getStartTimeUtcMillis()) 573 .withValue( 574 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 575 item.getEndTimeUtcMillis()) 576 .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, item.getContentRating()) 577 .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, item.getAudioLanguage()) 578 .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, item.getDescription()) 579 .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, item.getEventId()); 580 } 581 return builder.build(); 582 } 583 584 private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) { 585 try { 586 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations); 587 } catch (RemoteException | OperationApplicationException e) { 588 Log.e(TAG, "Error updating EPG " + channelName, e); 589 } 590 } 591 592 private void handleChannel(TunerChannel channel) { 593 long channelId = getChannelId(channel); 594 ContentValues values = new ContentValues(); 595 values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName()); 596 values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName()); 597 values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid()); 598 values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber()); 599 values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); 600 values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); 601 values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); 602 values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat()); 603 values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION); 604 values.put( 605 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, 606 channel.isRecordingProhibited() ? 1 : 0); 607 608 if (channelId <= 0) { 609 values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); 610 values.put( 611 TvContract.Channels.COLUMN_TYPE, 612 "QAM256".equals(channel.getModulation()) 613 ? TvContract.Channels.TYPE_ATSC_C 614 : TvContract.Channels.TYPE_ATSC_T); 615 values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber()); 616 617 // ATSC doesn't have original_network_id 618 values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency()); 619 620 Uri channelUri = 621 mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values); 622 channelId = ContentUris.parseId(channelUri); 623 } else { 624 mContext.getContentResolver() 625 .update(TvContract.buildChannelUri(channelId), values, null, null); 626 } 627 channel.setChannelId(channelId); 628 mTunerChannelMap.put(channelId, channel); 629 mTunerChannelIdMap.put(channel, channelId); 630 if (mIsScanning.get()) { 631 mScannedChannels.add(channel); 632 mPreviousScannedChannels.remove(channel); 633 } 634 if (mListener != null) { 635 mListener.onChannelArrived(channel); 636 } 637 } 638 639 private void clearChannels() { 640 int count = mContext.getContentResolver().delete(mChannelsUri, null, null); 641 if (count > 0) { 642 // We have just deleted obsolete data. Now tell the user that he or she needs 643 // to perform the auto-scan again. 644 if (mListener != null) { 645 mListener.onRescanNeeded(); 646 } 647 } 648 } 649 650 private void checkVersion() { 651 if (PermissionUtils.hasAccessAllEpg(mContext)) { 652 String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; 653 try (Cursor cursor = 654 mContext.getContentResolver() 655 .query( 656 mChannelsUri, 657 CHANNEL_DATA_SELECTION_ARGS, 658 selection, 659 new String[] {Integer.toString(VERSION)}, 660 null)) { 661 if (cursor != null && cursor.moveToFirst()) { 662 // The stored channel data seem outdated. Delete them all. 663 clearChannels(); 664 } 665 } 666 } else { 667 try (Cursor cursor = 668 mContext.getContentResolver() 669 .query( 670 mChannelsUri, 671 new String[] { 672 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 673 }, 674 null, 675 null, 676 null)) { 677 if (cursor != null) { 678 while (cursor.moveToNext()) { 679 int version = cursor.getInt(0); 680 if (version != VERSION) { 681 clearChannels(); 682 break; 683 } 684 } 685 } 686 } 687 } 688 } 689 690 private long getChannelId(TunerChannel channel) { 691 Long channelId = mTunerChannelIdMap.get(channel); 692 if (channelId != null) { 693 return channelId; 694 } 695 mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); 696 try (Cursor cursor = 697 mContext.getContentResolver() 698 .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 699 if (cursor != null && cursor.moveToFirst()) { 700 do { 701 TunerChannel tunerChannel = TunerChannel.fromCursor(cursor); 702 if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) { 703 mTunerChannelIdMap.put(channel, tunerChannel.getChannelId()); 704 mTunerChannelMap.put(tunerChannel.getChannelId(), channel); 705 return tunerChannel.getChannelId(); 706 } 707 } while (cursor.moveToNext()); 708 } 709 } 710 return -1; 711 } 712 713 private List<EitItem> getAllProgramsForChannel(TunerChannel channel) { 714 return getAllProgramsForChannel(channel, null, null); 715 } 716 717 private List<EitItem> getAllProgramsForChannel( 718 TunerChannel channel, @Nullable Long startTimeMs, @Nullable Long endTimeMs) { 719 List<EitItem> items = new ArrayList<>(); 720 long channelId = channel.getChannelId(); 721 Uri programsUri = 722 (startTimeMs == null || endTimeMs == null) 723 ? TvContract.buildProgramsUriForChannel(channelId) 724 : TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs); 725 try (Cursor cursor = 726 mContext.getContentResolver() 727 .query(programsUri, ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) { 728 if (cursor != null && cursor.moveToFirst()) { 729 do { 730 long id = cursor.getLong(0); 731 String titleText = cursor.getString(1); 732 long startTime = 733 ConvertUtils.convertUnixEpochToGPSTime( 734 cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS); 735 long endTime = 736 ConvertUtils.convertUnixEpochToGPSTime( 737 cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS); 738 int lengthInSecond = (int) (endTime - startTime); 739 String contentRating = cursor.getString(4); 740 String broadcastGenre = cursor.getString(5); 741 String canonicalGenre = cursor.getString(6); 742 String description = cursor.getString(7); 743 int eventId = cursor.getInt(8); 744 EitItem eitItem = 745 new EitItem( 746 id, 747 eventId, 748 titleText, 749 startTime, 750 lengthInSecond, 751 contentRating, 752 null, 753 null, 754 broadcastGenre, 755 canonicalGenre, 756 description); 757 items.add(eitItem); 758 } while (cursor.moveToNext()); 759 } 760 } 761 return items; 762 } 763 764 private void buildChannelMap() { 765 ArrayList<TunerChannel> channels = new ArrayList<>(); 766 try (Cursor cursor = 767 mContext.getContentResolver() 768 .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 769 if (cursor != null && cursor.moveToFirst()) { 770 do { 771 TunerChannel channel = TunerChannel.fromCursor(cursor); 772 if (channel != null) { 773 channels.add(channel); 774 } 775 } while (cursor.moveToNext()); 776 } 777 } 778 mTunerChannelMap.clear(); 779 mTunerChannelIdMap.clear(); 780 for (TunerChannel channel : channels) { 781 mTunerChannelMap.put(channel.getChannelId(), channel); 782 mTunerChannelIdMap.put(channel, channel.getChannelId()); 783 } 784 } 785 786 private static class ChannelEvent { 787 public final TunerChannel channel; 788 public final List<EitItem> eitItems; 789 790 public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) { 791 this.channel = channel; 792 this.eitItems = eitItems; 793 } 794 } 795 } 796