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