1 /*
2  * Copyright (C) 2016 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.hdhomerun;
18 
19 import android.content.Context;
20 import android.text.TextUtils;
21 import android.util.Log;
22 import com.android.tv.common.SoftPreconditions;
23 import com.android.tv.common.compat.TvInputConstantCompat;
24 import com.android.tv.tuner.api.Tuner;
25 import com.android.tv.tuner.data.TunerChannel;
26 import java.io.BufferedInputStream;
27 import java.io.IOException;
28 import java.net.HttpURLConnection;
29 import java.net.MalformedURLException;
30 import java.net.URL;
31 import java.net.URLConnection;
32 
33 /** Tuner implementation for HdHomeRun */
34 public class HdHomeRunTunerHal implements Tuner {
35     private static final String TAG = "HdHomeRunTunerHal";
36     private static final boolean DEBUG = false;
37 
38     private static final String CABLECARD_MODEL = "cablecard";
39     private static final String ATSC_MODEL = "atsc";
40     private static final String DVBC_MODEL = "dvbc";
41     private static final String DVBT_MODEL = "dvbt";
42 
43     private final HdHomeRunTunerManager mTunerManager;
44     private HdHomeRunDevice mDevice;
45     private BufferedInputStream mInputStream;
46     private HttpURLConnection mConnection;
47     private String mHttpConnectionAddress;
48     private final Context mContext;
49 
50     @DeliverySystemType private int mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
51 
52     public static final char VCHANNEL_SEPARATOR = '.';
53     public static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec
54     public static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec
55 
HdHomeRunTunerHal(Context context)56     public HdHomeRunTunerHal(Context context) {
57         mTunerManager = HdHomeRunTunerManager.getInstance();
58         mContext = context;
59     }
60 
61     @Override
openFirstAvailable()62     public boolean openFirstAvailable() {
63         SoftPreconditions.checkState(mDevice == null);
64         try {
65             mDevice = mTunerManager.acquireDevice(mContext);
66             if (mDevice != null) {
67                 if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) {
68                     mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId());
69                 }
70             }
71             return mDevice != null;
72         } catch (Exception e) {
73             Log.w(TAG, "Failed to open first available device", e);
74             return false;
75         }
76     }
77 
78     @Override
isDeviceOpen()79     public boolean isDeviceOpen() {
80         return mDevice != null;
81     }
82 
83     @Override
isReusable()84     public boolean isReusable() {
85         return false;
86     }
87 
88     @Override
getDeviceId()89     public long getDeviceId() {
90         return mDevice == null ? 0 : mDevice.getDeviceId();
91     }
92 
93     @Override
close()94     public void close() throws Exception {
95         closeInputStreamAndDisconnect();
96         if (mDevice != null) {
97             mTunerManager.releaseDevice(mDevice);
98             mDevice = null;
99         }
100     }
101 
102     @Override
tune( int frequency, @ModulationType String modulation, String channelNumber)103     public synchronized boolean tune(
104             int frequency, @ModulationType String modulation, String channelNumber) {
105         if (DEBUG) {
106             Log.d(
107                     TAG,
108                     "tune(frequency="
109                             + frequency
110                             + ", modulation="
111                             + modulation
112                             + ", channelNumber="
113                             + channelNumber
114                             + ")");
115         }
116         closeInputStreamAndDisconnect();
117         if (TextUtils.isEmpty(channelNumber)) {
118             return false;
119         }
120         channelNumber =
121                 channelNumber.replace(TunerChannel.CHANNEL_NUMBER_SEPARATOR, VCHANNEL_SEPARATOR);
122         mHttpConnectionAddress = "http://" + getIpAddress() + ":5004/auto/v" + channelNumber;
123         return connectAndOpenInputStream();
124     }
125 
connectAndOpenInputStream()126     private boolean connectAndOpenInputStream() {
127         URL url;
128         try {
129             url = new URL(mHttpConnectionAddress);
130         } catch (MalformedURLException e) {
131             Log.e(TAG, "Invalid address: " + mHttpConnectionAddress, e);
132             return false;
133         }
134         URLConnection connection;
135         try {
136             connection = url.openConnection();
137             connection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
138             connection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
139             if (connection instanceof HttpURLConnection) {
140                 mConnection = (HttpURLConnection) connection;
141             }
142         } catch (IOException e) {
143             Log.e(TAG, "Connection failed: " + mHttpConnectionAddress, e);
144             return false;
145         }
146         try {
147             mInputStream = new BufferedInputStream(connection.getInputStream());
148         } catch (IOException e) {
149             closeInputStreamAndDisconnect();
150             Log.e(TAG, "Failed to get input stream from " + mHttpConnectionAddress, e);
151             return false;
152         }
153         if (DEBUG) Log.d(TAG, "tuning to " + mHttpConnectionAddress);
154         return true;
155     }
156 
157     @Override
addPidFilter(int pid, @FilterType int filterType)158     public synchronized boolean addPidFilter(int pid, @FilterType int filterType) {
159         // no-op
160         return true;
161     }
162 
163     @Override
stopTune()164     public synchronized void stopTune() {
165         closeInputStreamAndDisconnect();
166     }
167 
168     @Override
readTsStream(byte[] javaBuffer, int javaBufferSize)169     public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) {
170         if (mInputStream != null) {
171             try {
172                 // Note: this call sometimes take more than 500ms, because the data is
173                 // streamed through network unlike connected tuner devices.
174                 return mInputStream.read(javaBuffer, 0, javaBufferSize);
175             } catch (IOException e) {
176                 Log.e(TAG, "Failed to read stream", e);
177                 closeInputStreamAndDisconnect();
178             }
179         }
180         if (connectAndOpenInputStream()) {
181             Log.w(TAG, "Tuned by http connection again");
182         } else {
183             Log.e(TAG, "Tuned by http connection again failed");
184         }
185         return 0;
186     }
187 
188     @Override
setHasPendingTune(boolean hasPendingTune)189     public void setHasPendingTune(boolean hasPendingTune) {
190         // no-op
191     }
192 
nativeGetDeliverySystemType(long deviceId)193     protected int nativeGetDeliverySystemType(long deviceId) {
194         String deviceModel = mDevice.getDeviceModel();
195         if (SoftPreconditions.checkState(!TextUtils.isEmpty(deviceModel))) {
196             if (deviceModel.contains(CABLECARD_MODEL) || deviceModel.contains(ATSC_MODEL)) {
197                 return DELIVERY_SYSTEM_ATSC;
198             } else if (deviceModel.contains(DVBC_MODEL)) {
199                 return DELIVERY_SYSTEM_DVBC;
200             } else if (deviceModel.contains(DVBT_MODEL)) {
201                 return DELIVERY_SYSTEM_DVBT;
202             }
203         }
204         return DELIVERY_SYSTEM_UNDEFINED;
205     }
206 
closeInputStreamAndDisconnect()207     private void closeInputStreamAndDisconnect() {
208         if (mInputStream != null) {
209             try {
210                 mInputStream.close();
211             } catch (IOException e) {
212                 Log.e(TAG, "Failed to close input stream", e);
213             }
214             mInputStream = null;
215         }
216         if (mConnection != null) {
217             mConnection.disconnect();
218             mConnection = null;
219         }
220     }
221 
222     /** Gets the number of tuners in a given HDHomeRun devices. */
getNumberOfDevices()223     public static int getNumberOfDevices() {
224         return HdHomeRunTunerManager.getInstance().getTunerCount();
225     }
226 
227     /** Returns the IP address. */
getIpAddress()228     public String getIpAddress() {
229         return HdHomeRunUtils.getIpString(mDevice.getIpAddress());
230     }
231 
232     /**
233      * Marks the device associated to this instance as a scanned device. Scanned device has higher
234      * priority among multiple HDHomeRun devices.
235      */
markAsScannedDevice(Context context)236     public void markAsScannedDevice(Context context) {
237         HdHomeRunTunerManager.markAsScannedDevice(context, mDevice);
238     }
239 
240     @Override
241     @DeliverySystemType
getDeliverySystemType()242     public int getDeliverySystemType() {
243         return Tuner.DELIVERY_SYSTEM_UNDEFINED;
244     }
245 
246     @Override
getSignalStrength()247     public int getSignalStrength() {
248         return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
249     }
250 }
251