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