blob: e06b9b4a829a9551a642374c699f8492480ba6f9 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.tuner;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.android.tv.Features;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.setup.TunerSetupActivity;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import com.android.tv.tuner.util.SystemPropertiesProxy;
import com.android.tv.tuner.util.TunerInputInfoUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Controls the package visibility of {@link TunerTvInputService}.
* <p>
* Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED},
* {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}
* to update the connection status of the supported USB TV tuners.
*/
public class TunerInputController {
private static final boolean DEBUG = true;
private static final String TAG = "TunerInputController";
private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
/**
* Action of {@link Intent} to check network connection repeatedly when it is necessary.
*/
private static final String CHECKING_NETWORK_CONNECTION =
"com.android.tv.action.CHECKING_NETWORK_CONNECTION";
private static final String EXTRA_CHECKING_DURATION =
"com.android.tv.action.extra.CHECKING_DURATION";
private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
private static final TunerDevice[] TUNER_DEVICES = {
new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
// WinTV-dualHD (bulk) will be supported after 2017 April security patch.
new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
// STOPSHIP: Add WinTV-soloHD (Isoc) temporary for test. Remove this after test complete.
new TunerDevice(0x2040, 0x0264, null),
};
private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
/**
* Checks status of USB devices to see if there are available USB tuners connected.
*/
public static void onCheckingUsbTunerStatus(Context context, String action) {
onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler());
}
private static void onCheckingUsbTunerStatus(Context context, String action,
@NonNull CheckDvbDeviceHandler handler) {
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
if (TunerHal.useBuiltInTuner(context)) {
enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN);
return;
}
// Falls back to the below to check USB tuner devices.
boolean enabled = isUsbTunerConnected(context);
handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
if (enabled) {
// Need to check if DVB driver is accessible. Since the driver creation
// could be happen after the USB event, delay the checking by
// DVB_DRIVER_CHECK_DELAY_MS.
handler.sendMessageDelayed(handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
DVB_DRIVER_CHECK_DELAY_MS);
} else {
if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
// Since network tuner is attached, do not disable TunerTvInput,
// just updates the TvInputInfo.
TunerInputInfoUtils.updateTunerInputInfo(context);
return;
}
enableTunerTvInputService(context, false, false, TextUtils
.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) ?
TunerHal.TUNER_TYPE_USB : null);
}
}
private static void onNetworkTunerChanged(Context context, boolean enabled) {
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
if (enabled) {
// Network tuner detection is initiated by UI. So the app should not
// be killed.
sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK);
} else {
sharedPreferences.edit()
.putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply();
if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) {
// Network tuner detection is initiated by UI. So the app should not
// be killed.
enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK);
} else {
// Since USB tuner is attached, do not disable TunerTvInput,
// just updates the TvInputInfo.
TunerInputInfoUtils.updateTunerInputInfo(context);
}
}
}
/**
* See if any USB tuner hardware is attached in the system.
*
* @param context {@link Context} instance
* @return {@code true} if any tuner device we support is plugged in
*/
private static boolean isUsbTunerConnected(Context context) {
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
Map<String, UsbDevice> deviceList = manager.getDeviceList();
String currentSecurityLevel =
SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
return true;
}
}
}
return false;
}
/**
* Enable/disable the component {@link TunerTvInputService}.
*
* @param context {@link Context} instance
* @param enabled {@code true} to enable the service; otherwise {@code false}
*/
private static void enableTunerTvInputService(Context context, boolean enabled,
boolean forceDontKillApp, Integer tunerType) {
if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
PackageManager pm = context.getPackageManager();
ComponentName componentName = new ComponentName(context, TunerTvInputService.class);
// Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling
// TvActivity, the following pm.setComponentEnabledSetting doesn't work.
((TvApplication) context.getApplicationContext()).handleInputCountChanged(
true, enabled, true);
// Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds
// (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only
// when the LiveChannels app is active since we don't want to kill the running app.
int flags = forceDontKillApp
|| TvApplication.getSingletons(context).getMainActivityWrapper().isCreated()
? PackageManager.DONT_KILL_APP : 0;
int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
if (newState != pm.getComponentEnabledSetting(componentName)) {
// Send/cancel the USB tuner TV input setup notification.
TunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
// Enable/disable the USB tuner TV input.
pm.setComponentEnabledSetting(componentName, newState, flags);
if (!enabled && tunerType != null) {
if (tunerType == TunerHal.TUNER_TYPE_USB) {
Toast.makeText(context, R.string.msg_usb_tuner_disconnected,
Toast.LENGTH_SHORT).show();
} else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
Toast.makeText(context, R.string.msg_network_tuner_disconnected,
Toast.LENGTH_SHORT).show();
}
}
if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
} else if (enabled) {
// When # of tuners is changed or the tuner input service is switching from/to using
// network tuners or the device just boots.
TunerInputInfoUtils.updateTunerInputInfo(context);
}
}
/**
* Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
*/
public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
boolean runningInMainProcess =
TvApplication.getSingletons(context).isRunningInMainProcess();
SoftPreconditions.checkState(runningInMainProcess);
if (!runningInMainProcess) {
return;
}
executeNetworkTunerDiscoveryAsyncTask(context, 0);
}
/**
* Discovers a network tuner.
* @param context {@link Context}
* @param repeatedDurationMs the time length to wait to repeatedly check network status to start
* finding network tuner when the network connection is not available.
* {@code 0} to disable repeatedly checking.
*/
private static void executeNetworkTunerDiscoveryAsyncTask(final Context context,
final long repeatedDurationMs) {
if (!Features.NETWORK_TUNER.isEnabled(context)) {
return;
}
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
if (isNetworkConnected(context)) {
// Implement and execute network tuner discovery AsyncTask here.
} else if (repeatedDurationMs > 0) {
AlarmManager alarmManager =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION);
networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs);
PendingIntent alarmIntent = PendingIntent.getBroadcast(
context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime()
+ repeatedDurationMs, alarmIntent);
}
return null;
}
@Override
protected void onPostExecute(Boolean result) {
if (result == null) {
return;
}
onNetworkTunerChanged(context, result);
}
}.execute();
}
private static boolean isNetworkConnected(Context context) {
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
public static class IntentReceiver extends BroadcastReceiver {
private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler();
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
TvApplication.setCurrentRunningProcess(context, true);
if (!Features.TUNER.isEnabled(context)) {
enableTunerTvInputService(context, false, false, null);
return;
}
switch (intent.getAction()) {
case Intent.ACTION_BOOT_COMPLETED:
executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS);
case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
case UsbManager.ACTION_USB_DEVICE_DETACHED:
onCheckingUsbTunerStatus(context, intent.getAction(), mHandler);
break;
case CHECKING_NETWORK_CONNECTION:
long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION,
INITIAL_CHECKING_DURATION_MS);
executeNetworkTunerDiscoveryAsyncTask(context,
Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS));
break;
}
}
}
/**
* Simple data holder for a USB device. Used to represent a tuner model, and compare
* against {@link UsbDevice}.
*/
private static class TunerDevice {
private final int vendorId;
private final int productId;
// security patch level from which the specific tuner type is supported.
private final String minSecurityLevel;
private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
this.vendorId = vendorId;
this.productId = productId;
this.minSecurityLevel = minSecurityLevel;
}
private boolean equals(UsbDevice device) {
return device.getVendorId() == vendorId && device.getProductId() == productId;
}
private boolean isSupported(String currentSecurityLevel) {
if (minSecurityLevel == null) {
return true;
}
long supportSecurityLevelTimeStamp = 0;
long currentSecurityLevelTimestamp = 0;
try {
SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
} catch (ParseException e) {
}
return supportSecurityLevelTimeStamp != 0
&& supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
}
}
private static class CheckDvbDeviceHandler extends Handler {
private DvbDeviceAccessor mDvbDeviceAccessor;
CheckDvbDeviceHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ENABLE_INPUT_SERVICE:
Context context = (Context) msg.obj;
if (mDvbDeviceAccessor == null) {
mDvbDeviceAccessor = new DvbDeviceAccessor(context);
}
boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
enableTunerTvInputService(
context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null);
break;
}
}
}
}