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.PendingIntent; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.SharedPreferences; 26 import android.content.pm.PackageManager; 27 import android.hardware.usb.UsbDevice; 28 import android.hardware.usb.UsbManager; 29 import android.net.ConnectivityManager; 30 import android.net.NetworkInfo; 31 import android.os.AsyncTask; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.SystemClock; 36 import android.preference.PreferenceManager; 37 import android.support.annotation.NonNull; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.widget.Toast; 41 42 import com.android.tv.Features; 43 import com.android.tv.R; 44 import com.android.tv.TvApplication; 45 import com.android.tv.common.SoftPreconditions; 46 import com.android.tv.tuner.setup.TunerSetupActivity; 47 import com.android.tv.tuner.tvinput.TunerTvInputService; 48 import com.android.tv.tuner.util.SystemPropertiesProxy; 49 import com.android.tv.tuner.util.TunerInputInfoUtils; 50 51 import java.text.ParseException; 52 import java.text.SimpleDateFormat; 53 import java.util.Map; 54 import java.util.Set; 55 import java.util.concurrent.TimeUnit; 56 57 /** 58 * Controls the package visibility of {@link TunerTvInputService}. 59 * <p> 60 * Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, 61 * {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} 62 * to update the connection status of the supported USB TV tuners. 63 */ 64 public class TunerInputController { 65 private static final boolean DEBUG = true; 66 private static final String TAG = "TunerInputController"; 67 private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; 68 private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; 69 private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; 70 71 /** 72 * Action of {@link Intent} to check network connection repeatedly when it is necessary. 73 */ 74 private static final String CHECKING_NETWORK_CONNECTION = 75 "com.android.tv.action.CHECKING_NETWORK_CONNECTION"; 76 77 private static final String EXTRA_CHECKING_DURATION = 78 "com.android.tv.action.extra.CHECKING_DURATION"; 79 80 private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); 81 private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); 82 83 private static final TunerDevice[] TUNER_DEVICES = { 84 new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q 85 new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q 86 // WinTV-dualHD (bulk) will be supported after 2017 April security patch. 87 new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) 88 // STOPSHIP: Add WinTV-soloHD (Isoc) temporary for test. Remove this after test complete. 89 new TunerDevice(0x2040, 0x0264, null), 90 }; 91 92 private static final int MSG_ENABLE_INPUT_SERVICE = 1000; 93 private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; 94 95 /** 96 * Checks status of USB devices to see if there are available USB tuners connected. 97 */ 98 public static void onCheckingUsbTunerStatus(Context context, String action) { 99 onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler()); 100 } 101 102 private static void onCheckingUsbTunerStatus(Context context, String action, 103 @NonNull CheckDvbDeviceHandler handler) { 104 SharedPreferences sharedPreferences = 105 PreferenceManager.getDefaultSharedPreferences(context); 106 if (TunerHal.useBuiltInTuner(context)) { 107 enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN); 108 return; 109 } 110 // Falls back to the below to check USB tuner devices. 111 boolean enabled = isUsbTunerConnected(context); 112 handler.removeMessages(MSG_ENABLE_INPUT_SERVICE); 113 if (enabled) { 114 // Need to check if DVB driver is accessible. Since the driver creation 115 // could be happen after the USB event, delay the checking by 116 // DVB_DRIVER_CHECK_DELAY_MS. 117 handler.sendMessageDelayed(handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), 118 DVB_DRIVER_CHECK_DELAY_MS); 119 } else { 120 if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { 121 // Since network tuner is attached, do not disable TunerTvInput, 122 // just updates the TvInputInfo. 123 TunerInputInfoUtils.updateTunerInputInfo(context); 124 return; 125 } 126 enableTunerTvInputService(context, false, false, TextUtils 127 .equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) ? 128 TunerHal.TUNER_TYPE_USB : null); 129 } 130 } 131 132 private static void onNetworkTunerChanged(Context context, boolean enabled) { 133 SharedPreferences sharedPreferences = 134 PreferenceManager.getDefaultSharedPreferences(context); 135 if (enabled) { 136 // Network tuner detection is initiated by UI. So the app should not 137 // be killed. 138 sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); 139 enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK); 140 } else { 141 sharedPreferences.edit() 142 .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply(); 143 if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) { 144 // Network tuner detection is initiated by UI. So the app should not 145 // be killed. 146 enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK); 147 } else { 148 // Since USB tuner is attached, do not disable TunerTvInput, 149 // just updates the TvInputInfo. 150 TunerInputInfoUtils.updateTunerInputInfo(context); 151 } 152 } 153 } 154 155 /** 156 * See if any USB tuner hardware is attached in the system. 157 * 158 * @param context {@link Context} instance 159 * @return {@code true} if any tuner device we support is plugged in 160 */ 161 private static boolean isUsbTunerConnected(Context context) { 162 UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 163 Map<String, UsbDevice> deviceList = manager.getDeviceList(); 164 String currentSecurityLevel = 165 SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); 166 167 for (UsbDevice device : deviceList.values()) { 168 if (DEBUG) { 169 Log.d(TAG, "Device: " + device); 170 } 171 for (TunerDevice tuner : TUNER_DEVICES) { 172 if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) { 173 Log.i(TAG, "Tuner found"); 174 return true; 175 } 176 } 177 } 178 return false; 179 } 180 181 /** 182 * Enable/disable the component {@link TunerTvInputService}. 183 * 184 * @param context {@link Context} instance 185 * @param enabled {@code true} to enable the service; otherwise {@code false} 186 */ 187 private static void enableTunerTvInputService(Context context, boolean enabled, 188 boolean forceDontKillApp, Integer tunerType) { 189 if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); 190 PackageManager pm = context.getPackageManager(); 191 ComponentName componentName = new ComponentName(context, TunerTvInputService.class); 192 193 // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling 194 // TvActivity, the following pm.setComponentEnabledSetting doesn't work. 195 ((TvApplication) context.getApplicationContext()).handleInputCountChanged( 196 true, enabled, true); 197 // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds 198 // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only 199 // when the LiveChannels app is active since we don't want to kill the running app. 200 int flags = forceDontKillApp 201 || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() 202 ? PackageManager.DONT_KILL_APP : 0; 203 int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED 204 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; 205 if (newState != pm.getComponentEnabledSetting(componentName)) { 206 // Send/cancel the USB tuner TV input setup notification. 207 TunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); 208 // Enable/disable the USB tuner TV input. 209 pm.setComponentEnabledSetting(componentName, newState, flags); 210 if (!enabled && tunerType != null) { 211 if (tunerType == TunerHal.TUNER_TYPE_USB) { 212 Toast.makeText(context, R.string.msg_usb_tuner_disconnected, 213 Toast.LENGTH_SHORT).show(); 214 } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { 215 Toast.makeText(context, R.string.msg_network_tuner_disconnected, 216 Toast.LENGTH_SHORT).show(); 217 } 218 } 219 if (DEBUG) Log.d(TAG, "Status updated:" + enabled); 220 } else if (enabled) { 221 // When # of tuners is changed or the tuner input service is switching from/to using 222 // network tuners or the device just boots. 223 TunerInputInfoUtils.updateTunerInputInfo(context); 224 } 225 } 226 227 /** 228 * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. 229 */ 230 public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) { 231 boolean runningInMainProcess = 232 TvApplication.getSingletons(context).isRunningInMainProcess(); 233 SoftPreconditions.checkState(runningInMainProcess); 234 if (!runningInMainProcess) { 235 return; 236 } 237 executeNetworkTunerDiscoveryAsyncTask(context, 0); 238 } 239 240 /** 241 * Discovers a network tuner. 242 * @param context {@link Context} 243 * @param repeatedDurationMs the time length to wait to repeatedly check network status to start 244 * finding network tuner when the network connection is not available. 245 * {@code 0} to disable repeatedly checking. 246 */ 247 private static void executeNetworkTunerDiscoveryAsyncTask(final Context context, 248 final long repeatedDurationMs) { 249 if (!Features.NETWORK_TUNER.isEnabled(context)) { 250 return; 251 } 252 new AsyncTask<Void, Void, Boolean>() { 253 @Override 254 protected Boolean doInBackground(Void... params) { 255 if (isNetworkConnected(context)) { 256 // Implement and execute network tuner discovery AsyncTask here. 257 } else if (repeatedDurationMs > 0) { 258 AlarmManager alarmManager = 259 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 260 Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); 261 networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION); 262 networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs); 263 PendingIntent alarmIntent = PendingIntent.getBroadcast( 264 context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT); 265 alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() 266 + repeatedDurationMs, alarmIntent); 267 } 268 return null; 269 } 270 271 @Override 272 protected void onPostExecute(Boolean result) { 273 if (result == null) { 274 return; 275 } 276 onNetworkTunerChanged(context, result); 277 } 278 }.execute(); 279 } 280 281 private static boolean isNetworkConnected(Context context) { 282 ConnectivityManager cm = (ConnectivityManager) 283 context.getSystemService(Context.CONNECTIVITY_SERVICE); 284 NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 285 return networkInfo != null && networkInfo.isConnected(); 286 } 287 288 public static class IntentReceiver extends BroadcastReceiver { 289 private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(); 290 291 @Override 292 public void onReceive(Context context, Intent intent) { 293 if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); 294 TvApplication.setCurrentRunningProcess(context, true); 295 if (!Features.TUNER.isEnabled(context)) { 296 enableTunerTvInputService(context, false, false, null); 297 return; 298 } 299 switch (intent.getAction()) { 300 case Intent.ACTION_BOOT_COMPLETED: 301 executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS); 302 case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: 303 case UsbManager.ACTION_USB_DEVICE_ATTACHED: 304 case UsbManager.ACTION_USB_DEVICE_DETACHED: 305 onCheckingUsbTunerStatus(context, intent.getAction(), mHandler); 306 break; 307 case CHECKING_NETWORK_CONNECTION: 308 long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION, 309 INITIAL_CHECKING_DURATION_MS); 310 executeNetworkTunerDiscoveryAsyncTask(context, 311 Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS)); 312 break; 313 } 314 } 315 } 316 317 /** 318 * Simple data holder for a USB device. Used to represent a tuner model, and compare 319 * against {@link UsbDevice}. 320 */ 321 private static class TunerDevice { 322 private final int vendorId; 323 private final int productId; 324 325 // security patch level from which the specific tuner type is supported. 326 private final String minSecurityLevel; 327 328 private TunerDevice(int vendorId, int productId, String minSecurityLevel) { 329 this.vendorId = vendorId; 330 this.productId = productId; 331 this.minSecurityLevel = minSecurityLevel; 332 } 333 334 private boolean equals(UsbDevice device) { 335 return device.getVendorId() == vendorId && device.getProductId() == productId; 336 } 337 338 private boolean isSupported(String currentSecurityLevel) { 339 if (minSecurityLevel == null) { 340 return true; 341 } 342 343 long supportSecurityLevelTimeStamp = 0; 344 long currentSecurityLevelTimestamp = 0; 345 try { 346 SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT); 347 supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime(); 348 currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime(); 349 } catch (ParseException e) { 350 } 351 return supportSecurityLevelTimeStamp != 0 352 && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp; 353 } 354 } 355 356 private static class CheckDvbDeviceHandler extends Handler { 357 private DvbDeviceAccessor mDvbDeviceAccessor; 358 359 CheckDvbDeviceHandler() { 360 super(Looper.getMainLooper()); 361 } 362 363 @Override 364 public void handleMessage(Message msg) { 365 switch (msg.what) { 366 case MSG_ENABLE_INPUT_SERVICE: 367 Context context = (Context) msg.obj; 368 if (mDvbDeviceAccessor == null) { 369 mDvbDeviceAccessor = new DvbDeviceAccessor(context); 370 } 371 boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); 372 enableTunerTvInputService( 373 context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null); 374 break; 375 } 376 } 377 } 378 } 379