Home | History | Annotate | Download | only in tuner
      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