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.usbtuner.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.text.format.DateUtils; 33 import android.util.Log; 34 35 import com.android.usbtuner.UsbTunerPreferences; 36 import com.android.usbtuner.data.PsipData.EitItem; 37 import com.android.usbtuner.data.TunerChannel; 38 import com.android.usbtuner.util.ConvertUtils; 39 import com.android.usbtuner.util.TisConfiguration; 40 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.concurrent.ConcurrentHashMap; 47 import java.util.concurrent.ConcurrentSkipListMap; 48 import java.util.concurrent.ConcurrentSkipListSet; 49 import java.util.concurrent.CountDownLatch; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 52 /** 53 * Manages the channel info and EPG data through {@link TvInputManager}. 54 */ 55 public class ChannelDataManager implements Handler.Callback { 56 private static final String TAG = "ChannelDataManager"; 57 58 private static final String[] ALL_PROGRAMS_SELECTION_ARGS = 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 private static final String[] CHANNEL_DATA_SELECTION_ARGS = new String[] { 69 TvContract.Channels._ID, 70 TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA }; 71 72 private static final int MSG_HANDLE_EVENTS = 1; 73 private static final int MSG_HANDLE_CHANNEL = 2; 74 private static final int MSG_BUILD_CHANNEL_MAP = 3; 75 private static final int MSG_REQUEST_PROGRAMS = 4; 76 private static final int MSG_CLEAR_CHANNELS = 6; 77 private static final int MSG_SCAN_COMPLETED = 7; 78 79 /** 80 * A version number to enforce consistency of the channel data. 81 * 82 * WARNING: If a change in the database serialization lead to breaking the backward 83 * compatibility, you must increment this value so that the old data are purged, 84 * and the user is requested to perform the auto-scan again to generate the new data set. 85 */ 86 private static final int VERSION = 6; 87 88 private final Context mContext; 89 private String mInputId; 90 private ProgramInfoListener mListener; 91 private HandlerThread mHandlerThread; 92 private Handler mHandler; 93 private ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap; 94 private ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap; 95 private final Uri mChannelsUri; 96 97 // Used for scanning 98 private final ConcurrentSkipListSet<TunerChannel> mScannedChannels; 99 private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels; 100 private AtomicBoolean mIsScanning; 101 private CountDownLatch mScanLatch; 102 103 public interface ProgramInfoListener { 104 105 /** 106 * Invoked when a request for getting programs of a channel has been processed and passes 107 * the requested channel and the programs retrieved from database to the listener. 108 */ onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs)109 void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs); 110 111 /** 112 * Invoked when programs of a channel have been arrived and passes the arrived channel and 113 * programs to the listener. 114 */ onProgramsArrived(TunerChannel channel, List<EitItem> programs)115 void onProgramsArrived(TunerChannel channel, List<EitItem> programs); 116 117 /** 118 * Invoked when a channel has been arrived and passes the arrived channel to the listener. 119 */ onChannelArrived(TunerChannel channel)120 void onChannelArrived(TunerChannel channel); 121 122 /** 123 * Invoked when the database schema has been changed and the old-format channels have been 124 * deleted. A receiver should notify to a user that re-scanning channels is necessary. 125 */ onRescanNeeded()126 void onRescanNeeded(); 127 } 128 ChannelDataManager(Context context)129 public ChannelDataManager(Context context) { 130 mContext = context; 131 if (TisConfiguration.isInternalTunerTvInput(context)) { 132 mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(), 133 InternalTunerTvInputService.class.getName())) + "/HW" + 134 TisConfiguration.getTunerHwDeviceId(context); 135 } else { 136 mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(), 137 UsbTunerTvInputService.class.getName())); 138 } 139 mChannelsUri = TvContract.buildChannelsUriForInput(mInputId); 140 mTunerChannelMap = new ConcurrentHashMap<>(); 141 mTunerChannelIdMap = new ConcurrentSkipListMap<>(); 142 mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread"); 143 mHandlerThread.start(); 144 mHandler = new Handler(mHandlerThread.getLooper(), this); 145 mIsScanning = new AtomicBoolean(); 146 mScannedChannels = new ConcurrentSkipListSet<>(); 147 mPreviousScannedChannels = new ConcurrentSkipListSet<>(); 148 } 149 150 // Public methods checkDataVersion(Context context)151 public void checkDataVersion(Context context) { 152 int version = UsbTunerPreferences.getChannelDataVersion(context); 153 Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")"); 154 if (version == VERSION) { 155 // Everything is awesome. Return and continue. 156 return; 157 } 158 setCurrentVersion(context); 159 160 // The stored channel data seem outdated. Delete them all. 161 mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS); 162 } 163 setCurrentVersion(Context context)164 public void setCurrentVersion(Context context) { 165 UsbTunerPreferences.setChannelDataVersion(context, VERSION); 166 } 167 setListener(ProgramInfoListener listener)168 public void setListener(ProgramInfoListener listener) { 169 mListener = listener; 170 } 171 release()172 public void release() { 173 mHandler.removeCallbacksAndMessages(null); 174 mHandlerThread.quitSafely(); 175 } 176 getChannel(long channelId)177 public TunerChannel getChannel(long channelId) { 178 TunerChannel channel = mTunerChannelMap.get(channelId); 179 if (channel != null) { 180 return channel; 181 } 182 mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); 183 byte[] data = null; 184 try (Cursor cursor = mContext.getContentResolver().query(TvContract.buildChannelUri( 185 channelId), CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 186 if (cursor != null && cursor.moveToFirst()) { 187 data = cursor.getBlob(1); 188 } 189 } 190 if (data == null) { 191 return null; 192 } 193 channel = TunerChannel.parseFrom(data); 194 if (channel == null) { 195 return null; 196 } 197 channel.setChannelId(channelId); 198 return channel; 199 } 200 requestProgramsData(TunerChannel channel)201 public void requestProgramsData(TunerChannel channel) { 202 mHandler.removeMessages(MSG_REQUEST_PROGRAMS); 203 mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget(); 204 } 205 notifyEventDetected(TunerChannel channel, List<EitItem> items)206 public void notifyEventDetected(TunerChannel channel, List<EitItem> items) { 207 mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget(); 208 } 209 notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)210 public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 211 mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget(); 212 } 213 214 // For scanning process 215 /** 216 * Invoked when starting a scanning mode. This method gets the previous channels to detect the 217 * obsolete channels after scanning and initializes the variables used for scanning. 218 */ notifyScanStarted()219 public void notifyScanStarted() { 220 mScannedChannels.clear(); 221 mPreviousScannedChannels.clear(); 222 try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, 223 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 224 if (cursor != null && cursor.moveToFirst()) { 225 do { 226 long channelId = cursor.getLong(0); 227 byte[] data = cursor.getBlob(1); 228 TunerChannel channel = TunerChannel.parseFrom(data); 229 if (channel != null) { 230 channel.setChannelId(channelId); 231 mPreviousScannedChannels.add(channel); 232 } 233 } while (cursor.moveToNext()); 234 } 235 } 236 mIsScanning.set(true); 237 } 238 239 /** 240 * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler 241 * in order to wait for finish the remaining messages in the handler queue. Then removes the 242 * obsolete channels, which are previous scanned but are in the scanned result. 243 */ notifyScanCompleted()244 public void notifyScanCompleted() { 245 mScanLatch = new CountDownLatch(1); 246 mHandler.sendEmptyMessage(MSG_SCAN_COMPLETED); 247 try { 248 mScanLatch.await(); 249 } catch (InterruptedException e) { 250 Log.e(TAG, "Scanning process could not finish", e); 251 } 252 mIsScanning.set(false); 253 if (mPreviousScannedChannels.isEmpty()) { 254 return; 255 } 256 ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 257 for (TunerChannel channel : mPreviousScannedChannels) { 258 ops.add(ContentProviderOperation.newDelete( 259 TvContract.buildChannelUri(channel.getChannelId())).build()); 260 } 261 try { 262 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); 263 } catch (RemoteException | OperationApplicationException e) { 264 Log.e(TAG, "Error deleting obsolete channels", e); 265 } 266 } 267 268 /** 269 * Returns the number of scanned channels in the scanning mode. 270 */ getScannedChannelCount()271 public int getScannedChannelCount() { 272 return mScannedChannels.size(); 273 } 274 275 @Override handleMessage(Message msg)276 public boolean handleMessage(Message msg) { 277 switch (msg.what) { 278 case MSG_HANDLE_EVENTS: { 279 ChannelEvent event = (ChannelEvent) msg.obj; 280 handleEvents(event.channel, event.eitItems); 281 return true; 282 } 283 case MSG_HANDLE_CHANNEL: { 284 TunerChannel channel = (TunerChannel) msg.obj; 285 handleChannel(channel); 286 return true; 287 } 288 case MSG_BUILD_CHANNEL_MAP: { 289 mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP); 290 buildChannelMap(); 291 return true; 292 } 293 case MSG_REQUEST_PROGRAMS: { 294 if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) { 295 return true; 296 } 297 TunerChannel channel = (TunerChannel) msg.obj; 298 if (mListener != null) { 299 mListener.onRequestProgramsResponse(channel, getAllProgramsForChannel(channel)); 300 } 301 return true; 302 } 303 case MSG_CLEAR_CHANNELS: { 304 clearChannels(); 305 return true; 306 } 307 case MSG_SCAN_COMPLETED: { 308 mScanLatch.countDown(); 309 return true; 310 } 311 } 312 return false; 313 } 314 315 // Private methods handleEvents(TunerChannel channel, List<EitItem> items)316 private void handleEvents(TunerChannel channel, List<EitItem> items) { 317 long channelId = getChannelId(channel); 318 if (channelId <= 0) { 319 return; 320 } 321 channel.setChannelId(channelId); 322 long currentTime = System.currentTimeMillis(); 323 List<EitItem> oldItems = getAllProgramsForChannel(channel); 324 // TODO: Find a right to check if the programs are added outside. 325 for (EitItem item : oldItems) { 326 if (item.getEventId() == 0) { 327 // The event has been added outside TV tuner. Do not update programs. 328 return; 329 } 330 } 331 List<EitItem> outdatedOldItems = new ArrayList<>(); 332 List<EitItem> programsAddedToEPG = new ArrayList<>(); 333 ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 334 Map<Integer, EitItem> eitItemMap = new HashMap<>(); 335 for (EitItem item : items) { 336 eitItemMap.put(item.getEventId(), item); 337 } 338 for (EitItem oldItem : oldItems) { 339 EitItem item = eitItemMap.get(oldItem.getEventId()); 340 if (item == null) { 341 outdatedOldItems.add(oldItem); 342 continue; 343 } 344 items.remove(item); 345 programsAddedToEPG.add(item); 346 347 // Since program descriptions arrive at different time, the older one may have the 348 // correct program description while the newer one has no clue what value is. 349 if (oldItem.getDescription() != null && item.getDescription() == null 350 && oldItem.getEventId() == item.getEventId() 351 && oldItem.getStartTime() == item.getStartTime() 352 && oldItem.getLengthInSecond() == item.getLengthInSecond() 353 && Objects.equals(oldItem.getContentRating(), item.getContentRating()) 354 && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre()) 355 && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) { 356 item.setDescription(oldItem.getDescription()); 357 } 358 if (item.compareTo(oldItem) != 0) { 359 ops.add(ContentProviderOperation.newUpdate( 360 TvContract.buildProgramUri(oldItem.getProgramId())) 361 .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) 362 .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 363 item.getStartTimeUtcMillis()) 364 .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 365 item.getEndTimeUtcMillis()) 366 .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, 367 item.getContentRating()) 368 .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, 369 item.getAudioLanguage()) 370 .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 371 item.getDescription()) 372 .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, 373 item.getEventId()) 374 .build()); 375 } 376 } 377 for (EitItem outdatedOldItem : outdatedOldItems) { 378 if (outdatedOldItem.getStartTimeUtcMillis() > currentTime) { 379 ops.add(ContentProviderOperation.newDelete( 380 TvContract.buildProgramUri(outdatedOldItem.getProgramId())).build()); 381 } 382 } 383 for (EitItem item : items) { 384 ops.add(ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI) 385 .withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()) 386 .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) 387 .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 388 item.getStartTimeUtcMillis()) 389 .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 390 item.getEndTimeUtcMillis()) 391 .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, 392 item.getContentRating()) 393 .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, 394 item.getAudioLanguage()) 395 .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 396 item.getDescription()) 397 .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, 398 item.getEventId()) 399 .build()); 400 programsAddedToEPG.add(item); 401 } 402 403 try { 404 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); 405 } catch (RemoteException | OperationApplicationException e) { 406 Log.e(TAG, "Error updating EPG " + channel.getName(), e); 407 } 408 409 // Schedule the audio and caption tracks of the current program and the programs being 410 // listed after the current one into TIS. 411 if (mListener != null) { 412 mListener.onProgramsArrived(channel, programsAddedToEPG); 413 } 414 } 415 handleChannel(TunerChannel channel)416 private void handleChannel(TunerChannel channel) { 417 long channelId = getChannelId(channel); 418 ContentValues values = new ContentValues(); 419 values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName()); 420 values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName()); 421 values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid()); 422 values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber()); 423 values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); 424 values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); 425 values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); 426 if (channelId <= 0) { 427 values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); 428 values.put(TvContract.Channels.COLUMN_TYPE, "QAM256".equals(channel.getModulation()) 429 ? TvContract.Channels.TYPE_ATSC_C : TvContract.Channels.TYPE_ATSC_T); 430 values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber()); 431 432 // ATSC doesn't have original_network_id 433 values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency()); 434 435 Uri channelUri = mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, 436 values); 437 channelId = ContentUris.parseId(channelUri); 438 } else { 439 mContext.getContentResolver().update( 440 TvContract.buildChannelUri(channelId), values, null, null); 441 } 442 channel.setChannelId(channelId); 443 mTunerChannelMap.put(channelId, channel); 444 mTunerChannelIdMap.put(channel, channelId); 445 if (mIsScanning.get()) { 446 mScannedChannels.add(channel); 447 mPreviousScannedChannels.remove(channel); 448 } 449 if (mListener != null) { 450 mListener.onChannelArrived(channel); 451 } 452 } 453 clearChannels()454 private void clearChannels() { 455 int count = mContext.getContentResolver().delete(mChannelsUri, null, null); 456 if (count > 0) { 457 // We have just deleted obsolete data. Now tell the user that he or she needs 458 // to perform the auto-scan again. 459 if (mListener != null) { 460 mListener.onRescanNeeded(); 461 } 462 } 463 } 464 getChannelId(TunerChannel channel)465 private long getChannelId(TunerChannel channel) { 466 Long channelId = mTunerChannelIdMap.get(channel); 467 if (channelId != null) { 468 return channelId; 469 } 470 mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); 471 try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, 472 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 473 if (cursor != null && cursor.moveToFirst()) { 474 do { 475 channelId = cursor.getLong(0); 476 byte[] providerData = cursor.getBlob(1); 477 TunerChannel tunerChannel = TunerChannel.parseFrom(providerData); 478 if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) { 479 channel.setChannelId(channelId); 480 mTunerChannelIdMap.put(channel, channelId); 481 mTunerChannelMap.put(channelId, channel); 482 return channelId; 483 } 484 } while (cursor.moveToNext()); 485 } 486 } 487 return -1; 488 } 489 getAllProgramsForChannel(TunerChannel channel)490 private List<EitItem> getAllProgramsForChannel(TunerChannel channel) { 491 List<EitItem> items = new ArrayList<>(); 492 try (Cursor cursor = mContext.getContentResolver().query( 493 TvContract.buildProgramsUriForChannel(channel.getChannelId()), 494 ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) { 495 if (cursor != null && cursor.moveToFirst()) { 496 do { 497 long id = cursor.getLong(0); 498 String titleText = cursor.getString(1); 499 long startTime = ConvertUtils.convertUnixEpochToGPSTime( 500 cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS); 501 long endTime = ConvertUtils.convertUnixEpochToGPSTime( 502 cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS); 503 int lengthInSecond = (int) (endTime - startTime); 504 String contentRating = cursor.getString(4); 505 String broadcastGenre = cursor.getString(5); 506 String canonicalGenre = cursor.getString(6); 507 String description = cursor.getString(7); 508 int eventId = cursor.getInt(8); 509 EitItem eitItem = new EitItem(id, eventId, titleText, startTime, lengthInSecond, 510 contentRating, null, null, broadcastGenre, canonicalGenre, description); 511 items.add(eitItem); 512 } while (cursor.moveToNext()); 513 } 514 } 515 return items; 516 } 517 buildChannelMap()518 private void buildChannelMap() { 519 ArrayList<TunerChannel> channels = new ArrayList<>(); 520 try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, 521 CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { 522 if (cursor != null && cursor.moveToFirst()) { 523 do { 524 long channelId = cursor.getLong(0); 525 byte[] data = cursor.getBlob(1); 526 TunerChannel channel = TunerChannel.parseFrom(data); 527 if (channel != null) { 528 channel.setChannelId(channelId); 529 channels.add(channel); 530 } 531 } while (cursor.moveToNext()); 532 } 533 } 534 mTunerChannelMap.clear(); 535 mTunerChannelIdMap.clear(); 536 for (TunerChannel channel : channels) { 537 mTunerChannelMap.put(channel.getChannelId(), channel); 538 mTunerChannelIdMap.put(channel, channel.getChannelId()); 539 } 540 } 541 542 private static class ChannelEvent { 543 public final TunerChannel channel; 544 public final List<EitItem> eitItems; 545 ChannelEvent(TunerChannel channel, List<EitItem> eitItems)546 public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) { 547 this.channel = channel; 548 this.eitItems = eitItems; 549 } 550 } 551 } 552