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