Android upload to network drive(samba share) performance issue - android

i have a problem when I upload a picture of 100kb to samba share with JCIFS from my tablet, it takes about 10-20 minutes (before I changed my buffer from 1024 to 20971520 it took almost 6 hours) but it does not give any effect anymore to increase it
it is not the connection issue as i had tested it with ES File where it Uploaded my picture immediately
private class MyCopy extends AsyncTask<String, String, String> {
String z = "";
String username = "", password = "", servername = "", filestocopy = "";
#Override
protected void onPreExecute() {
username = edtusername.getText().toString();
password = edtpassword.getText().toString();
servername = "smb://" + edtservername.getText().toString();
filestocopy = editdir.getText().toString();
}
protected String doInBackground(String... params) {
// String buffer;
// buffer = setingPreferences.getString("buffer", "");
File file = new File(filestocopy);
String filename = file.getName();
NtlmPasswordAuthentication auth1 = new NtlmPasswordAuthentication(
servername, username, password);
try {
SmbFile sfile = new SmbFile(servername + "/" + filename, auth1);
if (!sfile.exists())
sfile.createNewFile();
sfile.connect();
InputStream in = new FileInputStream(file);
SmbFileOutputStream sfos = new SmbFileOutputStream(sfile);
byte[] buf = new byte[20971520]; //(parseInt(buffer))
int len;
while ((len = in.read(buf)) > 0){
sfos.write(buf, 0, len);
}
in.close();
sfos.close();
z = "File copied successfully";
} catch (Exception ex) {
z = z + " " + ex.getMessage().toString();
}
return z;
}
}

The buffer size shouldn't make a noticeable difference, but it definitely shouldn't be 20M. Use something like 4k instead.
Are you sure it's the actual file transfer that is taking so long? There's no reason 100k should take more than a few seconds at the most. Have you tried putting log statements between each step, including before and after the authentication calls, createNewFile(), and connect() to check if those are the bottleneck?
Also, I believe you should be copying bytes while the read length is >= 0 instead of strictly > 0, since -1 signals the end of the stream, not 0.

Did you try
new SmbFile("username:password#server/")
instead of using NTLM? It can also be a DNS issue, so do try
jcifs.Config.setProperty("resolveOrder", "DNS");
If neither works, you might want to try BufferedOutputStream with your SmbFileOutputStream.

Related

Download files via FTP on Android

I need to make a connection with a local FTP protocol between a computer (server) and my Android device (client). This should download files (images, OBJ,...) to be used in the Android Unity app scene. I've used WWW class to create this connection and it works fine in the Unity player run in another computer as client. Once I've exported the same scene as Android apk it didn't work (I'm sure that FTP connection is stable and it works because I'm able to access to the files from the browser). Does anybody know if there is another way or there are problems in my code to use the FTP protocol on Android Unity app? (the client doesn't need any authorisation and the authentication is anonymous) Here is the code I use to download one image inside the scene and render it as a sprite.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.IO;
public class ClientFTP : MonoBehaviour
{
public UnityEngine.UI.Image label;
IEnumerator Start ()
{
// Create the connection and whait until it is established
string url = ("ftp://192.168.10.11/prova.png");
WWW ftpconnection = new WWW (url);
yield return ftpconnection;
// Download the image and render it as a texture
Texture2D tex = new Texture2D (250, 192);
ftpconnection.LoadImageIntoTexture (tex);
// Assign the texture to a new sprite
Sprite s = Sprite.Create (tex, new Rect (0, 0, 250f, 192f), new Vector2 (0.5f, 0.5f), 300);
label.preserveAspect = true;
label.sprite = s;
}
}
Why use FTP if you don't need Credential to access the files? You can just place the files in your server then access them with the WWW or UnityWebRequest API.
To answer your FTP question, WWW is not meant to be used with the FTP protocol. This is what the FtpWebRequest API is used for.
Below is a sample of FtpWebRequest.
private byte[] downloadWithFTP(string ftpUrl, string savePath = "", string userName = "", string password = "")
{
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri(ftpUrl));
//request.Proxy = null;
request.UsePassive = true;
request.UseBinary = true;
request.KeepAlive = true;
//If username or password is NOT null then use Credential
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
request.Credentials = new NetworkCredential(userName, password);
}
request.Method = WebRequestMethods.Ftp.DownloadFile;
//If savePath is NOT null, we want to save the file to path
//If path is null, we just want to return the file as array
if (!string.IsNullOrEmpty(savePath))
{
downloadAndSave(request.GetResponse(), savePath);
return null;
}
else
{
return downloadAsbyteArray(request.GetResponse());
}
}
byte[] downloadAsbyteArray(WebResponse request)
{
using (Stream input = request.GetResponseStream())
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while (input.CanRead && (read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
}
void downloadAndSave(WebResponse request, string savePath)
{
Stream reader = request.GetResponseStream();
//Create Directory if it does not exist
if (!Directory.Exists(Path.GetDirectoryName(savePath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
}
FileStream fileStream = new FileStream(savePath, FileMode.Create);
int bytesRead = 0;
byte[] buffer = new byte[2048];
while (true)
{
bytesRead = reader.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
break;
fileStream.Write(buffer, 0, bytesRead);
}
fileStream.Close();
}
Usage:
Download and save(No Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
downloadWithFTP("ftp://yourUrl.com/yourFile", path);
Download and save(With Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
downloadWithFTP("ftp://yourUrl.com/yourFile", path, "UserName", "Password");
Download Only (No Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
byte[] yourImage = downloadWithFTP("ftp://yourUrl.com/yourFile", "");
//Convert to Sprite
Texture2D tex = new Texture2D(250, 192);
tex.LoadImage(yourImage);
Sprite s = Sprite.Create(tex, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f));
Download Only (With Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
byte[] yourImage = downloadWithFTP("ftp://yourUrl.com/yourFile", "", "UserName", "Password");
//Convert to Sprite
Texture2D tex = new Texture2D(250, 192);
tex.LoadImage(yourImage);
Sprite s = Sprite.Create(tex, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f));
public boolean ftpDownloadVideo() {
String desFilePath = (Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)).toString(); ///xet trong moi truong SD card////
String srcFilePath = "/detect_disease/rice_detect_disease_2019_09_24__10_07_08.avi";
boolean status = false;
Log.d(TAG, "2");
try {
Log.e("downloadFTP login : ", "Success");
OutputStream desFileStream = new BufferedOutputStream( new FileOutputStream(desFilePath));
Log.d(TAG,"FileOutputStream");
status = mFTPClient.retrieveFile(srcFilePath, desFileStream);
Log.d(TAG, "4");
desFileStream.close();
Log.e("downloadFTP status : ", "" + status);
Log.d(TAG, "5");
return status;
} catch (Exception e) {
Log.d(TAG, "download failed");
}
return status;
}

Long Internet operations with limited bandwidth

yesterday I published my application which I tested on my phone and worked as intended. When my friends downloaded it using 3G and not WiFi my application failed to download all the content and as a result it crashed. I used a headless fragment which runs an AsyncTask in order to download the content (which is some photos) my guess is that it took a lot of time for some photos and skipped them, throwing some timeOut exception. My question is would this be avoided if instead of an fragment I used a service to run my AsyncTask and download the content?
private ArrayList<Monument> processJsonData(JSONObject jsonObj) throws IOException{
try{
JSONArray posts=jsonObj.getJSONArray(TAG_POSTS);
ArrayList<Monument> monuments = new ArrayList<Monument>();
for (int i=0; i<posts.length(); i++){
JSONArray attachments = c.optJSONArray(TAG_ATTACHMENTS);
if(attachments!=null){
int lengthSize;
if(attachments.length()<3)
lengthSize=attachments.length();
else
lengthSize=3;
for(int j=0;j<lengthSize;++j){
JSONObject atta = attachments.getJSONObject(j);
JSONObject images = atta.optJSONObject(TAG_IMAGES);
if(images!=null){
JSONObject medium = images.getJSONObject(TAG_MEDIUM);
String url_image = medium.getString(TAG_URL_IMAGE);
String id = atta.getString("id");
String filename =title.replace(" ","")+id+".nomedia";
File destination = new File(MyApplication.getPhotoStorage() ,filename);
URL url = new URL (url_image);
InputStream is = url.openStream();
OutputStream os = new FileOutputStream(destination);
byte[] b = new byte[2048];
int length;
while ((length = is.read(b)) != -1) {
os.write(b, 0, length);
}
is.close();
os.close();
localPhotosUrl.add(destination.getAbsolutePath());
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Edit so I made these changes in my code now I'm dealing with the connectionTimeout exception but I can't catch it properly
#Override
protected String doInBackground(String... args) {
JSONParser jParser = new JSONParser();
JSONObject jsonObj = jParser.getJSONFromUrl(url);
Log.d("check1",url);
try {
listOfObjects.addAll(processJsonData(jsonObj));
} catch (Exception e) {
e.printStackTrace();
onDownloadFailed(this);
} finally {
jsonObj=null;
}
return "done";
}
protected void onDownloadFailed(downloadUrl task) {
System.out.println(task.tag+" failed to download");
if(dtask1.cancel(true))
Log.d("TASK1", "Canceled");
if(dtask2.cancel(true))
Log.d("TASK2", "Canceled");
if(dtask3.cancel(true))
Log.d("TASK3", "Canceled");
if(dtask4.cancel(true))
Log.d("TASK4", "Canceled");
mCallbacks.onDownloadFailed();
}
private ArrayList<Monument> processJsonData(JSONObject jsonObj) throws IOException, SocketException, JSONException{
JSONArray attachments = c.optJSONArray(TAG_ATTACHMENTS);
if(attachments!=null){
int lengthSize;
if(attachments.length()<3)
lengthSize=attachments.length();
else
lengthSize=3;
for(int j=0;j<lengthSize;++j){
JSONObject atta = attachments.getJSONObject(j);
JSONObject images = atta.optJSONObject(TAG_IMAGES);
if(images!=null){
JSONObject medium = images.getJSONObject(TAG_MEDIUM);
String url_image = medium.getString(TAG_URL_IMAGE);
String id = atta.getString("id");
String filename =title.replace(" ","")+id+".nomedia";
File destination = new File(MyApplication.getPhotoStorage() ,filename);
try{
URL url = new URL (url_image);
InputStream is = url.openStream();
OutputStream os = new FileOutputStream(destination);
byte[] b = new byte[2048];
int length;
while ((length = is.read(b)) != -1) {
os.write(b, 0, length);
}
is.close();
os.close();
localPhotosUrl.add(destination.getAbsolutePath());
}catch (SocketException e) {
throw new SocketTimeoutException();
}
}
}
}
Alright, so I don't know much about JSON, but that shouldn't affect this answer as a whole I don't think. It looks like your issue could be solved by better use of Exception handling.
At the moment you are not really handling the exception at all, merely catching it and printing the stack trace. Also, because the entire method is inside one try{ } statement the method is exiting if there is a problem dealing with any one of the attachments. Instead, you could include a try{ } block inside of your for loop. This way, if any one of the loop blocks failed (due to say an unstable connection), you can use the catch block to j--; and then Thread.sleep(4000);. That way, when an exception is thrown in the loop it will be caught, the loop will be jumped back to try the same section again, and there will be a pause to to wait for a better connection.
Example (Not tested);
for (int j = 0; j < lengthSize; ++j) {
try{
JSONObject atta = attachments.getJSONObject(j);
JSONObject images = atta.optJSONObject(TAG_IMAGES);
if (images != null) {
JSONObject medium = images.getJSONObject(TAG_MEDIUM);
String url_image = medium.getString(TAG_URL_IMAGE);
String id = atta.getString("id");
String filename = title.replace(" ", "") + id + ".nomedia";
File destination = new File(MyApplication.getPhotoStorage(), filename);
URL url = new URL(url_image);
InputStream is = url.openStream();
OutputStream os = new FileOutputStream(destination);
byte[] b = new byte[2048];
int length;
while ((length = is.read(b)) != -1) {
os.write(b, 0, length);
}
is.close();
os.close();
localPhotosUrl.add(destination.getAbsolutePath());
}
}catch (Exception e){
Thread.sleep(4000);
j--;
}
}
You may also want to create a counter to see how many attempts have been made. If the code makes too many attempts you can then assume that it will never work and return from the method.
I hope this helps. Let me know how you get on.
Also, as an aside you should never really catch Exception. Better to catch the specific exception/s you are expecting so that you can deal with them differently depending on the Exception subtype. You don't want to catch a RunTimeException and try to handle it when there may be no effective way of doing so.
No. This won't be avoided. You cannot expect a connection to last for a long time. Anything can happen on the phone (battery exhausted, network signal lost, etc.)
You must code your app so that it properly checks if it has all the resources available and to retry (or better, resume) downloads that failed.
To increase your chances, break your resources in relatively small downloads instead of a huge file. Then the user does not have to download all from scratch when using 3G for instance.

How to download a database and integrate into the app

I'm trying to download a database from a server and then use it in my app. My initial situation is that I've got a database in my assets folder and I am trying to download the whole database from a server when the user is clicking a button.
I think I have to use the onUpgrade method but I don't know how I can download the database and then use it or and load it into the assetfolder.
You need to brake the problem into smaller issues.
Make sure that the server side is able to provide you with a web-service with which you'll do the actual download.
It would be ideal if your server would zip your sqlite database in order for you to minimize traffic.
After you have downloaded the db on your device, you'll have to unzip it, if you have taken this approach.
When you get to the point of having the db downloaded and unzipped it's just a matter of connecting to a sqlite db and Android provides api in this regards.
Of-course, all of the above needs to be done in a multithreaded environment, you could also take into account using an Android service.
This is an example of downloading a file from a server, it should help you get an idea, it's not too generic, it works in my environment.
private void handleSelectedItemDownload(final String downloadItem) {
try {
int bufferLength;
long downloadedSize = 0;
long downloadProgressBytes = 0;
final byte[] buffer = new byte[12 * 163840];
this.url = new URL(String.format(AppConstants.SERVICE_BASE_URL + AppConstants.MEDIA_IMAGE_REQUEST_URL_PATH + downloadItem));
InputStream inputStream = this.getStream(downloadedSize);
downloadedSize = this.chunkSize;
if (downloadedSize > 0) {
this.applicationFolder = downloadItem.equals("pictures") ? Utils.getThumbNailsFolder() : Utils.getApplicationFolder();
final File mediaFolder = new File(this.applicationFolder);
mediaFolder.mkdirs();
final File file = new File(mediaFolder, String.format(File.separator + downloadItem + ".zip"));
final FileOutputStream fileOutput = new FileOutputStream(file);
this.actionStatusProgressBar.setProgress(0);
final Message downloadStartMessage = new Message();
downloadStartMessage.what = ACTION_STARTED;
this.actionMessageHandler.sendMessage(downloadStartMessage);
int progressBarMax = (int) (this.totalDownloadSize / 1024);
this.actionStatusProgressBar.setMax(progressBarMax);
while (chunkSize != 0) {
while ((bufferLength = inputStream.read(buffer)) > 0) {
fileOutput.write(buffer, 0, bufferLength);
downloadProgressBytes += bufferLength;
final Message currentProgressMessage = new Message();
currentProgressMessage.arg1 = (int) (downloadProgressBytes / 1024);
currentProgressMessage.what = ACTION_PROGRESS_VALUE;
this.actionMessageHandler.sendMessage(currentProgressMessage);
}
inputStream.close();
this.urlConnection.disconnect();
inputStream = this.getStream(downloadedSize);
downloadedSize += chunkSize;
}
fileOutput.close();
}
this.urlConnection.disconnect();
this.totalDownloadSize = 0;
} catch (MalformedURLException e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
} catch (IOException e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
} catch (Exception e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
}
}
private InputStream getStream(final long downloadedSize) throws IOException {
this.urlConnection = (HttpURLConnection) this.url.openConnection();
this.urlConnection.setRequestMethod("GET");
this.urlConnection.addRequestProperty("DOWNLOADED_SIZE", String.valueOf(downloadedSize));
final InputStream inputStream = this.urlConnection.getInputStream();
this.chunkSize = this.urlConnection.getContentLength();
if (!Utils.stringIsNullOrEmpty(this.urlConnection.getHeaderField("TOTAL_SIZE"))) {
if (this.totalDownloadSize == 0) {
this.totalDownloadSize = Long.parseLong(this.urlConnection.getHeaderField("TOTAL_SIZE"));
}
}
return inputStream;
}
Afterwards, when you have downloaded your db file, you can just create a SQLiteDatabase object:
final File dbFile = new File(filePath);
if (dbFile.exists()) {
this.localDatabase = SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READWRITE);
this.localDatabase.setLocale(Locale.getDefault());
}
I still stick to the opinion that you should also give zipping a thought, you reduce a LOT the traffic and the time spent over network.

AsyncTask - slow download

I am using AsyncTask to download ~50 MB files from internet. Sometimes, when I download this file, progress bar gain is very slow (even when I am on Wi-Fi). And after minute, phone shows me, download complete, but the file itself has only ~100kB, no more. But when I restart device, and try to download file, download is executed briefly and quick. Has anyone faced same problem? Do I need to erase same download memory before downloading new file? I am downloading file to Environment.externalStoryDirectory().
Thx
Calling download from activity:
mProgressDialog = new ProgressDialog(ItemDetails.this);
mProgressDialog.setTitle("Downloading");
mProgressDialog.setMessage("Downloading sth...");
mProgressDialog.setIndeterminate(false);
mProgressDialog.setMax(100);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
DownloadMapTask downloadFile = new DownloadMapTask(ItemDetails.this);
downloadFile.execute(web_location_url);
mProgressDialog.show();
Download Async Task (two methods):
#Override
protected String doInBackground(String... urls) {
int count;
PATH=maps_loc+"/Android/data/test/maps/";
try {
URL url = new URL(urls[0]);
HttpURLConnection connection2 = (HttpURLConnection) url.openConnection();
connection2.setRequestMethod("GET");
connection2.setDoOutput(true);
connection2.connect();
int lenghtOfFile = connection2.getContentLength();
File apkdir = new File(PATH);
apkdir.mkdirs();
File newInstall = new File(PATH, name+".tmp");
InputStream input = new BufferedInputStream(url.openStream());
OutputStream output = new FileOutputStream(newInstall);
byte data[] = new byte[1024];
long total = 0;
while ((count = input.read(data)) != -1 && running==true) {
total += count;
publishProgress((int) (total * 100 / lenghtOfFile));
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public void onProgressUpdate(Integer... args) {
ItemDetails.mProgressDialog.setProgress(args[0]);
}
Some servers will close the connection if the client has slow speed and the download takes long time, which can be the case if your program is connected to the Internet through mobile data not Wi-Fi.
You should consider supporting download resume in your program to not start from scratch every time.
I do not think there is sort of download memory that you need to clear. I have an app that can easily downloads over 50MB with no problems.
Also, you might consider obtaining a lock for both Wi-Fi and processor to keep your program running until the download finishes.
Edit
In your code, try to print the value lenghtOfFile after the line int lenghtOfFile = connection2.getContentLength(); to make sure that it is the same as the actual file size you are downloading.
Below is alternative example code which supports resume that I am using in my projects. (it is just to illustrate the idea, you will need to modify the code to your needs)
HttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet(new URI(fileURL)));
HttpResponse response;
InputStream is = null;
FileOutputStream fos = null;
try {
boolean continueDownloading = false;
String tmpFileName = fileName + "_tmp";
outputFile = new File(downloadFolder, tmpFileName);
if (outputFile.exists()) {
localFileLength = outputFile.length();
if (localFileLength > 0) {
continueDownloading = true;
}
if (continueDownloading) {
request.addHeader("Range", "bytes=" + localFileLength + "-");
}
response = httpClient.execute(request);
long remoteFileLength = 0;
Header contentLengthHeader = response.getFirstHeader("Content-Length");
if (contentLengthHeader != null) {
remoteFileLength = Integer.parseInt(contentLengthHeader.getValue());
}
long downloaded = 0;
if (continueDownloading) {
downloaded = localFileLength;
}
long fullFileLength = downloaded + remoteFileLength;
fos = new FileOutputStream(outputFile, true);
is = response.getEntity().getContent();
byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE];
int len = 0;
while ((len = is.read(buffer)) != -1 && isDownloading) {
fos.write(buffer, 0, len);
downloaded += len;
}
fos.flush();
boolean success = downloaded == fullFileLength;
if (success) {
outputFile.renameTo(new File(downloadFolder, fileName));
}
} catch (Throwable ex) {
ex.printStackTrace();
} finally {
// clean up resources
}
Try using downloadManager instead of downloading manually , there are many advantages to using it.
Here is an example for it : DownloadManager Example
and take a look at the documentations : DownloadManager

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