Java tutorial
// Copyright 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.shonshampain.streamrecorder.util; import android.content.Context; import android.os.Handler; import android.util.Log; import com.shonshampain.streamrecorder.application.StreamRecorderApplication; import com.shonshampain.streamrecorder.events.ProxyAcceptingConnectionsEvent; import com.shonshampain.streamrecorder.events.RipEvent; import com.shonshampain.streamrecorder.events.ThrottleStreamRequest; import com.shonshampain.streamrecorder.helpers.FileHelper; import com.shonshampain.streamrecorder.helpers.NotificationHelper; import com.shonshampain.streamrecorder.managers.StreamManager; import org.apache.http.Header; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpResponseFactory; import org.apache.http.ParseException; import org.apache.http.ProtocolVersion; import org.apache.http.StatusLine; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.ClientConnectionOperator; import org.apache.http.conn.OperatedClientConnection; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.DefaultClientConnection; import org.apache.http.impl.conn.DefaultClientConnectionOperator; import org.apache.http.impl.conn.DefaultResponseParser; import org.apache.http.impl.conn.SingleClientConnManager; import org.apache.http.io.HttpMessageParser; import org.apache.http.io.SessionInputBuffer; import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicLineParser; import org.apache.http.message.ParserCursor; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import org.apache.http.util.CharArrayBuffer; import java.io.BufferedReader; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.StringTokenizer; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import de.greenrobot.event.EventBus; public class StreamProxy implements Runnable { private static final String TAG = "StreamProxy"; private static final boolean DBG = false; private static final boolean DBG_READS = false; private static final boolean DBG_WRITES = false; private static final boolean DBG_META = false; private static final int STREAM_STALLED_TIMEOUT = 10 * 1000; private static final int MAX_RETRIES = 10; private int port = 0; private int notificationId; private static Context context; public int getPort() { return port; } private static boolean isRunning = true; private static ServerSocket socket; private static Thread thread; private String localFile; FileHelper fileHelper; FileOutputStream fos; MetaDataExtractor mde; private StreamProxy() { } private static StreamProxy streamProxy; public static StreamProxy getInstance(Context context) { Logger.logEvent("StreamProxy:getInstance()"); StreamProxy.context = context; if (streamProxy == null) { Logger.logEvent("StreamProxy instance is null, creating new instance"); streamProxy = new StreamProxy(); } if (thread != null) { Logger.logEvent("StreamProxy stale thread, killing existing thread"); stop(); } Logger.logEvent("StreamProxy exiting getInstance()"); return streamProxy; } private void init() { try { socket = new ServerSocket(port, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); socket.setSoTimeout(5000); port = socket.getLocalPort(); Logger.d(DBG, TAG, "port " + port + " obtained"); } catch (IOException e) { Logger.e(TAG, "Error initializing server", e); } } public void start() { Logger.logEvent("StreamProxy:start()"); isRunning = true; init(); EventBus.getDefault().register(this); thread = new Thread(this); thread.start(); Handler handler = new Handler(); Runnable postAcceptingConnectionsRunnable = new Runnable() { @Override public void run() { EventBus.getDefault().post(new ProxyAcceptingConnectionsEvent()); Logger.logEvent("Proxy is accepting connections"); } }; // This is a bit of a hack. // We want to post the event after socket.accept, // but after socket.accept, we're in a wait state // This is good enough. handler.postDelayed(postAcceptingConnectionsRunnable, 1000); } public static void stop() { Logger.logEvent(">> Stopping StreamProxy"); if (socket != null && !socket.isClosed()) { try { socket.close(); socket = null; } catch (IOException ioe) { Logger.e(TAG, "Error closing socket", ioe); } } isRunning = false; if (thread == null) { Logger.e(TAG, "Cannot stop proxy; it has not been started."); throw new IllegalStateException("Cannot stop proxy; it has not been started."); } thread.interrupt(); try { thread.join(5000); } catch (InterruptedException e) { Logger.e(TAG, "Interrupted exception on stop()", e); } thread = null; EventBus.getDefault().unregister(streamProxy); context = null; } private int retryCount; @Override public void run() { Socket client = null; HttpRequest request; Logger.d(DBG, TAG, "running"); while (isRunning) { try { Logger.d(DBG, TAG, "Waiting on a connection"); ExecutorService executor = Executors.newFixedThreadPool(1); Callable<Socket> task = new Callable<Socket>() { @Override public Socket call() throws Exception { return socket.accept(); } }; Future<Socket> future = executor.submit(task); try { client = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS); } catch (TimeoutException to) { startOver(null, null, FailType.SocketAccept, null); continue; } catch (InterruptedException ie) { Logger.e(TAG, "The read operation was interrupted"); } catch (ExecutionException ee) { startOver(null, null, FailType.SocketAccept, null); continue; } if (client == null) { continue; } Logger.d(DBG, TAG, "client connected"); request = readRequest(client); Logger.d(DBG, TAG, "calling process request..."); if (request != null) { processRequest(request, client); } Logger.d(DBG, TAG, "...and we're back"); } catch (SocketTimeoutException e) { Logger.d(DBG, TAG, "Socket timed out"); } catch (IOException e) { Log.e(TAG, "Error connecting to client", e); } } Logger.d(DBG, TAG, "Proxy interrupted. Shutting down."); } private HttpRequest readRequest(Socket client) { HttpRequest request; InputStream is; String firstLine; try { is = client.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192); firstLine = reader.readLine(); } catch (IOException e) { Logger.e(TAG, "Error parsing request", e); return null; } if (firstLine == null) { Logger.d(DBG, TAG, "Proxy client closed connection without a request."); return null; } StringTokenizer st = new StringTokenizer(firstLine); String method = st.nextToken(); String uri = st.nextToken(); Logger.d(DBG, TAG, uri); String realUri = uri.substring(1); Logger.d(DBG, TAG, realUri); request = new BasicHttpRequest(method, realUri); return request; } private HttpResponse download(String url) { DefaultHttpClient seed = new DefaultHttpClient(); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); SingleClientConnManager mgr = new MyClientConnManager(seed.getParams(), registry); final DefaultHttpClient http = new DefaultHttpClient(mgr, seed.getParams()); final HttpGet method = new HttpGet(url); method.addHeader("Icy-MetaData", "1"); HttpResponse response; Logger.d(DBG, TAG, "starting download"); ExecutorService executor = Executors.newFixedThreadPool(1); Callable<HttpResponse> readTask = new Callable<HttpResponse>() { @Override public HttpResponse call() throws Exception { return http.execute(method); } }; Future<HttpResponse> future = executor.submit(readTask); try { response = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS); } catch (TimeoutException to) { return null; } catch (InterruptedException ie) { Logger.e(TAG, "The read operation was interrupted"); return null; } catch (ExecutionException ee) { return null; } Logger.d(DBG, TAG, "downloaded"); return response; } private void processRequest(HttpRequest request, Socket client) throws IllegalStateException, IOException { if (request == null) { return; } String url = request.getRequestLine().getUri(); int mi = 0; Logger.d(DBG, TAG, "processing: " + url); Logger.d(DBG, TAG, "request: " + request.getRequestLine()); for (Header h : request.getAllHeaders()) { Logger.d(DBG, TAG, "header: [" + h.getName() + "] = [" + h.getValue() + "]"); } HttpResponse realResponse = download(url); if (realResponse == null) { startOver(null, client, FailType.CantDownload, null); return; } Logger.d(DBG, TAG, "downloading..."); final InputStream data = realResponse.getEntity().getContent(); StatusLine line = realResponse.getStatusLine(); HttpResponse response = new BasicHttpResponse(line); response.setHeaders(realResponse.getAllHeaders()); Logger.d(DBG, TAG, "reading headers"); StringBuilder httpString = new StringBuilder(); httpString.append(response.getStatusLine().toString()); httpString.append("\n"); for (Header h : response.getAllHeaders()) { if (h.getName().equalsIgnoreCase("Transfer-Encoding")) { /* The KCRW stream specifies chunked encoding in their headers, * however, when we read their stream, the data gets "unchunked". * Therefore, we cannot advertise a chunked encoding unless * we actually re-chunk the data; which we are not. */ httpString.append("Accept-Ranges: bytes\r\n"); httpString.append("Content-Length: 9999999999\r\n"); } else { if (h.getName().equalsIgnoreCase("icy-metaint")) { mi = Integer.parseInt(h.getValue()); Logger.d(DBG, TAG, "Creating new meta data extractor with interval: " + mi); mde = new MetaDataExtractor(mi); } httpString.append(h.getName()).append(": ").append(h.getValue()).append("\r\n"); } } httpString.append("\n"); Logger.d(DBG, TAG, "headers done: [" + httpString + "]"); try { byte[] buffer = httpString.toString().getBytes(); int readBytes; Logger.d(DBG_WRITES, TAG, "writing headers to client"); client.getOutputStream().write(buffer, 0, buffer.length); // Start streaming content. final byte[] buff = new byte[1024 * 50]; boolean endOfStream = false; ExecutorService executor = Executors.newFixedThreadPool(1); while (isRunning && !endOfStream) { Callable<Integer> readTask = new Callable<Integer>() { @Override public Integer call() throws Exception { return data.read(buff, 0, buff.length); } }; Future<Integer> future = executor.submit(readTask); try { readBytes = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS); } catch (TimeoutException to) { startOver(data, client, FailType.Stall, null); return; } catch (InterruptedException ie) { Logger.e(TAG, "The read operation was interrupted"); continue; } endOfStream = readBytes == -1; if (!endOfStream) { Logger.d(DBG_READS, TAG, "Raw read: " + readBytes + " bytes"); if (mi > 0) { readBytes = mde.processBuffer(buff, readBytes); Logger.d(DBG_META, TAG, "Status: " + mde.getStatus() + ", running count: " + mde.getRunningCount()); } Logger.d(DBG_WRITES, TAG, "writing " + readBytes + " bytes of content to client"); client.getOutputStream().write(buff, 0, readBytes); if (fileHelper != null) { Logger.d(DBG, TAG, "writing " + readBytes + " bytes of content to file"); // NotificationHelper.build(context, "StreamRecorder Rip Control", "writing " + readBytes + " bytes", notificationId); fileHelper.write(fos, buff, readBytes); } } } } catch (Exception e) { startOver(data, client, FailType.Unexpected, e); } } private enum FailType { Stall, Unexpected, CantDownload, SocketAccept } private void startOver(InputStream data, Socket client, FailType type, Exception e) throws IOException { if (data != null) { Logger.e(TAG, "Closing data connection"); data.close(); } if (client != null && !client.isClosed()) { Logger.e(TAG, "Closing client connection"); client.close(); } ++retryCount; switch (type) { case Stall: Logger.e(TAG, "Stream has stalled, rethrottling(" + retryCount + ") the data connection"); break; case Unexpected: Logger.e(TAG, "Unexpected exception: " + e.getLocalizedMessage() + " retrying(" + retryCount + ")"); break; case CantDownload: Logger.e(TAG, "Cannot download, retrying(" + retryCount + ")"); break; case SocketAccept: Logger.e(TAG, "Cannot accept on the socket, retrying(" + retryCount + ")"); break; } if (retryCount == MAX_RETRIES) { Logger.e(TAG, "Retry count exceeded, stopping stream service"); StreamManager.getInstance().stop(StreamRecorderApplication.getContext()); } else { EventBus.getDefault().post(new ThrottleStreamRequest()); } } @SuppressWarnings("unused") public void onEventMainThread(RipEvent event) { if (event.playlist != null) { Logger.d(DBG, TAG, "Ripping file: [" + event.playlist.localFile + "]"); Logger.logEvent("Ripping file: [" + event.playlist.localFile + "]"); fileHelper = new FileHelper(); localFile = event.playlist.localFile; fos = fileHelper.open("" + localFile); notificationId = event.playlist._id; NotificationHelper.build(context, "StreamRecorder Rip Control", "Opening file[" + localFile + "]", notificationId); } else { Logger.d(DBG, TAG, "Closing file"); Logger.logEvent("Closing file"); fileHelper.close(fos); fileHelper = null; NotificationHelper.build(context, "StreamRecorder Rip Control", "file[" + localFile + "] closed, size[" + FileHelper.size(localFile) + "]", notificationId); } } private class IcyLineParser extends BasicLineParser { private static final String ICY_PROTOCOL_NAME = "ICY"; private IcyLineParser() { super(); } @Override public boolean hasProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) { boolean superFound = super.hasProtocolVersion(buffer, cursor); if (superFound) { return true; } int index = cursor.getPos(); final int protolength = ICY_PROTOCOL_NAME.length(); if (buffer.length() < protolength) return false; // not long enough for "HTTP/1.1" if (index < 0) { // end of line, no tolerance for trailing whitespace // this works only for single-digit major and minor version index = buffer.length() - protolength; } else if (index == 0) { // beginning of line, tolerate leading whitespace while ((index < buffer.length()) && HTTP.isWhitespace(buffer.charAt(index))) { index++; } } // else within line, don't tolerate whitespace return index + protolength <= buffer.length() && buffer.substring(index, index + protolength).equals(ICY_PROTOCOL_NAME); } @Override public ProtocolVersion parseProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) throws ParseException { if (buffer == null) { throw new IllegalArgumentException("Char array buffer may not be null"); } if (cursor == null) { throw new IllegalArgumentException("Parser cursor may not be null"); } final int protolength = ICY_PROTOCOL_NAME.length(); int indexFrom = cursor.getPos(); int indexTo = cursor.getUpperBound(); skipWhitespace(buffer, cursor); int i = cursor.getPos(); // long enough for "HTTP/1.1"? if (i + protolength + 4 > indexTo) { throw new ParseException("Not a valid protocol version: " + buffer.substring(indexFrom, indexTo)); } // check the protocol name and slash if (!buffer.substring(i, i + protolength).equals(ICY_PROTOCOL_NAME)) { return super.parseProtocolVersion(buffer, cursor); } cursor.updatePos(i + protolength); return createProtocolVersion(1, 0); } @Override public StatusLine parseStatusLine(CharArrayBuffer buffer, ParserCursor cursor) throws ParseException { return super.parseStatusLine(buffer, cursor); } } class MyClientConnection extends DefaultClientConnection { @Override protected HttpMessageParser createResponseParser(final SessionInputBuffer buffer, final HttpResponseFactory responseFactory, final HttpParams params) { return new DefaultResponseParser(buffer, new IcyLineParser(), responseFactory, params); } } class MyClientConnectionOperator extends DefaultClientConnectionOperator { public MyClientConnectionOperator(final SchemeRegistry sr) { super(sr); } @Override public OperatedClientConnection createConnection() { return new MyClientConnection(); } } class MyClientConnManager extends SingleClientConnManager { private MyClientConnManager(HttpParams params, SchemeRegistry schreg) { super(params, schreg); } @Override protected ClientConnectionOperator createConnectionOperator(final SchemeRegistry sr) { return new MyClientConnectionOperator(sr); } } }