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