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; 18 19 import android.app.AlarmManager; 20 import android.app.Notification; 21 import android.app.NotificationChannel; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.content.BroadcastReceiver; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.SharedPreferences; 29 import android.content.pm.PackageManager; 30 import android.content.pm.PackageManager.NameNotFoundException; 31 import android.graphics.Bitmap; 32 import android.graphics.BitmapFactory; 33 import android.hardware.usb.UsbDevice; 34 import android.hardware.usb.UsbManager; 35 import android.net.ConnectivityManager; 36 import android.net.NetworkInfo; 37 import android.net.Uri; 38 import android.os.AsyncTask; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.Message; 42 import android.os.SystemClock; 43 import android.preference.PreferenceManager; 44 import android.support.annotation.NonNull; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.widget.Toast; 48 import com.android.tv.R; 49 import com.android.tv.Starter; 50 import com.android.tv.TvApplication; 51 import com.android.tv.TvSingletons; 52 import com.android.tv.common.BuildConfig; 53 import com.android.tv.common.util.SystemPropertiesProxy; 54 55 56 import com.android.tv.tuner.setup.BaseTunerSetupActivity; 57 import com.android.tv.tuner.util.TunerInputInfoUtils; 58 import java.text.ParseException; 59 import java.text.SimpleDateFormat; 60 import java.util.Collections; 61 import java.util.HashMap; 62 import java.util.HashSet; 63 import java.util.Map; 64 import java.util.Set; 65 import java.util.concurrent.TimeUnit; 66 67 /** 68 * Controls the package visibility of {@link BaseTunerTvInputService}. 69 * 70 * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code 71 * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to 72 * update the connection status of the supported USB TV tuners. 73 */ 74 public class TunerInputController { 75 private static final boolean DEBUG = false; 76 private static final String TAG = "TunerInputController"; 77 private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; 78 private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; 79 private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; 80 private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s"; 81 82 /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */ 83 private static final String CHECKING_NETWORK_TUNER_STATUS = 84 "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS"; 85 86 private static final String EXTRA_CHECKING_DURATION = 87 "com.android.tv.action.extra.CHECKING_DURATION"; 88 private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP"; 89 90 private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); 91 private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); 92 private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification"; 93 94 // TODO: Load settings from XML file 95 private static final TunerDevice[] TUNER_DEVICES = { 96 new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q 97 new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q 98 // WinTV-dualHD (bulk) will be supported after 2017 April security patch. 99 new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) 100 new TunerDevice(0x2040, 0x0264, null), 101 }; 102 103 private static final int MSG_ENABLE_INPUT_SERVICE = 1000; 104 private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; 105 106 private final ComponentName usbTunerComponent; 107 private final ComponentName networkTunerComponent; 108 private final ComponentName builtInTunerComponent; 109 private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>(); 110 111 private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>(); 112 private final Map<ComponentName, String> mNotificationMessages = new HashMap<>(); 113 private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>(); 114 115 private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this); 116 117 public TunerInputController(ComponentName embeddedTuner) { 118 usbTunerComponent = embeddedTuner; 119 networkTunerComponent = usbTunerComponent; 120 builtInTunerComponent = usbTunerComponent; 121 for (TunerDevice device : TUNER_DEVICES) { 122 mTunerServiceMapping.put(device, usbTunerComponent); 123 } 124 } 125 126 /** Checks status of USB devices to see if there are available USB tuners connected. */ 127 public void onCheckingUsbTunerStatus(Context context, String action) { 128 onCheckingUsbTunerStatus(context, action, mHandler); 129 } 130 131 private void onCheckingUsbTunerStatus( 132 Context context, String action, @NonNull CheckDvbDeviceHandler handler) { 133 Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context); 134 handler.removeMessages(MSG_ENABLE_INPUT_SERVICE); 135 if (!connectedUsbTuners.isEmpty()) { 136 // Need to check if DVB driver is accessible. Since the driver creation 137 // could be happen after the USB event, delay the checking by 138 // DVB_DRIVER_CHECK_DELAY_MS. 139 handler.sendMessageDelayed( 140 handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), 141 DVB_DRIVER_CHECK_DELAY_MS); 142 } else { 143 handleTunerStatusChanged( 144 context, 145 false, 146 connectedUsbTuners, 147 TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) 148 ? TunerHal.TUNER_TYPE_USB 149 : null); 150 } 151 } 152 153 private void onNetworkTunerChanged(Context context, boolean enabled) { 154 SharedPreferences sharedPreferences = 155 PreferenceManager.getDefaultSharedPreferences(context); 156 if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED) 157 && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) 158 == enabled) { 159 // the status is not changed 160 return; 161 } 162 if (enabled) { 163 sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); 164 } else { 165 sharedPreferences 166 .edit() 167 .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) 168 .apply(); 169 } 170 // Network tuner detection is initiated by UI. So the app should not 171 // be killed. 172 handleTunerStatusChanged( 173 context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK); 174 } 175 176 /** 177 * See if any USB tuner hardware is attached in the system. 178 * 179 * @param context {@link Context} instance 180 * @return {@code true} if any tuner device we support is plugged in 181 */ 182 private Set<TunerDevice> getConnectedUsbTuners(Context context) { 183 UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 184 Map<String, UsbDevice> deviceList = manager.getDeviceList(); 185 String currentSecurityLevel = 186 SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); 187 188 Set<TunerDevice> devices = new HashSet<>(); 189 for (UsbDevice device : deviceList.values()) { 190 if (DEBUG) { 191 Log.d(TAG, "Device: " + device); 192 } 193 for (TunerDevice tuner : TUNER_DEVICES) { 194 if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) { 195 Log.i(TAG, "Tuner found"); 196 devices.add(tuner); 197 } 198 } 199 } 200 return devices; 201 } 202 203 private void handleTunerStatusChanged( 204 Context context, 205 boolean forceDontKillApp, 206 Set<TunerDevice> connectedUsbTuners, 207 Integer triggerType) { 208 Map<ComponentName, Integer> serviceToEnable = new HashMap<>(); 209 Set<ComponentName> serviceToDisable = new HashSet<>(); 210 serviceToDisable.add(builtInTunerComponent); 211 serviceToDisable.add(networkTunerComponent); 212 if (TunerFeatures.TUNER.isEnabled(context)) { 213 // TODO: support both built-in tuner and other tuners at the same time? 214 if (TunerHal.useBuiltInTuner(context)) { 215 enableTunerTvInputService( 216 context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent); 217 return; 218 } 219 SharedPreferences sharedPreferences = 220 PreferenceManager.getDefaultSharedPreferences(context); 221 if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { 222 serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK); 223 } 224 } 225 for (TunerDevice device : TUNER_DEVICES) { 226 if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) { 227 serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB); 228 } else { 229 serviceToDisable.add(mTunerServiceMapping.get(device)); 230 } 231 } 232 serviceToDisable.removeAll(serviceToEnable.keySet()); 233 for (ComponentName serviceComponent : serviceToEnable.keySet()) { 234 if (isTunerPackageInstalled(context, serviceComponent)) { 235 enableTunerTvInputService( 236 context, 237 true, 238 forceDontKillApp, 239 serviceToEnable.get(serviceComponent), 240 serviceComponent); 241 } else { 242 sendNotificationToInstallPackage(context, serviceComponent); 243 } 244 } 245 for (ComponentName serviceComponent : serviceToDisable) { 246 if (isTunerPackageInstalled(context, serviceComponent)) { 247 enableTunerTvInputService( 248 context, false, forceDontKillApp, triggerType, serviceComponent); 249 } else { 250 cancelNotificationToInstallPackage(context, serviceComponent); 251 } 252 } 253 } 254 255 /** 256 * Enable/disable the component {@link BaseTunerTvInputService}. 257 * 258 * @param context {@link Context} instance 259 * @param enabled {@code true} to enable the service; otherwise {@code false} 260 */ 261 private static void enableTunerTvInputService( 262 Context context, 263 boolean enabled, 264 boolean forceDontKillApp, 265 Integer tunerType, 266 ComponentName serviceComponent) { 267 if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); 268 PackageManager pm = context.getPackageManager(); 269 int newState = 270 enabled 271 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED 272 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; 273 if (newState != pm.getComponentEnabledSetting(serviceComponent)) { 274 int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0; 275 if (serviceComponent.getPackageName().equals(context.getPackageName())) { 276 // Don't kill APP when handling input count changing. Or the following 277 // setComponentEnabledSetting() call won't work. 278 ((TvApplication) context.getApplicationContext()) 279 .handleInputCountChanged(true, enabled, true); 280 // Bundled input. Don't kill app if LiveChannels app is active since we don't want 281 // to kill the running app. 282 if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) { 283 flags |= PackageManager.DONT_KILL_APP; 284 } 285 // Send/cancel the USB tuner TV input setup notification. 286 BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); 287 if (!enabled && tunerType != null) { 288 if (tunerType == TunerHal.TUNER_TYPE_USB) { 289 Toast.makeText( 290 context, 291 R.string.msg_usb_tuner_disconnected, 292 Toast.LENGTH_SHORT) 293 .show(); 294 } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { 295 Toast.makeText( 296 context, 297 R.string.msg_network_tuner_disconnected, 298 Toast.LENGTH_SHORT) 299 .show(); 300 } 301 } 302 } 303 // Enable/disable the USB tuner TV input. 304 pm.setComponentEnabledSetting(serviceComponent, newState, flags); 305 if (DEBUG) Log.d(TAG, "Status updated:" + enabled); 306 } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) { 307 // When # of tuners is changed or the tuner input service is switching from/to using 308 // network tuners or the device just boots. 309 TunerInputInfoUtils.updateTunerInputInfo(context); 310 } 311 } 312 313 /** 314 * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. 315 */ 316 public void executeNetworkTunerDiscoveryAsyncTask(final Context context) { 317 executeNetworkTunerDiscoveryAsyncTask(context, 0, 0); 318 } 319 320 /** 321 * Discovers a network tuner. 322 * 323 * @param context {@link Context} 324 * @param repeatedDurationMs The time length to wait to repeatedly check network status to start 325 * finding network tuner when the network connection is not available. {@code 0} to disable 326 * repeatedly checking. 327 * @param deviceIp The previous discovered device IP, 0 if none. 328 */ 329 private void executeNetworkTunerDiscoveryAsyncTask( 330 final Context context, final long repeatedDurationMs, final int deviceIp) { 331 if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) { 332 return; 333 } 334 final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); 335 networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS); 336 if (!isNetworkConnected(context) && repeatedDurationMs > 0) { 337 sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs); 338 } else { 339 new AsyncTask<Void, Void, Boolean>() { 340 @Override 341 protected Boolean doInBackground(Void... params) { 342 Boolean result = null; 343 // Implement and execute network tuner discovery AsyncTask here. 344 return result; 345 } 346 347 @Override 348 protected void onPostExecute(Boolean foundNetworkTuner) { 349 if (foundNetworkTuner == null) { 350 return; 351 } 352 sendCheckingAlarm( 353 context, 354 networkCheckingIntent, 355 foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs); 356 onNetworkTunerChanged(context, foundNetworkTuner); 357 } 358 }.execute(); 359 } 360 } 361 362 private static boolean isNetworkConnected(Context context) { 363 ConnectivityManager cm = 364 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 365 NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 366 return networkInfo != null && networkInfo.isConnected(); 367 } 368 369 private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) { 370 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 371 intent.putExtra(EXTRA_CHECKING_DURATION, delayMs); 372 PendingIntent alarmIntent = 373 PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 374 alarmManager.set( 375 AlarmManager.ELAPSED_REALTIME, 376 SystemClock.elapsedRealtime() + delayMs, 377 alarmIntent); 378 } 379 380 private static boolean isTunerPackageInstalled( 381 Context context, ComponentName serviceComponent) { 382 try { 383 context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0); 384 return true; 385 } catch (NameNotFoundException e) { 386 return false; 387 } 388 } 389 390 private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) { 391 if (!BuildConfig.ENG) { 392 return; 393 } 394 String applicationName = mTunerApplicationNames.get(serviceComponent); 395 if (applicationName == null) { 396 applicationName = context.getString(R.string.tuner_install_default_application_name); 397 } 398 String contentTitle = 399 context.getString( 400 R.string.tuner_install_notification_content_title, applicationName); 401 String contentText = mNotificationMessages.get(serviceComponent); 402 if (contentText == null) { 403 contentText = context.getString(R.string.tuner_install_notification_content_text); 404 } 405 Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent); 406 if (largeIcon == null) { 407 // TODO: Make a better default image. 408 largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store); 409 } 410 NotificationManager notificationManager = 411 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 412 if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) { 413 createNotificationChannel(context, notificationManager); 414 } 415 Intent intent = new Intent(Intent.ACTION_VIEW); 416 intent.setData( 417 Uri.parse( 418 String.format( 419 PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName()))); 420 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 421 Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); 422 builder.setAutoCancel(true) 423 .setSmallIcon(R.drawable.ic_launcher_s) 424 .setLargeIcon(largeIcon) 425 .setContentTitle(contentTitle) 426 .setContentText(contentText) 427 .setCategory(Notification.CATEGORY_RECOMMENDATION) 428 .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); 429 notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build()); 430 } 431 432 private static void cancelNotificationToInstallPackage( 433 Context context, ComponentName serviceComponent) { 434 NotificationManager notificationManager = 435 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 436 notificationManager.cancel(serviceComponent.getPackageName(), 0); 437 } 438 439 private static void createNotificationChannel( 440 Context context, NotificationManager notificationManager) { 441 notificationManager.createNotificationChannel( 442 new NotificationChannel( 443 NOTIFICATION_CHANNEL_ID, 444 context.getResources() 445 .getString(R.string.ut_setup_notification_channel_name), 446 NotificationManager.IMPORTANCE_HIGH)); 447 } 448 449 public static class IntentReceiver extends BroadcastReceiver { 450 451 @Override 452 public void onReceive(Context context, Intent intent) { 453 if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); 454 Starter.start(context); 455 TunerInputController tunerInputController = 456 TvSingletons.getSingletons(context).getTunerInputController(); 457 if (!TunerFeatures.TUNER.isEnabled(context)) { 458 tunerInputController.handleTunerStatusChanged( 459 context, false, Collections.emptySet(), null); 460 return; 461 } 462 switch (intent.getAction()) { 463 case Intent.ACTION_BOOT_COMPLETED: 464 tunerInputController.executeNetworkTunerDiscoveryAsyncTask( 465 context, INITIAL_CHECKING_DURATION_MS, 0); 466 // fall through 467 case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: 468 case UsbManager.ACTION_USB_DEVICE_ATTACHED: 469 case UsbManager.ACTION_USB_DEVICE_DETACHED: 470 tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction()); 471 break; 472 case CHECKING_NETWORK_TUNER_STATUS: 473 long repeatedDurationMs = 474 intent.getLongExtra( 475 EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS); 476 tunerInputController.executeNetworkTunerDiscoveryAsyncTask( 477 context, 478 Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS), 479 intent.getIntExtra(EXTRA_DEVICE_IP, 0)); 480 break; 481 default: // fall out 482 } 483 } 484 } 485 486 /** 487 * Simple data holder for a USB device. Used to represent a tuner model, and compare against 488 * {@link UsbDevice}. 489 */ 490 private static class TunerDevice { 491 private final int vendorId; 492 private final int productId; 493 494 // security patch level from which the specific tuner type is supported. 495 private final String minSecurityLevel; 496 497 private TunerDevice(int vendorId, int productId, String minSecurityLevel) { 498 this.vendorId = vendorId; 499 this.productId = productId; 500 this.minSecurityLevel = minSecurityLevel; 501 } 502 503 private boolean equalsTo(UsbDevice device) { 504 return device.getVendorId() == vendorId && device.getProductId() == productId; 505 } 506 507 private boolean isSupported(String currentSecurityLevel) { 508 if (minSecurityLevel == null) { 509 return true; 510 } 511 512 long supportSecurityLevelTimeStamp = 0; 513 long currentSecurityLevelTimestamp = 0; 514 try { 515 SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT); 516 supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime(); 517 currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime(); 518 } catch (ParseException e) { 519 } 520 return supportSecurityLevelTimeStamp != 0 521 && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp; 522 } 523 } 524 525 private static class CheckDvbDeviceHandler extends Handler { 526 527 private final TunerInputController mTunerInputController; 528 private DvbDeviceAccessor mDvbDeviceAccessor; 529 530 CheckDvbDeviceHandler(TunerInputController tunerInputController) { 531 super(Looper.getMainLooper()); 532 this.mTunerInputController = tunerInputController; 533 } 534 535 @Override 536 public void handleMessage(Message msg) { 537 switch (msg.what) { 538 case MSG_ENABLE_INPUT_SERVICE: 539 Context context = (Context) msg.obj; 540 if (mDvbDeviceAccessor == null) { 541 mDvbDeviceAccessor = new DvbDeviceAccessor(context); 542 } 543 boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); 544 mTunerInputController.handleTunerStatusChanged( 545 context, 546 false, 547 enabled 548 ? mTunerInputController.getConnectedUsbTuners(context) 549 : Collections.emptySet(), 550 TunerHal.TUNER_TYPE_USB); 551 break; 552 default: // fall out 553 } 554 } 555 } 556 } 557