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.datamanager; 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.NonNull; 33 import android.support.annotation.Nullable; 34 import android.text.format.DateUtils; 35 import android.util.Log; 36 import com.android.tv.common.util.PermissionUtils; 37 import com.android.tv.tuner.data.PsipData.EitItem; 38 import com.android.tv.tuner.data.TunerChannel; 39 import com.android.tv.tuner.prefs.TunerPreferences; 40 import com.android.tv.tuner.util.ConvertUtils; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.concurrent.ConcurrentHashMap; 48 import java.util.concurrent.ConcurrentSkipListMap; 49 import java.util.concurrent.ConcurrentSkipListSet; 50 import java.util.concurrent.TimeUnit; 51 import java.util.concurrent.atomic.AtomicBoolean; 52 53 /** Manages the channel info and EPG data for a specific inputId. */ 54 public class ChannelDataManager implements Handler.Callback { 55 private static final String TAG = "ChannelDataManager"; 56 57 private static final String[] ALL_PROGRAMS_SELECTION_ARGS = 58 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 }; 69 private static final String[] CHANNEL_DATA_SELECTION_ARGS = 70 new String[] { 71 TvContract.Channels._ID, 72 TvContract.Channels.COLUMN_LOCKED, 73 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, 74 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 75 }; 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 * <p>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, and the user is 95 * 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 ChannelHandlingDoneListener mChannelHandlingDoneListener; 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 */ onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs)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 */ onProgramsArrived(TunerChannel channel, List<EitItem> programs)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 */ onChannelArrived(TunerChannel channel)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 */ onRescanNeeded()139 void onRescanNeeded(); 140 } 141 142 /** Listens for all channel handling to be done. */ 143 public interface ChannelHandlingDoneListener { 144 /** Invoked when all pending channels have been handled. */ onChannelHandlingDone()145 void onChannelHandlingDone(); 146 } 147 ChannelDataManager(Context context, String inputId)148 public ChannelDataManager(Context context, String inputId) { 149 mContext = context; 150 mInputId = inputId; 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(ChannelHandlingDoneListener listener, Handler handler)188 public void setChannelScanListener(ChannelHandlingDoneListener listener, Handler handler) { 189 mChannelHandlingDoneListener = listener; 190 mChannelScanHandler = handler; 191 } 192 release()193 public void release() { 194 mHandler.removeCallbacksAndMessages(null); 195 releaseSafely(); 196 } 197 releaseSafely()198 public void releaseSafely() { 199 mHandlerThread.quitSafely(); 200 mListener = null; 201 mChannelHandlingDoneListener = null; 202 mChannelScanHandler = null; 203 } 204 getChannel(long channelId)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 requestProgramsData(TunerChannel channel)238 public void requestProgramsData(TunerChannel channel) { 239 mHandler.removeMessages(MSG_REQUEST_PROGRAMS); 240 mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget(); 241 } 242 notifyEventDetected(TunerChannel channel, List<EitItem> items)243 public void notifyEventDetected(TunerChannel channel, List<EitItem> items) { 244 mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget(); 245 } 246 notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)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 */ notifyScanStarted()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 */ notifyScanCompleted()285 public void notifyScanCompleted() { 286 // Send an empty 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 scannedChannelHandlingCompleted()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 (mChannelHandlingDoneListener != null && mChannelScanHandler != null) { 309 mChannelScanHandler.post(() -> mChannelHandlingDoneListener.onChannelHandlingDone()); 310 } else { 311 Log.e(TAG, "Error. mChannelHandlingDoneListener is null."); 312 } 313 } 314 315 /** Returns the number of scanned channels in the scanning mode. */ getScannedChannelCount()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 */ removeAllCallbacksAndMessages()323 public void removeAllCallbacksAndMessages() { 324 mHandler.removeCallbacksAndMessages(null); 325 } 326 327 @Override handleMessage(Message msg)328 public boolean handleMessage(Message msg) { 329 switch (msg.what) { 330 case MSG_HANDLE_EVENTS: 331 { 332 ChannelEvent event = (ChannelEvent) msg.obj; 333 handleEvents(event.channel, event.eitItems); 334 return true; 335 } 336 case MSG_HANDLE_CHANNEL: 337 { 338 TunerChannel channel = (TunerChannel) msg.obj; 339 if (channel != null) { 340 handleChannel(channel); 341 } 342 if (scanCompleted.get() 343 && mIsScanning.get() 344 && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) { 345 // Complete the scan when all found channels have already been handled. 346 scannedChannelHandlingCompleted(); 347 } 348 return true; 349 } 350 case MSG_BUILD_CHANNEL_MAP: 351 { 352 mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP); 353 buildChannelMap(); 354 return true; 355 } 356 case MSG_REQUEST_PROGRAMS: 357 { 358 if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) { 359 return true; 360 } 361 TunerChannel channel = (TunerChannel) msg.obj; 362 if (mListener != null) { 363 mListener.onRequestProgramsResponse( 364 channel, getAllProgramsForChannel(channel)); 365 } 366 return true; 367 } 368 case MSG_CLEAR_CHANNELS: 369 { 370 clearChannels(); 371 return true; 372 } 373 case MSG_CHECK_VERSION: 374 { 375 checkVersion(); 376 return true; 377 } 378 default: // fall out 379 Log.w(TAG, "unexpected case in handleMessage ( " + msg.what + " )"); 380 } 381 return false; 382 } 383 384 @NonNull 385 @Override toString()386 public String toString() { 387 return "ChannelDataManager[" + mInputId + "]"; 388 } 389 390 // Private methods handleEvents(TunerChannel channel, List<EitItem> items)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 (EitItem lhs, EitItem rhs) -> 445 Long.compare( 446 lhs.getStartTimeUtcMillis(), 447 rhs.getStartTimeUtcMillis())); 448 if (pos >= 0) { 449 // Same start Time found. Overlapped. 450 continue; 451 } 452 int insertPoint = -1 - pos; 453 // Check the two adjacent items. 454 if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis() 455 || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) { 456 continue; 457 } 458 } 459 ops.add( 460 buildContentProviderOperation( 461 ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI), 462 newItem, 463 channel)); 464 if (ops.size() >= BATCH_OPERATION_COUNT) { 465 applyBatch(channel.getName(), ops); 466 ops.clear(); 467 } 468 } 469 applyBatch(channel.getName(), ops); 470 return; 471 } 472 473 List<EitItem> outdatedOldItems = new ArrayList<>(); 474 Map<Integer, EitItem> newEitItemMap = new HashMap<>(); 475 for (EitItem item : items) { 476 newEitItemMap.put(item.getEventId(), item); 477 } 478 for (EitItem oldItem : oldItems) { 479 EitItem item = newEitItemMap.get(oldItem.getEventId()); 480 if (item == null) { 481 outdatedOldItems.add(oldItem); 482 continue; 483 } 484 485 // Since program descriptions arrive at different time, the older one may have the 486 // correct program description while the newer one has no clue what value is. 487 if (oldItem.getDescription() != null 488 && item.getDescription() == null 489 && oldItem.getEventId() == item.getEventId() 490 && oldItem.getStartTime() == item.getStartTime() 491 && oldItem.getLengthInSecond() == item.getLengthInSecond() 492 && Objects.equals(oldItem.getContentRating(), item.getContentRating()) 493 && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre()) 494 && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) { 495 item.setDescription(oldItem.getDescription()); 496 } 497 if (item.compareTo(oldItem) != 0) { 498 ops.add( 499 buildContentProviderOperation( 500 ContentProviderOperation.newUpdate( 501 TvContract.buildProgramUri(oldItem.getProgramId())), 502 item, 503 null)); 504 if (ops.size() >= BATCH_OPERATION_COUNT) { 505 applyBatch(channel.getName(), ops); 506 ops.clear(); 507 } 508 } 509 newEitItemMap.remove(item.getEventId()); 510 } 511 for (EitItem unverifiedOldItems : outdatedOldItems) { 512 if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) { 513 // The given new EIT item list covers partial time span of EPG. Here, we delete old 514 // item only when it has an overlapping with the new EIT item list. 515 long startTime = unverifiedOldItems.getStartTimeUtcMillis(); 516 long endTime = unverifiedOldItems.getEndTimeUtcMillis(); 517 for (EitItem item : newEitItemMap.values()) { 518 long newItemStartTime = item.getStartTimeUtcMillis(); 519 long newItemEndTime = item.getEndTimeUtcMillis(); 520 if ((startTime >= newItemStartTime && startTime < newItemEndTime) 521 || (endTime > newItemStartTime && endTime <= newItemEndTime)) { 522 ops.add( 523 ContentProviderOperation.newDelete( 524 TvContract.buildProgramUri( 525 unverifiedOldItems.getProgramId())) 526 .build()); 527 if (ops.size() >= BATCH_OPERATION_COUNT) { 528 applyBatch(channel.getName(), ops); 529 ops.clear(); 530 } 531 break; 532 } 533 } 534 } 535 } 536 for (EitItem item : newEitItemMap.values()) { 537 if (item.getEndTimeUtcMillis() < currentTime) { 538 continue; 539 } 540 ops.add( 541 buildContentProviderOperation( 542 ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI), 543 item, 544 channel)); 545 if (ops.size() >= BATCH_OPERATION_COUNT) { 546 applyBatch(channel.getName(), ops); 547 ops.clear(); 548 } 549 } 550 551 applyBatch(channel.getName(), ops); 552 } 553 buildContentProviderOperation( ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel)554 private ContentProviderOperation buildContentProviderOperation( 555 ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { 556 if (channel != null) { 557 builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); 558 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 559 builder.withValue( 560 TvContract.Programs.COLUMN_RECORDING_PROHIBITED, 561 channel.isRecordingProhibited() ? 1 : 0); 562 } 563 } 564 if (item != null) { 565 builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) 566 .withValue( 567 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 568 item.getStartTimeUtcMillis()) 569 .withValue( 570 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 571 item.getEndTimeUtcMillis()) 572 .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, item.getContentRating()) 573 .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, item.getAudioLanguage()) 574 .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, item.getDescription()) 575 .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, item.getEventId()); 576 } 577 return builder.build(); 578 } 579 applyBatch(String channelName, ArrayList<ContentProviderOperation> operations)580 private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) { 581 try { 582 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations); 583 } catch (RemoteException | OperationApplicationException e) { 584 Log.e(TAG, "Error updating EPG " + channelName, e); 585 } 586 } 587 handleChannel(TunerChannel channel)588 private void handleChannel(TunerChannel channel) { 589 long channelId = getChannelId(channel); 590 ContentValues values = new ContentValues(); 591 values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName()); 592 values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName()); 593 values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid()); 594 values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber()); 595 values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); 596 values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); 597 values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); 598 values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat()); 599 values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION); 600 values.put( 601 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, 602 channel.isRecordingProhibited() ? 1 : 0); 603 604 if (channelId <= 0) { 605 values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); 606 values.put( 607 TvContract.Channels.COLUMN_TYPE, 608 "QAM256".equals(channel.getModulation()) 609 ? TvContract.Channels.TYPE_ATSC_C 610 : TvContract.Channels.TYPE_ATSC_T); 611 values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber()); 612 613 // ATSC doesn't have original_network_id 614 values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency()); 615 616 Uri channelUri = 617 mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values); 618 channelId = ContentUris.parseId(channelUri); 619 } else { 620 mContext.getContentResolver() 621 .update(TvContract.buildChannelUri(channelId), values, null, null); 622 } 623 channel.setChannelId(channelId); 624 mTunerChannelMap.put(channelId, channel); 625 mTunerChannelIdMap.put(channel, channelId); 626 if (mIsScanning.get()) { 627 mScannedChannels.add(channel); 628 mPreviousScannedChannels.remove(channel); 629 } 630 if (mListener != null) { 631 mListener.onChannelArrived(channel); 632 } 633 } 634 clearChannels()635 private void clearChannels() { 636 int count = mContext.getContentResolver().delete(mChannelsUri, null, null); 637 if (count > 0) { 638 // We have just deleted obsolete data. Now tell the user that they need 639 // to perform the auto-scan again. 640 if (mListener != null) { 641 mListener.onRescanNeeded(); 642 } 643 } 644 } 645 checkVersion()646 private void checkVersion() { 647 if (PermissionUtils.hasAccessAllEpg(mContext)) { 648 String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; 649 try (Cursor cursor = 650 mContext.getContentResolver() 651 .query( 652 mChannelsUri, 653 CHANNEL_DATA_SELECTION_ARGS, 654 selection, 655 new String[] {Integer.toString(VERSION)}, 656 null)) { 657 if (cursor != null && cursor.moveToFirst()) { 658 // The stored channel data seem outdated. Delete them all. 659 clearChannels(); 660 } 661 } 662 } else { 663 try (Cursor cursor = 664 mContext.getContentResolver() 665 .query( 666 mChannelsUri, 667 new String[] { 668 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 669 }, 670 null, 671 null, 672 null)) { 673 if (cursor != null) { 674 while (cursor.moveToNext()) { 675 int version = cursor.getInt(0); 676 if (version != VERSION) { 677 clearChannels(); 678 break; 679 } 680 } 681 } 682 } 683 } 684 } 685 getChannelId(TunerChannel channel)686 private long getChannelId(TunerChannel channel) { 687 Long channelId = mTunerChannelIdMap.get(channel); 688 if (channelId != null) { 689 return channelId; 690 } 691 mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); 692 try (Cursor cursor = 693 mContext.getContentResolver() 694 .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 695 if (cursor != null && cursor.moveToFirst()) { 696 do { 697 TunerChannel tunerChannel = TunerChannel.fromCursor(cursor); 698 if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) { 699 mTunerChannelIdMap.put(channel, tunerChannel.getChannelId()); 700 mTunerChannelMap.put(tunerChannel.getChannelId(), channel); 701 return tunerChannel.getChannelId(); 702 } 703 } while (cursor.moveToNext()); 704 } 705 } 706 return -1; 707 } 708 getAllProgramsForChannel(TunerChannel channel)709 private List<EitItem> getAllProgramsForChannel(TunerChannel channel) { 710 return getAllProgramsForChannel(channel, null, null); 711 } 712 getAllProgramsForChannel( TunerChannel channel, @Nullable Long startTimeMs, @Nullable Long endTimeMs)713 private List<EitItem> getAllProgramsForChannel( 714 TunerChannel channel, @Nullable Long startTimeMs, @Nullable Long endTimeMs) { 715 List<EitItem> items = new ArrayList<>(); 716 long channelId = channel.getChannelId(); 717 Uri programsUri = 718 (startTimeMs == null || endTimeMs == null) 719 ? TvContract.buildProgramsUriForChannel(channelId) 720 : TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs); 721 try (Cursor cursor = 722 mContext.getContentResolver() 723 .query(programsUri, ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) { 724 if (cursor != null && cursor.moveToFirst()) { 725 do { 726 long id = cursor.getLong(0); 727 String titleText = cursor.getString(1); 728 long startTime = 729 ConvertUtils.convertUnixEpochToGPSTime( 730 cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS); 731 long endTime = 732 ConvertUtils.convertUnixEpochToGPSTime( 733 cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS); 734 int lengthInSecond = (int) (endTime - startTime); 735 String contentRating = cursor.getString(4); 736 String broadcastGenre = cursor.getString(5); 737 String canonicalGenre = cursor.getString(6); 738 String description = cursor.getString(7); 739 int eventId = cursor.getInt(8); 740 EitItem eitItem = 741 new EitItem( 742 id, 743 eventId, 744 titleText, 745 startTime, 746 lengthInSecond, 747 contentRating, 748 null, 749 null, 750 broadcastGenre, 751 canonicalGenre, 752 description); 753 items.add(eitItem); 754 } while (cursor.moveToNext()); 755 } 756 } 757 return items; 758 } 759 buildChannelMap()760 private void buildChannelMap() { 761 ArrayList<TunerChannel> channels = new ArrayList<>(); 762 try (Cursor cursor = 763 mContext.getContentResolver() 764 .query(mChannelsUri, CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 765 if (cursor != null && cursor.moveToFirst()) { 766 do { 767 TunerChannel channel = TunerChannel.fromCursor(cursor); 768 if (channel != null) { 769 channels.add(channel); 770 } 771 } while (cursor.moveToNext()); 772 } 773 } 774 mTunerChannelMap.clear(); 775 mTunerChannelIdMap.clear(); 776 for (TunerChannel channel : channels) { 777 mTunerChannelMap.put(channel.getChannelId(), channel); 778 mTunerChannelIdMap.put(channel, channel.getChannelId()); 779 } 780 } 781 782 private static class ChannelEvent { 783 public final TunerChannel channel; 784 public final List<EitItem> eitItems; 785 ChannelEvent(TunerChannel channel, List<EitItem> eitItems)786 public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) { 787 this.channel = channel; 788 this.eitItems = eitItems; 789 } 790 } 791 } 792