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.media.tv.TvContract;
21 import android.os.ConditionVariable;
22 import android.util.Log;
23 import android.util.Xml;
24 import com.android.tv.tuner.api.ChannelScanListener;
25 import com.android.tv.tuner.data.TunerChannel;
26 import com.android.tv.tuner.ts.EventDetector.EventListener;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.net.HttpURLConnection;
30 import java.net.URL;
31 import java.util.regex.Pattern;
32 import org.xmlpull.v1.XmlPullParser;
33 import org.xmlpull.v1.XmlPullParserException;
34 
35 /** A helper class to perform channel scan on HDHomeRun tuner. */
36 public class HdHomeRunChannelScan {
37     private static final String TAG = "HdHomeRunChannelScan";
38     private static final boolean DEBUG = false;
39 
40     private static final String LINEUP_FILENAME = "lineup.xml";
41     private static final String NAME_LINEUP = "Lineup";
42     private static final String NAME_PROGRAM = "Program";
43     private static final String NAME_GUIDE_NUMBER = "GuideNumber";
44     private static final String NAME_GUIDE_NAME = "GuideName";
45     private static final String NAME_HD = "HD";
46     private static final String NAME_TAGS = "Tags";
47     private static final String NAME_DRM = "DRM";
48 
49     private final Context mContext;
50     private final ChannelScanListener mEventListener;
51     private final HdHomeRunTunerHal mTunerHal;
52     private int mProgramCount;
53 
HdHomeRunChannelScan( Context context, EventListener eventListener, HdHomeRunTunerHal hal)54     public HdHomeRunChannelScan(
55             Context context, EventListener eventListener, HdHomeRunTunerHal hal) {
56         mContext = context;
57         mEventListener = eventListener;
58         mTunerHal = hal;
59     }
60 
scan(ConditionVariable conditionStopped)61     public void scan(ConditionVariable conditionStopped) {
62         String urlString = "http://" + mTunerHal.getIpAddress() + "/" + LINEUP_FILENAME;
63         if (DEBUG) Log.d(TAG, "Reading " + urlString);
64         URL url;
65         HttpURLConnection connection = null;
66         InputStream inputStream;
67         try {
68             url = new URL(urlString);
69             connection = (HttpURLConnection) url.openConnection();
70             connection.setReadTimeout(HdHomeRunTunerHal.READ_TIMEOUT_MS_FOR_URLCONNECTION);
71             connection.setConnectTimeout(HdHomeRunTunerHal.CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
72             connection.setRequestMethod("GET");
73             connection.setDoInput(true);
74             connection.connect();
75             inputStream = connection.getInputStream();
76         } catch (IOException e) {
77             Log.e(TAG, "Connection failed: " + urlString, e);
78             if (connection != null) {
79                 connection.disconnect();
80             }
81             return;
82         }
83         if (conditionStopped.block(-1)) {
84             try {
85                 inputStream.close();
86             } catch (IOException e) {
87                 // Does nothing.
88             }
89             connection.disconnect();
90             return;
91         }
92 
93         XmlPullParser parser = Xml.newPullParser();
94         try {
95             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
96             parser.setInput(inputStream, null);
97             parser.nextTag();
98             parser.require(XmlPullParser.START_TAG, null, NAME_LINEUP);
99             while (parser.next() != XmlPullParser.END_TAG) {
100                 if (conditionStopped.block(-1)) {
101                     break;
102                 }
103                 if (parser.getEventType() != XmlPullParser.START_TAG) {
104                     continue;
105                 }
106                 String name = parser.getName();
107                 // Starts by looking for the program tag
108                 if (name.equals(NAME_PROGRAM)) {
109                     readProgram(parser);
110                 } else {
111                     skip(parser);
112                 }
113             }
114             inputStream.close();
115         } catch (IOException | XmlPullParserException e) {
116             Log.e(TAG, "Parse error", e);
117         }
118         connection.disconnect();
119         mTunerHal.markAsScannedDevice(mContext);
120     }
121 
readProgram(XmlPullParser parser)122     private void readProgram(XmlPullParser parser) throws XmlPullParserException, IOException {
123         parser.require(XmlPullParser.START_TAG, null, NAME_PROGRAM);
124         String guideNumber = "";
125         String guideName = "";
126         String videoFormat = null;
127         String tags = "";
128         boolean recordingProhibited = false;
129         while (parser.next() != XmlPullParser.END_TAG) {
130             if (parser.getEventType() != XmlPullParser.START_TAG) {
131                 continue;
132             }
133             String name = parser.getName();
134             if (name.equals(NAME_GUIDE_NUMBER)) {
135                 guideNumber = readText(parser, NAME_GUIDE_NUMBER);
136             } else if (name.equals(NAME_GUIDE_NAME)) {
137                 guideName = readText(parser, NAME_GUIDE_NAME);
138             } else if (name.equals(NAME_HD)) {
139                 videoFormat = TvContract.Channels.VIDEO_FORMAT_720P;
140                 skip(parser);
141             } else if (name.equals(NAME_TAGS)) {
142                 tags = readText(parser, NAME_TAGS);
143             } else if (name.equals(NAME_DRM)) {
144                 String drm = readText(parser, NAME_DRM);
145                 try {
146                     recordingProhibited = (Integer.parseInt(drm)) != 0;
147                 } catch (NumberFormatException e) {
148                     Log.e(TAG, "Load DRM property failed: illegal number: " + drm);
149                     // If DRM property is present, we treat it as copy-once or copy-never.
150                     recordingProhibited = true;
151                 }
152             } else {
153                 skip(parser);
154             }
155         }
156         if (!tags.isEmpty()) {
157             // Skip encrypted channels since we don't know how to decrypt them.
158             return;
159         }
160         int major;
161         int minor = 0;
162         final String separator = Character.toString(HdHomeRunTunerHal.VCHANNEL_SEPARATOR);
163         if (guideNumber.contains(separator)) {
164             String[] parts = guideNumber.split(Pattern.quote(separator));
165             major = Integer.parseInt(parts[0]);
166             minor = Integer.parseInt(parts[1]);
167         } else {
168             major = Integer.parseInt(guideNumber);
169         }
170         // Need to assign a unique program number (i.e. mProgramCount) to avoid being duplicated.
171         mEventListener.onChannelDetected(
172                 TunerChannel.forNetwork(
173                         major, minor, mProgramCount++, guideName, recordingProhibited, videoFormat),
174                 true);
175     }
176 
readText(XmlPullParser parser, String name)177     private String readText(XmlPullParser parser, String name)
178             throws IOException, XmlPullParserException {
179         String result = "";
180         parser.require(XmlPullParser.START_TAG, null, name);
181         if (parser.next() == XmlPullParser.TEXT) {
182             result = parser.getText();
183             parser.nextTag();
184         }
185         parser.require(XmlPullParser.END_TAG, null, name);
186         if (DEBUG) Log.d(TAG, "<" + name + ">=" + result);
187         return result;
188     }
189 
skip(XmlPullParser parser)190     private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
191         if (parser.getEventType() != XmlPullParser.START_TAG) {
192             throw new IllegalStateException();
193         }
194         int depth = 1;
195         while (depth != 0) {
196             switch (parser.next()) {
197                 case XmlPullParser.END_TAG:
198                     depth--;
199                     break;
200                 case XmlPullParser.START_TAG:
201                     depth++;
202                     break;
203             }
204         }
205     }
206 }
207