mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
348 lines
11 KiB
Java
348 lines
11 KiB
Java
/**
|
|
* UploaderThread.java
|
|
* Copyright (C) 2009 Char Software Inc., DBA Localytics
|
|
*
|
|
* This code is provided under the Localytics Modified BSD License.
|
|
* A copy of this license has been distributed in a file called LICENSE
|
|
* with this source code.
|
|
*
|
|
* Please visit www.localytics.com for more information.
|
|
*/
|
|
|
|
package com.localytics.android;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.FileReader;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.io.UnsupportedEncodingException;
|
|
|
|
import org.apache.http.HttpResponse;
|
|
import org.apache.http.StatusLine;
|
|
import org.apache.http.client.ClientProtocolException;
|
|
import org.apache.http.client.methods.HttpPost;
|
|
import org.apache.http.entity.StringEntity;
|
|
import org.apache.http.impl.client.DefaultHttpClient;
|
|
|
|
import android.util.Log;
|
|
|
|
/**
|
|
* The thread which handles uploading Localytics data.
|
|
* @author Localytics
|
|
*/
|
|
public class UploaderThread extends Thread
|
|
{
|
|
private Runnable _completeCallback;
|
|
private File _localyticsDir;
|
|
private String _sessionFilePrefix;
|
|
private String _uploaderFilePrefix;
|
|
private String _closeFilePrefix;
|
|
|
|
// The Tag used in logging.
|
|
private final static String LOG_TAG = "Localytics_uploader";
|
|
|
|
// The URL to send Localytics session data to
|
|
private final static String ANALYTICS_URL = "http://analytics.localytics.com/api/datapoints/bulk";
|
|
|
|
// The size of the buffer used for reading in files.
|
|
private final static int BUFFER_SIZE = 1024;
|
|
|
|
/**
|
|
* Creates a thread which uploads the session files in the passed Localytics
|
|
* Directory. All files starting with sessionFilePrefix are renamed,
|
|
* uploaded and deleted on upload. This way the sessions can continue
|
|
* writing data regardless of whether or not the upload succeeds. Files
|
|
* which have been renamed still count towards the total number of Localytics
|
|
* files which can be stored on the disk.
|
|
* @param appContext The context used to access the disk
|
|
* @param completeCallback A runnable which is called notifying the caller that upload is complete.
|
|
* @param localyticsDir The directory containing the session files
|
|
* @param sessionFilePrefix The filename prefix identifying the session files.
|
|
* @param uploaderfilePrefix The filename prefixed identifying files to be uploaded.
|
|
*/
|
|
public UploaderThread(
|
|
File localyticsDir,
|
|
String sessionFilePrefix,
|
|
String uploaderFilePrefix,
|
|
String closeFilePrefix,
|
|
Runnable completeCallback)
|
|
{
|
|
this._localyticsDir = localyticsDir;
|
|
this._sessionFilePrefix = sessionFilePrefix;
|
|
this._uploaderFilePrefix = uploaderFilePrefix;
|
|
this._closeFilePrefix = closeFilePrefix;
|
|
this._completeCallback = completeCallback;
|
|
}
|
|
|
|
/**
|
|
* Renames all the session files (so that other threads can keep writing
|
|
* datapoints without affecting the upload. And then uploads them.
|
|
*/
|
|
public void run()
|
|
{
|
|
int numFilesToUpload = 0;
|
|
|
|
try
|
|
{
|
|
if(this._localyticsDir != null && this._localyticsDir.exists())
|
|
{
|
|
String basePath = this._localyticsDir.getAbsolutePath();
|
|
|
|
// rename all the files, taking care to rename the session files
|
|
// before the close files.
|
|
renameOrAppendSessionFiles(basePath);
|
|
renameOrAppendCloseFiles(basePath);
|
|
|
|
// Grab all the files to be uploaded
|
|
FilenameFilter filter = new FilenameFilter()
|
|
{
|
|
public boolean accept(File dir, String name)
|
|
{
|
|
return name.startsWith(_uploaderFilePrefix);
|
|
}
|
|
};
|
|
|
|
String uploaderFiles[] = this._localyticsDir.list(filter);
|
|
numFilesToUpload = uploaderFiles.length;
|
|
String postBody = createPostBodyFromFiles(basePath, uploaderFiles);
|
|
|
|
// Attempt to upload this data. If successful, delete all the uploaderFiles.
|
|
Log.v(UploaderThread.LOG_TAG, "Attempting to upload " + numFilesToUpload + " files.");
|
|
if(uploadSessions(postBody.toString()) == true)
|
|
{
|
|
int currentFile;
|
|
File uploadedFile;
|
|
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
|
|
{
|
|
uploadedFile = new File(basePath + "/" + uploaderFiles[currentFile]);
|
|
uploadedFile.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify the caller the upload is complete.
|
|
if(this._completeCallback != null)
|
|
{
|
|
this._completeCallback.run();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.v(UploaderThread.LOG_TAG, "Swallowing exception: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Looks at every file whose name starts with the session file prefix
|
|
* and renamed or appends it to the appropriately named uploader file.
|
|
* @param basePath The full path to the directory containing the files to upload
|
|
*/
|
|
private void renameOrAppendSessionFiles(String basePath)
|
|
{
|
|
int currentFile;
|
|
|
|
// Create a filter to only grab the session files.
|
|
FilenameFilter filter = new FilenameFilter()
|
|
{
|
|
public boolean accept(File dir, String name)
|
|
{
|
|
return name.startsWith(_sessionFilePrefix);
|
|
}
|
|
};
|
|
|
|
// Go through each of the session files
|
|
String[] originalFiles = this._localyticsDir.list(filter);
|
|
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
|
|
{
|
|
String originalFileName = basePath + "/" + originalFiles[currentFile];
|
|
String targetFileName = basePath + "/" + this._uploaderFilePrefix + originalFiles[currentFile];
|
|
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Looks at every close file in the directory and renames or appends it to
|
|
* the appropriate uploader file. This is done separately from the session
|
|
* files because it makes life simpler on the webservice if the close events
|
|
* come after the session events
|
|
* @param basePath The full path to the directory containing the files to upload
|
|
*/
|
|
private void renameOrAppendCloseFiles(String basePath)
|
|
{
|
|
int currentFile;
|
|
|
|
// Create a filter to only grab the session files.
|
|
FilenameFilter filter = new FilenameFilter()
|
|
{
|
|
public boolean accept(File dir, String name)
|
|
{
|
|
return name.startsWith(_closeFilePrefix);
|
|
}
|
|
};
|
|
|
|
// Go through each of the session files
|
|
String[] originalFiles = this._localyticsDir.list(filter);
|
|
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
|
|
{
|
|
String originalFileName = basePath + "/" + originalFiles[currentFile];
|
|
|
|
// In order for the close events to be appended to the appropriate files
|
|
// remove the close prefix and prepend the session prefix
|
|
String targetFileName = basePath + "/"
|
|
+ this._uploaderFilePrefix
|
|
+ getSessionFilenameFromCloseFile(originalFiles[currentFile]);
|
|
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines what the name of the session file matching this close file would be
|
|
* @param closeFilename Name of close file to be used as a guide
|
|
* @return The filename of the session which matches this close file
|
|
*/
|
|
private String getSessionFilenameFromCloseFile(String closeFilename)
|
|
{
|
|
return this._sessionFilePrefix + closeFilename.substring(this._closeFilePrefix.length());
|
|
}
|
|
|
|
/**
|
|
* Checks if destination file exists. If so, it appends the contents of
|
|
* source to destination and deletes source. Otherwise, it rename source
|
|
* to destination.
|
|
* @param source File containing the data to be moved
|
|
* @param destination Target for the data
|
|
*/
|
|
private static void renameOrAppendFile(File source, File destination)
|
|
{
|
|
if(destination.exists())
|
|
{
|
|
try
|
|
{
|
|
InputStream in = new FileInputStream(source);
|
|
OutputStream out = new FileOutputStream(destination, true);
|
|
|
|
byte[] buf = new byte[1024];
|
|
int len;
|
|
while ((len = in.read(buf)) > 0)
|
|
{
|
|
out.write(buf, 0, len);
|
|
}
|
|
in.close();
|
|
out.close();
|
|
source.delete();
|
|
}
|
|
catch (FileNotFoundException e)
|
|
{
|
|
Log.v(LOG_TAG, "File not found.");
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
Log.v(LOG_TAG, "IO Exception: " + e.getMessage());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
source.renameTo(destination);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads in the input files and cats them together in one big string which makes up the
|
|
* HTTP request body.
|
|
* @param basePath The directory to get the files from
|
|
* @param uploaderFiles the list of files to read
|
|
* @return A string containing a YML blob which can be uploaded to the webservice.
|
|
*/
|
|
private String createPostBodyFromFiles(final String basePath, final String[] uploaderFiles)
|
|
{
|
|
int currentFile;
|
|
File inputFile;
|
|
StringBuffer postBody = new StringBuffer();
|
|
|
|
// Read each file in to one buffer. This allows the upload to happen as one
|
|
// large transfer instead of many smaller transfers which is preferable on
|
|
// a mobile device in which the time required to make a connection is often
|
|
// disproportionately large compared to the time to upload the data.
|
|
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
|
|
{
|
|
inputFile = new File(basePath + "/" + uploaderFiles[currentFile]);
|
|
|
|
try
|
|
{
|
|
BufferedReader reader = new BufferedReader(
|
|
new InputStreamReader(
|
|
new FileInputStream(inputFile),
|
|
"UTF-8"),
|
|
UploaderThread.BUFFER_SIZE);
|
|
char[] buf = new char[1024];
|
|
int numRead;
|
|
while( (numRead = reader.read(buf)) > 0)
|
|
{
|
|
postBody.append(buf, 0, numRead);
|
|
}
|
|
reader.close();
|
|
}
|
|
catch (FileNotFoundException e)
|
|
{
|
|
Log.v(LOG_TAG, "File Not Found");
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
Log.v(LOG_TAG, "IOException: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
return postBody.toString();
|
|
}
|
|
|
|
/**
|
|
* Uploads the post Body to the webservice
|
|
* @param ymlBlob String containing the YML to upload
|
|
* @return True on success, false on failure.
|
|
*/
|
|
private boolean uploadSessions(String ymlBlob)
|
|
{
|
|
DefaultHttpClient client = new DefaultHttpClient();
|
|
HttpPost method = new HttpPost(UploaderThread.ANALYTICS_URL);
|
|
|
|
try
|
|
{
|
|
StringEntity postBody = new StringEntity(ymlBlob, "utf8");
|
|
method.setEntity(postBody);
|
|
HttpResponse response = client.execute(method);
|
|
|
|
StatusLine status = response.getStatusLine();
|
|
Log.v(UploaderThread.LOG_TAG, "Upload complete. Status: " + status.getStatusCode());
|
|
|
|
// On any response from the webservice, return true so the local files get
|
|
// deleted. This avoid an infinite loop in which a bad file keeps getting
|
|
// submitted to the webservice time and again.
|
|
return true;
|
|
}
|
|
|
|
// return true for any transportation errors.
|
|
catch (UnsupportedEncodingException e)
|
|
{
|
|
Log.v(LOG_TAG, "UnsuppEncodingException: " + e.getMessage());
|
|
return false;
|
|
}
|
|
catch (ClientProtocolException e)
|
|
{
|
|
Log.v(LOG_TAG, "ClientProtocolException: " + e.getMessage());
|
|
return false;
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
Log.v(LOG_TAG, "IOException: " + e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
}
|