blob: 02611bbfbb536bfd28b5c473b7a20cd759b775b1 [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.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
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.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
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.R;
import com.android.tv.Starter;
import com.android.tv.TvApplication;
import com.android.tv.TvSingletons;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.util.SystemPropertiesProxy;
import com.android.tv.tuner.setup.BaseTunerSetupActivity;
import com.android.tv.tuner.util.TunerInputInfoUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Controls the package visibility of {@link BaseTunerTvInputService}.
*
* <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 = false;
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";
private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s";
/** Action of {@link Intent} to check network connection repeatedly when it is necessary. */
private static final String CHECKING_NETWORK_TUNER_STATUS =
"com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS";
private static final String EXTRA_CHECKING_DURATION =
"com.android.tv.action.extra.CHECKING_DURATION";
private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP";
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 String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification";
// TODO: Load settings from XML file
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)
new TunerDevice(0x2040, 0x0264, null),
};
private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
private final ComponentName usbTunerComponent;
private final ComponentName networkTunerComponent;
private final ComponentName builtInTunerComponent;
private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>();
private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>();
private final Map<ComponentName, String> mNotificationMessages = new HashMap<>();
private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>();
private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this);
public TunerInputController(ComponentName embeddedTuner) {
usbTunerComponent = embeddedTuner;
networkTunerComponent = usbTunerComponent;
builtInTunerComponent = usbTunerComponent;
for (TunerDevice device : TUNER_DEVICES) {
mTunerServiceMapping.put(device, usbTunerComponent);
}
}
/** Checks status of USB devices to see if there are available USB tuners connected. */
public void onCheckingUsbTunerStatus(Context context, String action) {
onCheckingUsbTunerStatus(context, action, mHandler);
}
private void onCheckingUsbTunerStatus(
Context context, String action, @NonNull CheckDvbDeviceHandler handler) {
Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context);
handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
if (!connectedUsbTuners.isEmpty()) {
// 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 {
handleTunerStatusChanged(
context,
false,
connectedUsbTuners,
TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED)
? TunerHal.TUNER_TYPE_USB
: null);
}
}
private void onNetworkTunerChanged(Context context, boolean enabled) {
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED)
&& sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
== enabled) {
// the status is not changed
return;
}
if (enabled) {
sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
} else {
sharedPreferences
.edit()
.putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
.apply();
}
// Network tuner detection is initiated by UI. So the app should not
// be killed.
handleTunerStatusChanged(
context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK);
}
/**
* 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 Set<TunerDevice> getConnectedUsbTuners(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);
Set<TunerDevice> devices = new HashSet<>();
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
devices.add(tuner);
}
}
}
return devices;
}
private void handleTunerStatusChanged(
Context context,
boolean forceDontKillApp,
Set<TunerDevice> connectedUsbTuners,
Integer triggerType) {
Map<ComponentName, Integer> serviceToEnable = new HashMap<>();
Set<ComponentName> serviceToDisable = new HashSet<>();
serviceToDisable.add(builtInTunerComponent);
serviceToDisable.add(networkTunerComponent);
if (TunerFeatures.TUNER.isEnabled(context)) {
// TODO: support both built-in tuner and other tuners at the same time?
if (TunerHal.useBuiltInTuner(context)) {
enableTunerTvInputService(
context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent);
return;
}
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK);
}
}
for (TunerDevice device : TUNER_DEVICES) {
if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) {
serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB);
} else {
serviceToDisable.add(mTunerServiceMapping.get(device));
}
}
serviceToDisable.removeAll(serviceToEnable.keySet());
for (ComponentName serviceComponent : serviceToEnable.keySet()) {
if (isTunerPackageInstalled(context, serviceComponent)) {
enableTunerTvInputService(
context,
true,
forceDontKillApp,
serviceToEnable.get(serviceComponent),
serviceComponent);
} else {
sendNotificationToInstallPackage(context, serviceComponent);
}
}
for (ComponentName serviceComponent : serviceToDisable) {
if (isTunerPackageInstalled(context, serviceComponent)) {
enableTunerTvInputService(
context, false, forceDontKillApp, triggerType, serviceComponent);
} else {
cancelNotificationToInstallPackage(context, serviceComponent);
}
}
}
/**
* Enable/disable the component {@link BaseTunerTvInputService}.
*
* @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,
ComponentName serviceComponent) {
if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
PackageManager pm = context.getPackageManager();
int newState =
enabled
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
if (newState != pm.getComponentEnabledSetting(serviceComponent)) {
int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0;
if (serviceComponent.getPackageName().equals(context.getPackageName())) {
// Don't kill APP when handling input count changing. Or the following
// setComponentEnabledSetting() call won't work.
((TvApplication) context.getApplicationContext())
.handleInputCountChanged(true, enabled, true);
// Bundled input. Don't kill app if LiveChannels app is active since we don't want
// to kill the running app.
if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) {
flags |= PackageManager.DONT_KILL_APP;
}
// Send/cancel the USB tuner TV input setup notification.
BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
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();
}
}
}
// Enable/disable the USB tuner TV input.
pm.setComponentEnabledSetting(serviceComponent, newState, flags);
if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
} else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) {
// 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 void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
executeNetworkTunerDiscoveryAsyncTask(context, 0, 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.
* @param deviceIp The previous discovered device IP, 0 if none.
*/
private void executeNetworkTunerDiscoveryAsyncTask(
final Context context, final long repeatedDurationMs, final int deviceIp) {
if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) {
return;
}
final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS);
if (!isNetworkConnected(context) && repeatedDurationMs > 0) {
sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs);
} else {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
Boolean result = null;
// Implement and execute network tuner discovery AsyncTask here.
return result;
}
@Override
protected void onPostExecute(Boolean foundNetworkTuner) {
if (foundNetworkTuner == null) {
return;
}
sendCheckingAlarm(
context,
networkCheckingIntent,
foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs);
onNetworkTunerChanged(context, foundNetworkTuner);
}
}.execute();
}
}
private static boolean isNetworkConnected(Context context) {
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
intent.putExtra(EXTRA_CHECKING_DURATION, delayMs);
PendingIntent alarmIntent =
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + delayMs,
alarmIntent);
}
private static boolean isTunerPackageInstalled(
Context context, ComponentName serviceComponent) {
try {
context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) {
if (!BuildConfig.ENG) {
return;
}
String applicationName = mTunerApplicationNames.get(serviceComponent);
if (applicationName == null) {
applicationName = context.getString(R.string.tuner_install_default_application_name);
}
String contentTitle =
context.getString(
R.string.tuner_install_notification_content_title, applicationName);
String contentText = mNotificationMessages.get(serviceComponent);
if (contentText == null) {
contentText = context.getString(R.string.tuner_install_notification_content_text);
}
Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent);
if (largeIcon == null) {
// TODO: Make a better default image.
largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store);
}
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
createNotificationChannel(context, notificationManager);
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(
Uri.parse(
String.format(
PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName())));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher_s)
.setLargeIcon(largeIcon)
.setContentTitle(contentTitle)
.setContentText(contentText)
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build());
}
private static void cancelNotificationToInstallPackage(
Context context, ComponentName serviceComponent) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(serviceComponent.getPackageName(), 0);
}
private static void createNotificationChannel(
Context context, NotificationManager notificationManager) {
notificationManager.createNotificationChannel(
new NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getResources()
.getString(R.string.ut_setup_notification_channel_name),
NotificationManager.IMPORTANCE_HIGH));
}
public static class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
Starter.start(context);
TunerInputController tunerInputController =
TvSingletons.getSingletons(context).getTunerInputController();
if (!TunerFeatures.TUNER.isEnabled(context)) {
tunerInputController.handleTunerStatusChanged(
context, false, Collections.emptySet(), null);
return;
}
switch (intent.getAction()) {
case Intent.ACTION_BOOT_COMPLETED:
tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
context, INITIAL_CHECKING_DURATION_MS, 0);
// fall through
case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
case UsbManager.ACTION_USB_DEVICE_DETACHED:
tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction());
break;
case CHECKING_NETWORK_TUNER_STATUS:
long repeatedDurationMs =
intent.getLongExtra(
EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS);
tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
context,
Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS),
intent.getIntExtra(EXTRA_DEVICE_IP, 0));
break;
default: // fall out
}
}
}
/**
* 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 equalsTo(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 final TunerInputController mTunerInputController;
private DvbDeviceAccessor mDvbDeviceAccessor;
CheckDvbDeviceHandler(TunerInputController tunerInputController) {
super(Looper.getMainLooper());
this.mTunerInputController = tunerInputController;
}
@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();
mTunerInputController.handleTunerStatusChanged(
context,
false,
enabled
? mTunerInputController.getConnectedUsbTuners(context)
: Collections.emptySet(),
TunerHal.TUNER_TYPE_USB);
break;
default: // fall out
}
}
}
}