SSLProtocolException when reading https responses on Android 2.3.3 devices - android

I'm facing a problem with a bug (Issue 16121) that was introduced in Gingerbread 2.3.3 and fixed with 2.3.4.
Reading the response of a https request throws an SSLProtocolException after reading ~40kB from the inputstream. The problem is described at Issue 16121. At the bottom of the page is a android project that reveals the bug. The bug report originates from user 'Alex' on stackoverflow (question).
I have an app in the market that sends many different https requests. Most of them need to receive up to 200kB. I cannot change anything on the server side. I use the DefaultHttpClient to send https requests.
How can I efficiently solve that problem?
Using a different https api? Which https api?

Here are some sample methods, as noted in the comment, I am using CharTerminatedInputStream, under the Apache license. The URL to see it is in the comment as well.
private static final int BUFFER_SIZE = 4096;
private static final char[] TERMINATOR = new char[]{'\r', '\n', '\r', '\n'};
/**
* Simple SSL connect example to avoid Issue 15356 on Android 2.3.3
*
* #param host The host/server name
* #param port The TCP port to use (443 is default for HTTP over SSL)
* #param file The file you are requesting (/path/to/file/on/server.doc)
* #param fileOut Your <code>OutputStream</code> for the file you are writing to
* #throws Exception If any error occurs - obviously should be improved for your implementation
*/
private static void downloadFileOverSSL(String host, int port, String file, OutputStream fileOut) throws Exception {
PrintWriter socketOut = null;
InputStream socketIn = null;
try {
// create a socket to talk to the server on
SocketFactory factory = SSLSocketFactory.getDefault();
Socket socket = factory.createSocket(host, port);
// we'll use this to send our request to the server
socketOut = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
//This is what Java was sending using URLConnection, and it works here too...
// You can always change this to something both your app and server will understand depending how it is setup
// This is the least you need in the request:
/*String requestStr = "GET " + file + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"\r\n";*/
String requestStr = "GET " + file + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"User-Agent: Java/1.6.0_25\r\n" +
"Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2" +
"Connection: keep-alive\r\n" +
"\r\n";
//Log.i(getLogTag(), "Request being sent: `" + requestStr + "\"");
// send the request to the server
socketOut.print(requestStr);
socketOut.flush();
// this reads the server's response
socketIn = socket.getInputStream();
/*
Write the results into our local file's output stream
*/
// This is the tricky part, the raw socket returns the HTTP 200 response and headers.
// This can probably be optimized, but it's just reading through until it finds \r\n\r\n
// You can use something like CharTerminatedInputStream
// (ref: http://www.java2s.com/Tutorial/Java/0180__File/AnInputStreamclassthatterminatesthestreamwhenitencountersaparticularbytesequence.htm)
CharTerminatedInputStream charTermInput = new CharTerminatedInputStream(socketIn, TERMINATOR);
while (charTermInput.read() != -1) {
// -1 indicates a match was made, IOException or ProtocolException thrown if match not made by end of stream
}
int numBytesRead;
byte[] buffer = new byte[BUFFER_SIZE];
while ((numBytesRead = socketIn.read(buffer, 0, BUFFER_SIZE)) != -1) {
fileOut.write(buffer, 0, numBytesRead);
//Log.d(getLogTag(), "Reading data [" + numBytesRead + "]: " + new String(buffer, 0, numBytesRead));
}
fileOut.flush();
} finally {
safeClose(socketOut);
safeClose(socketIn);
safeClose(fileOut);
}
}
private static void safeClose(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ioe) {
//Log.w(getLogTag(), "Failed to close stream", ioe);
}
}
}

Related

Android connect to Bluetooth via Hands Free Protocol

I would like to create an application which can connect to a Bluetooth Headset via the Hands Free Protocol (HFP). I followed the Android example and have now a BluetoothSocket with its Input and OutputStream. Below you see my read and write methods (read method is executed by another Thread)
public void read() {
while (true) {
Log.d("ME", "Waiting for data");
try { // read until Exception is thrown
numBytes = inStream.read(dataBuffer);
String str = new String(dataBuffer,0,numBytes);
msgHandler.obtainMessage(numBytes, str).sendToTarget();
} catch (Exception e) {
Log.d("ME", "Input stream was disconnected", e);
break; // BluetoothDevice was disconnected => Exit
}
}
}
public void write(byte[] bytes) {
try {
outStream.write(bytes);
outStream.flush();
Log.e("ME", "Wrote: " + new String(bytes));
} catch (IOException e) {
Log.e("ME", "Error occurred when sending data", e);
}
}
When the connection is opened the Bluetooth headset sends AT+BRSF=191 over the InputStream. I tried to response with +BRSF:20\r but here is my problem. After that the device does not send any other data over the InputStream. It does not come to an Exception - it's more like the device does not know how to responde to my message. Do I send the wrong data? I have all the information from here: (HF = Hands-Free Unit AG = Audio Gateway)
Do you have any ideas what I did wrong? Have I missed something?
EDIT: These are my write calls:
write("+BRSF: 191\r");
write("OK\r");
You were missing the OK response. According to this document, the OK-code consists of a windows-style newline (CR LF), the literal OK and then another newline.
Do note that other commands are terminated by a carriage return only. For more information on the hands-free protocol, you can refer to that very document you linked in your post.
Example code:
public static final String OK = statusCode("OK")
public static final String ERROR = statusCode("ERROR")
public static String statusCode(String code) {
return "\r\n" + code + "\r\n";
}
public static String command(String cmd) {
return cmd + "\r";
}
Now you can use OK and ERROR in your code as constants, and you can use the statusCode method for other status codes.

Could'nt download file on Android while on PC it downloads ok (amazon)

Got an weird issue. A file with Url: https://s3.amazonaws.com/myappdata/msg/171401089927.mp3 (not available any more) downloads ok on PC and its mp3 file. But when I try to DL it on Android FOA Im getting content-type "application/xml" instead of "audio/mpeg" and when downloading starts I'm getting:
05-30 12:13:44.478: E/PlayerService(28023): java.io.FileNotFoundException: https://s3.amazonaws.com/myappdata/msg/171401089927.mp3
05-30 12:13:44.478: E/PlayerService(28023): at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
05-30 12:13:44.478: E/PlayerService(28023): at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:270)
The code used to DL:
/**
* Download the url stream to a temporary location
*/
public void downloadAudioIncrement(String mediaUrl) throws IOException {
Log.i(TAG, "downloadAudioIncrement(): mediaUrl: "+mediaUrl+"\ncacheDir: "+cacheDir);
URL url = null;
try {
url = new URL(mediaUrl);
} catch (MalformedURLException e) {
e.printStackTrace();
throw new IOException("Unable to create InputStream for mediaUrl:" + mediaUrl);
}
// this file will represent whole downloaded song
mp3FileDownloaded = new File(cacheDir, mp3FileName);
if (!mp3FileDownloaded.exists())
//FileUtils.makeDirsForFile(mp3FileDownloaded);
try{
mp3FileDownloaded.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (!mp3FileDownloaded.canWrite())
throw new IOException("Can't open temporary file for writing");
HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection();
urlConnection.setReadTimeout(1000 * 20);
urlConnection.setConnectTimeout(1000 * 5);
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
int mp3BytesSize = urlConnection.getContentLength();
// final String
// contentLengthStr=urlConnection.getHeaderField("content-length");
String ctype = urlConnection.getContentType();
if (ctype == null) {
ctype = "";
} else {
ctype = ctype.toLowerCase(Locale.US);
}
// See if we can handle this type
Log.i(TAG, "Content Type: " + ctype);
if ( ctype.contains("audio/mpeg") || TextUtils.isEmpty(ctype) ) {
String temp = urlConnection.getHeaderField(BITRATE_HEADER);
Log.i(TAG, "Bitrate: " + temp);
// if (temp != null){
// bitrate = new Integer(temp).intValue();
// }
} else {
Log.e(TAG, UNSUPPORTED_AUDIO_TYPE+": " + ctype);
// throw new IOException(UNSUPPORTED_AUDIO_TYPE+": " + ctype);
// Log.e(TAG, "Or we could not connect to audio");
// stop();
// return;
}
final InputStream stream = new BufferedInputStream(urlConnection.getInputStream(),8192);
...
Right at the last shown line of code (instantiating the InputStream stream) the mentioned IOExeption raised. There are other mp3 files exists at same location and they are downloading with no any issue but only mentioned above url fails.What could be wrong here?
UPDATE
Its appears that this issue happens on HTC Rezound with AOS 4.0.4. On other device, with AOS 2.3.5 everything works ok.
seems like the line
urlConnection.setDoOutput(true);
was the source of issue since I don't upload any data. Everything works fine since I'd comment it. Also these FileNotFoundException while getting the InputStream object from HttpURLConnection and Android HttpUrlConnection getInputStream throws NullPointerException threads might be helpfull.

Unexpected status line: ICY 200 OK for URL openStream() method?

According to changes for kitakt 4.4 there were some problems with playing shoutcast streams (those returning "ICY" instead of "HTTP/1.x" response).
So solution for kitkat was to reregister "icy" protocol prefix in JVM once before we opened a stream by this:
try {
java.net.URL.setURLStreamHandlerFactory( new java.net.URLStreamHandlerFactory(){
public java.net.URLStreamHandler createURLStreamHandler( String protocol ) {
Log.d( LOG, "Asking for stream handler for protocol: '" + protocol + "'" );
if ("icy".equals( protocol )) return new com.spoledge.aacdecoder.IcyURLStreamHandler();
return null;
}
});
}
catch (Throwable t) {
Log.w( LOG, "Cannot set the ICY URLStreamHandler - maybe already set ? - " + t );
}
I have problem with open audio stream to make it register. After I call url.opnestream(stream) I got exception:
java.net.ProtocolException: Unexpected status line: ICY 200 OK
How could I fix it?
Here is sample of registering audio, so far what I did..
try {
URL url = null;
url = new URL(u);
inputStream = url.openStream();
startTime = System.currentTimeMillis();
Boolean isSDPresent = android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED);
String fileName = File.separator + "radio_" + "recording_" + channelMetadata.replaceAll("\\W", "") + System.currentTimeMillis();
if(isSDPresent)
{
outputSource = Environment.getExternalStorageDirectory() + fileName;
}
else
{
outputSource = Environment.getDataDirectory() + fileName;
}
if(contentType.equals("audio/aacp"))
fileOutputStream = new FileOutputStream(outputSource + ".acc");
else if(contentType.equals("audio/mpeg"))
fileOutputStream = new FileOutputStream(outputSource + ".mp3");
else
fileOutputStream = new FileOutputStream(outputSource + ".nieznany_format");
int bytesRead = 0;
int bytes;
while (((bytes = inputStream.read()) != -1) && isRecording) {
fileOutputStream.write(bytes);
bytesRead++;
stopTime = System.currentTimeMillis();
long seconds = (Math.abs(startTime-stopTime));
int minutes = 1000 * 60 * 60;
if(minutes<=seconds)
{
Log.d("xxx", "recording task exceed stopped");
break;
}
}
inputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
isRecording = false;
}
isRecording = false;
return null;
Various changes were made for Android 4.4, and it seems that the non-standard ShoutCast ICY header is not supported by Android.
It seems though that the good people of OkHttp fixed the issue (issue 1 and issue 2) already a few days ago.
What you can do, is simply use OkHttp lib directly in your application and by that you'll use the newer OkHttp version (the one with the fix) and not the one shipped with Android (where you'll have to wait for an OS update).
This will also fix it for your application running on other devices that might suffer from that issue.
The answer of Assaf Gamliel worked for me. For those not familiar with this, you have to download the last .jar from okHttp and the .jar from the dependency okio . Add them to your libs directory and connect like this :
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(mediaUrl)
.build();
Response response = client.newCall(request).execute();
InputStream stream = response.body().byteStream();
The easiest fix ever.
which is 100% working for me,
for shoutcast , change your URL ex. "http://192.168.1.1:9292" to "icy://192.168.1.1:9292"
and you'll be fine.

Android TCP app hanging on inStream.readline()

This is a continuation of this question because it my orginal question was answered, but it did not solve the bug.
Question:
How do I fix the code hanging on this line inStream.readline()
My Intent:
This is in a thread that will loop through checking if there is an outMessage, if there is, it will send the message.
Next it will check it if there is anything in the in-stream, if there is, it will send it to the handler in my main activity.
Lastly, it will sleep for 1 second, then check again.
This should allow me to read/write multiple times without needing to close and open the socket.
Problem:
It is reading and writing better, but still not working properly
What is happening now:
If outMessage is initialized with a value, upon connection with the server, the socket:
writes and flushes the value (server receives & responds)
updates value of outMessage (to null or to "x" depending on how i have it hard-coded)
reads and shows the response message from the server
re-enters for the next loop
IF i set outMessage to null, it skips over that if statements correctly then hangs; otherwise, if i set outMessage to a string (lets say "x"), it goes through the whole if statement, then hangs.
The code it hangs on is either of the inStream.readline() calls (I currently have one commented out).
Additional info:
- once connected, I can type in the "send" box, submit (updates the outMessage value), then disconnect. Upon re-connecting, it will read the value and do the sequence again until it get stuck on that same line.
Changes since the referenced question:
- Made outMessage and connectionStatus both 'volatile'
- added end-of-line delimiters in neccesary places.
Code:
public void run() {
while (connectionStatus != TCP_SOCKET_STATUS_CONNECTED) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while (connectionStatus == TCP_SOCKET_STATUS_CONNECTED) {
try {
if (outMessage != null){
OutStream.writeBytes(outMessage + "\n");
OutStream.flush();
sendMessageToAllUI(0, MAINACTIVITY_SET_TEXT_STATE, "appendText" , "OUT TO SERVER: " + outMessage);
outMessage = "x";
}
Thread.sleep(100);
// if (InStream.readLine().length() > 0) {
String modifiedSentence = InStream.readLine();
sendMessageToAllUI(0, MAINACTIVITY_SET_TEXT_STATE, "appendText" , "IN FROM SERVER: " + modifiedSentence);
// }
Thread.sleep(1000);
} catch (IOException e) {
connectionLost();
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
The thread that makes the socket:
public void run() {
setName("AttemptConnectionThread");
connectionStatus = TCP_SOCKET_STATUS_CONNECTING;
try {
SocketAddress sockaddr = new InetSocketAddress(serverIP, port);
tempSocketClient = new Socket(); // Create an unbound socket
// This method will block no more than timeoutMs. If the timeout occurs, SocketTimeoutException is thrown.
tempSocketClient.connect(sockaddr, timeoutMs);
OutStream = new DataOutputStream(tempSocketClient.getOutputStream());
InStream = new BufferedReader(new InputStreamReader(tempSocketClient.getInputStream()));
socketClient = tempSocketClient;
socketClient.setTcpNoDelay(true);
connected();
} catch (UnknownHostException e) {
connectionFailed();
} catch (SocketTimeoutException e) {
connectionFailed();
} catch (IOException e) {
// Close the socket
try {
tempSocketClient.close();
} catch (IOException e2) {
}
connectionFailed();
return;
}
}
Server:
public static void main(String[] args) throws IOException {
String clientSentence;
String capitalizedSentence;
try {
ServerSocket welcomeSocket = new ServerSocket(8888);
SERVERIP = getLocalIpAddress();
System.out.println("Connected and waiting for client input!\n Listening on IP: " + SERVERIP +"\n\n");
Socket connectionSocket = welcomeSocket.accept();
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
while(true)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clientSentence = inFromClient.readLine();
System.out.println("clientSentance == " + clientSentence);
String ip = connectionSocket.getInetAddress().toString().substring(1);
if(clientSentence != null)
{
System.out.println("In from client ("+ip+")("+ System.currentTimeMillis() +"): "+clientSentence);
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence + '\n');
System.out.println("Out to client ("+ip+"): "+capitalizedSentence);
}
}
} catch (IOException e) {
//if server is already running, it will not open new port but instead re-print the open ports information
SERVERIP = getLocalIpAddress();
System.out.println("Connected and waiting for client input!\n");
System.out.println("Listening on IP: " + SERVERIP +"\n\n");
}
}
Thanks in advance!
Edits:
added the server code after updating
I tried messing around with setting the SoTimout for the socket but took that back out
Your server is specifically designed to receive exactly one line from a client and send exactly one line back. Look at the code:
while (true) {
Socket connectionSocket = welcomeSocket.accept();
BufferedReader inFromClient = new BufferedReader(
new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(
connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine();
String ip = connectionSocket.getInetAddress().toString()
.substring(1);
System.out.println("In from client (" + ip + "): "
+ clientSentence);
if (clientSentence != null) {
capitalizedSentence = clientSentence.toUpperCase() + '\n';
System.out.println("Out to client (" + ip + "): "
+ capitalizedSentence);
outToClient.writeBytes(capitalizedSentence + "\n");
}
Notice that inside the loop it accepts a new connection, reads exactly one line, and then writes exactly one line. It doesn't close the connection. It doesn't sanely end the conversation. It just stops reading.
A client that worked with this server would have to connect, send exactly one line, read exactly one line back, and then the client would have to close the connection. Your client doesn't do that. Why? Because you had no idea that's what you had to do. Why? Because you had no design ... no plan.
So that's your specific issue. But please, let me urge you to take a huge step back and totally change your approach. Before you write a single line of code, please actually design and specify a protocol at the byte level. The protocol should say what data is sent, how messages are delimited, who sends when, who closes the connection, and so on.
Otherwise, it's impossible to debug your code. Looking at the server code above, is it correct? Well, who knows. Because it's unclear what it's supposed to do. When you wrote the client, you assumed the server behaved one way. Was that assumption valid? Is the server broken? Who knows, because there's no specification of what the server is supposed to do.
You need to check if there is data available:
if (InStream.available > 0) {
String modifiedSentence = InStream.readLine();
sendMessageToAllUI(0, MAINACTIVITY_SET_TEXT_STATE, "appendText" , "IN FROM SERVER: " + modifiedSentence);
}
But to be honest, even that is not ideal because you have no gurantee that the eond-of-line will have been received. If the server sends a few bytes but never sends the end-of-line then you will still be blocking forever. Production socket code should never rely on readLine but instead read into a buffer and check that buffer for end-of-line (or whatever criteria your protocol needs).
Didn't read closely enough, I thought InStream was an InputStream instance. InputStream has available. InputStreamReader has ready (which in turn calls InputStream.available. As long as you keep a refernce to either of these then you can see if data is available to be read.

Android ServerSocket programming with jCIFS streaming files

I've got a bit of an issue and I've been asking regarding it quite a few times, but I think I'm one step closer now, so hopefully someone can help me with the rest.
My previous questions:
Connect to NAS device from Android
How to open files in Android with default viewer using jCIFS
Put simply - I want to create an application that:
Can connect to a NAS device using jCIFS
Is capable of launching files in the default viewer - i.e. a video in the video player
The first part is relatively easy and I've already done that, but the second part is what's troubling me and what I've asked about a few times before. I think I've made some progress though.
I think I need to use a ServerSocket in my application to somehow create a bridge between the NAS and the application that's playing the content. I'm thinking this could be done using a Service. The files from the NAS device can be accessed as a FileInputStream.
There are plenty of applications on Market (i.e. ES File Explorer) that are capable of doing this without root access, so I know it's possible - at the moment I just don't know how.
I've been looking at Logcat while using some of the aforementioned applications, and they all seem to be creating a local server and then launch a video Intent from that server. How can this be achieved?
Basic answer is to use SmbFileInputStream to get InputStream You probably use this.
Now the tricky part is how to offer InputStream to other apps.
One possible approach, how many apps provide streaming of any InputStream to other apps on device, is to use http: URL scheme, and tunel your stream over http.
Then apps that can handle http URLs can open and use your data.
For this you have to make some kind of http server, which sounds difficult, but actually is achievable task. Good source to start with is nanohttpd library which is just one java source, originally used to list files in dirs, but you can adapt it to stream your InputStream over http. That's what I did with success.
Your url would look like http:// localhost:12345 where 12345 is port on which your server listens for requests. This port may be obtained from ServerSocket.getLocalPort(). Then give this URL to some app and your server waits for connection and sends data.
A note about http streaming: some apps (e.g. video players) like seekable http streams (http Range header). Since you can get also SmbRandomAccessFile, you can make your tiny server to provide any part of data in file. Android's built-in video player needs such seekable http stream in order to allow seeking in video file, otherwise it gives "Video can't be played" error. Your server must be ready to handle disconnects and multiple connects with different Range values.
Basic tasks of http server:
create ServerSocket
create Thread waiting for connection (Socket accept = serverSocket.accept()), one thread may be ok since you'd handle single client at a time
read http request (socket.getInputStream()), mainly check GET method and Range header)
send headers, mainly Content-Type, Content-Length, Accept-Ranges, Content-Range headers
send actual binary data, which is plain copying of InputStream (file) to OutputStream (socket)
handle disconnects, errors, exceptions
Good luck in implementation.
EDIT:
Here's my class that does the thing. It references some non-present classes for file, which should be trivial for you to replace by your file class.
/**
* This is simple HTTP local server for streaming InputStream to apps which are capable to read data from url.
* Random access input stream is optionally supported, depending if file can be opened in this mode.
*/
public class StreamOverHttp{
private static final boolean debug = false;
private final Browser.FileEntry file;
private final String fileMimeType;
private final ServerSocket serverSocket;
private Thread mainThread;
/**
* Some HTTP response status codes
*/
private static final String
HTTP_BADREQUEST = "400 Bad Request",
HTTP_416 = "416 Range not satisfiable",
HTTP_INTERNALERROR = "500 Internal Server Error";
public StreamOverHttp(Browser.FileEntry f, String forceMimeType) throws IOException{
file = f;
fileMimeType = forceMimeType!=null ? forceMimeType : file.mimeType;
serverSocket = new ServerSocket(0);
mainThread = new Thread(new Runnable(){
#Override
public void run(){
try{
while(true) {
Socket accept = serverSocket.accept();
new HttpSession(accept);
}
}catch(IOException e){
e.printStackTrace();
}
}
});
mainThread.setName("Stream over HTTP");
mainThread.setDaemon(true);
mainThread.start();
}
private class HttpSession implements Runnable{
private boolean canSeek;
private InputStream is;
private final Socket socket;
HttpSession(Socket s){
socket = s;
BrowserUtils.LOGRUN("Stream over localhost: serving request on "+s.getInetAddress());
Thread t = new Thread(this, "Http response");
t.setDaemon(true);
t.start();
}
#Override
public void run(){
try{
openInputStream();
handleResponse(socket);
}catch(IOException e){
e.printStackTrace();
}finally {
if(is!=null) {
try{
is.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
private void openInputStream() throws IOException{
// openRandomAccessInputStream must return RandomAccessInputStream if file is ssekable, null otherwise
is = openRandomAccessInputStream(file);
if(is!=null)
canSeek = true;
else
is = openInputStream(file, 0);
}
private void handleResponse(Socket socket){
try{
InputStream inS = socket.getInputStream();
if(inS == null)
return;
byte[] buf = new byte[8192];
int rlen = inS.read(buf, 0, buf.length);
if(rlen <= 0)
return;
// Create a BufferedReader for parsing the header.
ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
Properties pre = new Properties();
// Decode the header into params and header java properties
if(!decodeHeader(socket, hin, pre))
return;
String range = pre.getProperty("range");
Properties headers = new Properties();
if(file.fileSize!=-1)
headers.put("Content-Length", String.valueOf(file.fileSize));
headers.put("Accept-Ranges", canSeek ? "bytes" : "none");
int sendCount;
String status;
if(range==null || !canSeek) {
status = "200 OK";
sendCount = (int)file.fileSize;
}else {
if(!range.startsWith("bytes=")){
sendError(socket, HTTP_416, null);
return;
}
if(debug)
BrowserUtils.LOGRUN(range);
range = range.substring(6);
long startFrom = 0, endAt = -1;
int minus = range.indexOf('-');
if(minus > 0){
try{
String startR = range.substring(0, minus);
startFrom = Long.parseLong(startR);
String endR = range.substring(minus + 1);
endAt = Long.parseLong(endR);
}catch(NumberFormatException nfe){
}
}
if(startFrom >= file.fileSize){
sendError(socket, HTTP_416, null);
inS.close();
return;
}
if(endAt < 0)
endAt = file.fileSize - 1;
sendCount = (int)(endAt - startFrom + 1);
if(sendCount < 0)
sendCount = 0;
status = "206 Partial Content";
((RandomAccessInputStream)is).seek(startFrom);
headers.put("Content-Length", "" + sendCount);
String rangeSpec = "bytes " + startFrom + "-" + endAt + "/" + file.fileSize;
headers.put("Content-Range", rangeSpec);
}
sendResponse(socket, status, fileMimeType, headers, is, sendCount, buf, null);
inS.close();
if(debug)
BrowserUtils.LOGRUN("Http stream finished");
}catch(IOException ioe){
if(debug)
ioe.printStackTrace();
try{
sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
}catch(Throwable t){
}
}catch(InterruptedException ie){
// thrown by sendError, ignore and exit the thread
if(debug)
ie.printStackTrace();
}
}
private boolean decodeHeader(Socket socket, BufferedReader in, Properties pre) throws InterruptedException{
try{
// Read the request line
String inLine = in.readLine();
if(inLine == null)
return false;
StringTokenizer st = new StringTokenizer(inLine);
if(!st.hasMoreTokens())
sendError(socket, HTTP_BADREQUEST, "Syntax error");
String method = st.nextToken();
if(!method.equals("GET"))
return false;
if(!st.hasMoreTokens())
sendError(socket, HTTP_BADREQUEST, "Missing URI");
while(true) {
String line = in.readLine();
if(line==null)
break;
// if(debug && line.length()>0) BrowserUtils.LOGRUN(line);
int p = line.indexOf(':');
if(p<0)
continue;
final String atr = line.substring(0, p).trim().toLowerCase();
final String val = line.substring(p + 1).trim();
pre.put(atr, val);
}
}catch(IOException ioe){
sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
}
return true;
}
}
/**
* #param fileName is display name appended to Uri, not really used (may be null), but client may display it as file name.
* #return Uri where this stream listens and servers.
*/
public Uri getUri(String fileName){
int port = serverSocket.getLocalPort();
String url = "http://localhost:"+port;
if(fileName!=null)
url += '/'+URLEncoder.encode(fileName);
return Uri.parse(url);
}
public void close(){
BrowserUtils.LOGRUN("Closing stream over http");
try{
serverSocket.close();
mainThread.join();
}catch(Exception e){
e.printStackTrace();
}
}
/**
* Returns an error message as a HTTP response and
* throws InterruptedException to stop further request processing.
*/
private static void sendError(Socket socket, String status, String msg) throws InterruptedException{
sendResponse(socket, status, "text/plain", null, null, 0, null, msg);
throw new InterruptedException();
}
private static void copyStream(InputStream in, OutputStream out, byte[] tmpBuf, long maxSize) throws IOException{
while(maxSize>0){
int count = (int)Math.min(maxSize, tmpBuf.length);
count = in.read(tmpBuf, 0, count);
if(count<0)
break;
out.write(tmpBuf, 0, count);
maxSize -= count;
}
}
/**
* Sends given response to the socket, and closes the socket.
*/
private static void sendResponse(Socket socket, String status, String mimeType, Properties header, InputStream isInput, int sendCount, byte[] buf, String errMsg){
try{
OutputStream out = socket.getOutputStream();
PrintWriter pw = new PrintWriter(out);
{
String retLine = "HTTP/1.0 " + status + " \r\n";
pw.print(retLine);
}
if(mimeType!=null) {
String mT = "Content-Type: " + mimeType + "\r\n";
pw.print(mT);
}
if(header != null){
Enumeration<?> e = header.keys();
while(e.hasMoreElements()){
String key = (String)e.nextElement();
String value = header.getProperty(key);
String l = key + ": " + value + "\r\n";
// if(debug) BrowserUtils.LOGRUN(l);
pw.print(l);
}
}
pw.print("\r\n");
pw.flush();
if(isInput!=null)
copyStream(isInput, out, buf, sendCount);
else if(errMsg!=null) {
pw.print(errMsg);
pw.flush();
}
out.flush();
out.close();
}catch(IOException e){
if(debug)
BrowserUtils.LOGRUN(e.getMessage());
}finally {
try{
socket.close();
}catch(Throwable t){
}
}
}
}
/**
* Seekable InputStream.
* Abstract, you must add implementation for your purpose.
*/
abstract class RandomAccessInputStream extends InputStream{
/**
* #return total length of stream (file)
*/
abstract long length();
/**
* Seek within stream for next read-ing.
*/
abstract void seek(long offset) throws IOException;
#Override
public int read() throws IOException{
byte[] b = new byte[1];
read(b);
return b[0]&0xff;
}
}
In Samsung S5 (Android version 5.1.1), I faced a problem of range request starting from a value greater than the file size and I solved it by setting status = "200 OK" as below:
if (startFrom >= contentLength) {
// when you receive a request from MediaPlayer that does not contain Range in the HTTP header , then it is requesting a new stream
// https://code.google.com/p/android/issues/detail?id=3031
status = "200 OK";
}
The remaining headers were left as a fresh request for the stream

Categories

Resources