Advertisement
Guest User

Untitled

a guest
Nov 22nd, 2014
136
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.01 KB | None | 0 0
  1. /*
  2. * Copyright (C) 2014 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. package com.google.android.exoplayer.upstream;
  17.  
  18. import android.text.TextUtils;
  19. import android.util.Log;
  20.  
  21. import com.google.android.exoplayer.C;
  22. import com.google.android.exoplayer.util.Assertions;
  23. import com.google.android.exoplayer.util.Predicate;
  24. import com.google.android.exoplayer.util.Util;
  25.  
  26. import java.io.File;
  27. import java.io.FileInputStream;
  28. import java.io.FileNotFoundException;
  29. import java.io.FileOutputStream;
  30. import java.io.IOException;
  31. import java.io.InputStream;
  32. import java.net.HttpURLConnection;
  33. import java.net.URL;
  34. import java.util.HashMap;
  35. import java.util.List;
  36. import java.util.Map;
  37. import java.util.regex.Matcher;
  38. import java.util.regex.Pattern;
  39.  
  40. /**
  41. * An http {@link DataSource}.
  42. */
  43. public class HttpDataSource implements DataSource {
  44.  
  45. /**
  46. * A {@link Predicate} that rejects content types often used for pay-walls.
  47. */
  48. public static final Predicate<String> REJECT_PAYWALL_TYPES = new Predicate<String>() {
  49.  
  50. @Override
  51. public boolean evaluate(String contentType) {
  52. contentType = Util.toLowerInvariant(contentType);
  53. return !TextUtils.isEmpty(contentType)
  54. && (!contentType.contains("text") || contentType.contains("text/vtt"))
  55. && !contentType.contains("html") && !contentType.contains("xml");
  56. }
  57.  
  58. };
  59.  
  60. /**
  61. * Thrown when an error is encountered when trying to read from HTTP data source.
  62. */
  63. public static class HttpDataSourceException extends IOException {
  64.  
  65. /*
  66. * The {@link DataSpec} associated with the current connection.
  67. */
  68. public final DataSpec dataSpec;
  69.  
  70. public HttpDataSourceException(DataSpec dataSpec) {
  71. super();
  72. this.dataSpec = dataSpec;
  73. }
  74.  
  75. public HttpDataSourceException(String message, DataSpec dataSpec) {
  76. super(message);
  77. this.dataSpec = dataSpec;
  78. }
  79.  
  80. public HttpDataSourceException(IOException cause, DataSpec dataSpec) {
  81. super(cause);
  82. this.dataSpec = dataSpec;
  83. }
  84.  
  85. public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec) {
  86. super(message, cause);
  87. this.dataSpec = dataSpec;
  88. }
  89.  
  90. }
  91.  
  92. /**
  93. * Thrown when the content type is invalid.
  94. */
  95. public static final class InvalidContentTypeException extends HttpDataSourceException {
  96.  
  97. public final String contentType;
  98.  
  99. public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
  100. super("Invalid content type: " + contentType, dataSpec);
  101. this.contentType = contentType;
  102. }
  103.  
  104. }
  105.  
  106. /**
  107. * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
  108. */
  109. public static final class InvalidResponseCodeException extends HttpDataSourceException {
  110.  
  111. /**
  112. * The response code that was outside of the 2xx range.
  113. */
  114. public final int responseCode;
  115.  
  116. /**
  117. * An unmodifiable map of the response header fields and values.
  118. */
  119. public final Map<String, List<String>> headerFields;
  120.  
  121. public InvalidResponseCodeException(int responseCode, Map<String, List<String>> headerFields,
  122. DataSpec dataSpec) {
  123. super("Response code: " + responseCode, dataSpec);
  124. this.responseCode = responseCode;
  125. this.headerFields = headerFields;
  126. }
  127.  
  128. }
  129.  
  130. public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
  131. public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
  132.  
  133. private static final String TAG = "HttpDataSource";
  134. private static final Pattern CONTENT_RANGE_HEADER =
  135. Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
  136.  
  137. private final int connectTimeoutMillis;
  138. private final int readTimeoutMillis;
  139. private final String userAgent;
  140. private final Predicate<String> contentTypePredicate;
  141. private final HashMap<String, String> requestProperties;
  142. private final TransferListener listener;
  143.  
  144. private DataSpec dataSpec;
  145. private HttpURLConnection connection;
  146. private InputStream inputStream;
  147. private boolean opened;
  148.  
  149. private long dataLength;
  150. private long bytesRead;
  151.  
  152. private File cacheFile;
  153. private FileInputStream cacheInputStream;
  154. private FileOutputStream cacheOutputStream;
  155.  
  156. /**
  157. * @param userAgent The User-Agent string that should be used.
  158. * @param contentTypePredicate An optional {@link Predicate}. If a content type is
  159. * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
  160. * {@link #open(DataSpec)}.
  161. */
  162. public HttpDataSource(String userAgent, Predicate<String> contentTypePredicate) {
  163. this(userAgent, contentTypePredicate, null);
  164. }
  165.  
  166. /**
  167. * @param userAgent The User-Agent string that should be used.
  168. * @param contentTypePredicate An optional {@link Predicate}. If a content type is
  169. * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
  170. * {@link #open(DataSpec)}.
  171. * @param listener An optional listener.
  172. */
  173. public HttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
  174. TransferListener listener) {
  175. this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
  176. DEFAULT_READ_TIMEOUT_MILLIS);
  177. }
  178.  
  179. /**
  180. * @param userAgent The User-Agent string that should be used.
  181. * @param contentTypePredicate An optional {@link Predicate}. If a content type is
  182. * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
  183. * {@link #open(DataSpec)}.
  184. * @param listener An optional listener.
  185. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
  186. * interpreted as an infinite timeout.
  187. * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
  188. * as an infinite timeout.
  189. */
  190. public HttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
  191. TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
  192. this.userAgent = Assertions.checkNotEmpty(userAgent);
  193. this.contentTypePredicate = contentTypePredicate;
  194. this.listener = listener;
  195. this.requestProperties = new HashMap<String, String>();
  196. this.connectTimeoutMillis = connectTimeoutMillis;
  197. this.readTimeoutMillis = readTimeoutMillis;
  198. }
  199.  
  200. /**
  201. * Sets the value of a request header field. The value will be used for subsequent connections
  202. * established by the source.
  203. *
  204. * @param name The name of the header field.
  205. * @param value The value of the field.
  206. */
  207. public void setRequestProperty(String name, String value) {
  208. Assertions.checkNotNull(name);
  209. Assertions.checkNotNull(value);
  210. synchronized (requestProperties) {
  211. requestProperties.put(name, value);
  212. }
  213. }
  214.  
  215. /**
  216. * Clears the value of a request header field. The change will apply to subsequent connections
  217. * established by the source.
  218. *
  219. * @param name The name of the header field.
  220. */
  221. public void clearRequestProperty(String name) {
  222. Assertions.checkNotNull(name);
  223. synchronized (requestProperties) {
  224. requestProperties.remove(name);
  225. }
  226. }
  227.  
  228. /**
  229. * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}.
  230. */
  231. public void clearAllRequestProperties() {
  232. synchronized (requestProperties) {
  233. requestProperties.clear();
  234. }
  235. }
  236.  
  237. @Override
  238. public long open(DataSpec dataSpec) throws HttpDataSourceException {
  239. this.dataSpec = dataSpec;
  240. this.bytesRead = 0;
  241.  
  242. //
  243. if (cacheFile != null) {
  244. if (cacheFile.exists()) {
  245. try {
  246. cacheInputStream = new FileInputStream(cacheFile);
  247. } catch (FileNotFoundException e) {
  248. e.printStackTrace(); //Not going to happen
  249. }
  250. return cacheFile.length();
  251. } else {
  252. try {
  253. cacheOutputStream = new FileOutputStream(cacheFile);
  254. } catch (FileNotFoundException e) {
  255. e.printStackTrace();
  256. }
  257. }
  258. }
  259.  
  260. try {
  261. connection = makeConnection(dataSpec);
  262. } catch (IOException e) {
  263. throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
  264. dataSpec);
  265. }
  266.  
  267. // Check for a valid response code.
  268. int responseCode;
  269. try {
  270. responseCode = connection.getResponseCode();
  271. } catch (IOException e) {
  272. throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
  273. dataSpec);
  274. }
  275. if (responseCode < 200 || responseCode > 299) {
  276. Map<String, List<String>> headers = connection.getHeaderFields();
  277. closeConnection();
  278. throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
  279. }
  280.  
  281. // Check for a valid content type.
  282. String contentType = connection.getContentType();
  283. if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
  284. closeConnection();
  285. throw new InvalidContentTypeException(contentType, dataSpec);
  286. }
  287.  
  288. long contentLength = getContentLength(connection);
  289. dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;
  290.  
  291. if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED
  292. && contentLength != dataSpec.length) {
  293. // The DataSpec specified a length and we resolved a length from the response headers, but
  294. // the two lengths do not match.
  295. closeConnection();
  296. throw new HttpDataSourceException(
  297. new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec);
  298. }
  299.  
  300. try {
  301. inputStream = connection.getInputStream();
  302. } catch (IOException e) {
  303. closeConnection();
  304. throw new HttpDataSourceException(e, dataSpec);
  305. }
  306.  
  307. opened = true;
  308. if (listener != null) {
  309. listener.onTransferStart();
  310. }
  311.  
  312. return dataLength;
  313. }
  314.  
  315. @Override
  316. public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
  317. int read = 0;
  318. if (cacheInputStream != null) { //Means cache file exists and we should read data
  319. try {
  320. read = cacheInputStream.read(buffer, offset, readLength);
  321. } catch (IOException e) {
  322. e.printStackTrace(); //TODO: throw CacheException or smth
  323. }
  324. } else {
  325. try {
  326. read = inputStream.read(buffer, offset, readLength);
  327. } catch (IOException e) {
  328. throw new HttpDataSourceException(e, dataSpec);
  329. }
  330. if (cacheOutputStream != null) {
  331. try {
  332. cacheOutputStream.write(buffer, offset, readLength);
  333. } catch (IOException e) {
  334. e.printStackTrace();
  335. }
  336. }
  337. }
  338.  
  339. if (read > 0) {
  340. bytesRead += read;
  341. if (listener != null) {
  342. listener.onBytesTransferred(read);
  343. }
  344. } else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) {
  345. // Check for cases where the server closed the connection having not sent the correct amount
  346. // of data. We can only do this if we know the length of the data we were expecting.
  347. try {
  348. if (cacheOutputStream != null) {
  349. cacheOutputStream.close();
  350. cacheOutputStream = null;
  351. }
  352. } catch (IOException e) {
  353. e.printStackTrace();
  354. }
  355. cacheFile.delete();
  356. throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
  357. dataSpec);
  358. }
  359.  
  360. return read;
  361. }
  362.  
  363. @Override
  364. public void close() throws HttpDataSourceException {
  365. try {
  366. if (cacheInputStream != null)
  367. cacheInputStream.close();
  368. if (cacheOutputStream != null)
  369. cacheOutputStream.close();
  370. } catch (IOException e) {
  371. e.printStackTrace();
  372. }
  373. cacheInputStream = null;
  374. cacheOutputStream = null;
  375.  
  376. try {
  377. if (inputStream != null) {
  378. try {
  379. inputStream.close();
  380. } catch (IOException e) {
  381. throw new HttpDataSourceException(e, dataSpec);
  382. }
  383. inputStream = null;
  384. }
  385. } finally {
  386. if (opened) {
  387. opened = false;
  388. if (listener != null) {
  389. listener.onTransferEnd();
  390. }
  391. closeConnection();
  392. }
  393. }
  394. }
  395.  
  396. private void closeConnection() {
  397. if (connection != null) {
  398. connection.disconnect();
  399. connection = null;
  400. }
  401. }
  402.  
  403. /**
  404. * Returns the current connection, or null if the source is not currently opened.
  405. *
  406. * @return The current open connection, or null.
  407. */
  408. protected final HttpURLConnection getConnection() {
  409. return connection;
  410. }
  411.  
  412. /**
  413. * Returns the number of bytes that have been read since the most recent call to
  414. * {@link #open(DataSpec)}.
  415. *
  416. * @return The number of bytes read.
  417. */
  418. protected final long bytesRead() {
  419. return bytesRead;
  420. }
  421.  
  422. /**
  423. * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
  424. * <p/>
  425. * If the total length of the data being read is known, then this length minus {@code bytesRead()}
  426. * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned.
  427. *
  428. * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
  429. */
  430. protected final long bytesRemaining() {
  431. return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead;
  432. }
  433.  
  434. private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
  435. URL url = new URL(dataSpec.uri.toString());
  436. HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  437. connection.setConnectTimeout(connectTimeoutMillis);
  438. connection.setReadTimeout(readTimeoutMillis);
  439. connection.setDoOutput(false);
  440. synchronized (requestProperties) {
  441. for (HashMap.Entry<String, String> property : requestProperties.entrySet()) {
  442. connection.setRequestProperty(property.getKey(), property.getValue());
  443. }
  444. }
  445. connection.setRequestProperty("Accept-Encoding", "deflate");
  446. connection.setRequestProperty("User-Agent", userAgent);
  447. connection.setRequestProperty("Range", buildRangeHeader(dataSpec));
  448. connection.connect();
  449. return connection;
  450. }
  451.  
  452. private String buildRangeHeader(DataSpec dataSpec) {
  453. String rangeRequest = "bytes=" + dataSpec.position + "-";
  454. if (dataSpec.length != C.LENGTH_UNBOUNDED) {
  455. rangeRequest += (dataSpec.position + dataSpec.length - 1);
  456. }
  457. return rangeRequest;
  458. }
  459.  
  460. private long getContentLength(HttpURLConnection connection) {
  461. long contentLength = C.LENGTH_UNBOUNDED;
  462. String contentLengthHeader = connection.getHeaderField("Content-Length");
  463. if (!TextUtils.isEmpty(contentLengthHeader)) {
  464. try {
  465. contentLength = Long.parseLong(contentLengthHeader);
  466. } catch (NumberFormatException e) {
  467. Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
  468. }
  469. }
  470. String contentRangeHeader = connection.getHeaderField("Content-Range");
  471. if (!TextUtils.isEmpty(contentRangeHeader)) {
  472. Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
  473. if (matcher.find()) {
  474. try {
  475. long contentLengthFromRange =
  476. Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
  477. if (contentLength < 0) {
  478. // Some proxy servers strip the Content-Length header. Fall back to the length
  479. // calculated here in this case.
  480. contentLength = contentLengthFromRange;
  481. } else if (contentLength != contentLengthFromRange) {
  482. // If there is a discrepancy between the Content-Length and Content-Range headers,
  483. // assume the one with the larger value is correct. We have seen cases where carrier
  484. // change one of them to reduce the size of a request, but it is unlikely anybody would
  485. // increase it.
  486. Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader +
  487. "]");
  488. contentLength = Math.max(contentLength, contentLengthFromRange);
  489. }
  490. } catch (NumberFormatException e) {
  491. Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
  492. }
  493. }
  494. }
  495. return contentLength;
  496. }
  497.  
  498. public File getCacheFile() {
  499. return cacheFile;
  500. }
  501.  
  502. public void setCacheFile(File cacheFile) {
  503. this.cacheFile = cacheFile;
  504. }
  505. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement