blob: 45045ce9d7ddcc63e5c9d55928dc104278b0e880 [file] [log] [blame]
package edu.umich.PowerTutor.service;
import edu.umich.PowerTutor.ui.UMLogger;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.PowerManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.DeflaterOutputStream;
/* This class is responsible for all of the policy decisions on when to actually
* send log information back to our log collecting servers and is also
* responsible for actually sending the data should it decide that it is
* appropriate.
*/
public class LogUploader {
private static final String TAG = "LogUploader";
public static final String UPLOAD_FILE = "PowerTrace_Upload.log";
private static final long NONE_LOG_LENGTH = 1 << 20; // 1 MiB
private static final long WIFI_LOG_LENGTH = 1 << 17; // 128 KiB
private static final long THREEG_LOG_LENGTH = 1 << 19; // 512 KiB
private static final int CONNECTION_NONE = 0;
private static final int CONNECTION_WIFI = 1;
private static final int CONNECTION_3G = 2;
private boolean plugged;
private File logFile;
private ConnectivityManager connectivityManager;
private TelephonyManager telephonyManager;
private Thread uploadThread;
public LogUploader(Context context) {
telephonyManager = (TelephonyManager)context.getSystemService(
Context.TELEPHONY_SERVICE);
connectivityManager = (ConnectivityManager)context.getSystemService(
Context.CONNECTIVITY_SERVICE);
logFile = context.getFileStreamPath(UPLOAD_FILE);
}
public synchronized boolean shouldUpload() {
switch(connectionAvailable()) {
case CONNECTION_WIFI:
return plugged && logFile.length() > WIFI_LOG_LENGTH;
case CONNECTION_3G:
return plugged && logFile.length() > THREEG_LOG_LENGTH;
default: // CONNECTION_NONE
return logFile.length() > NONE_LOG_LENGTH;
}
}
public synchronized void plug(boolean plugged) {
this.plugged = plugged;
}
private int connectionAvailable() {
/* TODO: Maybe we should only send data when the device is plugged in.
*/
NetworkInfo info = connectivityManager.getActiveNetworkInfo();
if(info == null || !connectivityManager.getBackgroundDataSetting()) {
return CONNECTION_NONE;
}
int netType = info.getType();
int netSubtype = info.getSubtype();
if (netType == ConnectivityManager.TYPE_WIFI) {
return info.isConnected() ? CONNECTION_WIFI : CONNECTION_NONE;
} else if (netType == ConnectivityManager.TYPE_MOBILE
&& netSubtype == TelephonyManager.NETWORK_TYPE_UMTS
&& !telephonyManager.isNetworkRoaming()) {
return info.isConnected() ? CONNECTION_3G : CONNECTION_NONE;
}
return CONNECTION_NONE;
}
public void upload(String origFile) {
if(new File(origFile).renameTo(logFile)) {
interrupt();
uploadThread = new Thread() {
public void run() {
long runID = System.currentTimeMillis();
for(int iter = 1; !interrupted(); iter++) {
if(send(runID)) {
break;
}
if(iter > 12) iter = 12; // The max wait is a little over 1 hour.
Log.i(TAG, "Failed to send log. Will try again in " + (1 << iter) +
" seconds");
try {
do {
sleep(1000 * (1 << iter)); // Sleep for 2^iter seconds.
} while(connectionAvailable() == CONNECTION_NONE);
} catch(InterruptedException e) {
break;
}
}
}
};
uploadThread.start();
} else {
Log.w(TAG, "Failed to move log file before sending");
}
}
public boolean isUploading() {
return uploadThread != null && uploadThread.isAlive();
}
public void interrupt() {
if(uploadThread != null) {
uploadThread.interrupt();
}
}
public void join() throws InterruptedException {
if(uploadThread != null) {
uploadThread.join();
}
}
public boolean send(long runID) {
Log.i(TAG, "Sending log data");
Socket s = new Socket();
try {
s.setSoTimeout(4000);
s.connect(new InetSocketAddress(UMLogger.SERVER_IP, UMLogger.SERVER_PORT),
15000);
} catch(IOException e) {
/* Failed to connect to server. Try again later.
*/
return false;
}
try {
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(logFile), 1024);
BufferedOutputStream sockOut = new BufferedOutputStream(
s.getOutputStream(), 1024);
/* Write the prefix string to the server. */
sockOut.write(getPrefix(runID, logFile.length()));
sockOut.write(0);
/* Write the log file to the server. */
byte[] buf = new byte[1024];
while(true) {
int sz = in.read(buf, 0, buf.length);
if(sz == -1) break;
sockOut.write(buf, 0, sz);
}
sockOut.flush();
int response = s.getInputStream().read();
in.close();
s.close();
if(response != 0) {
Log.w(TAG, "Log data not accepted by server");
}
} catch(SocketTimeoutException e) {
/* Connection trouble with server. Try again later.
*/
return false;
} catch(IOException e) {
Log.w(TAG, "Unexpected exception sending log. Dropping log data");
e.printStackTrace();
}
logFile.delete();
return true;
}
private byte[] getPrefix(long runID, long payloadLength) {
String deviceID = telephonyManager.getDeviceId();
return (UMLogger.CURRENT_VERSION + '|' + sanatize(Build.DEVICE) + '|' +
getMD5(deviceID) + "|" + payloadLength).getBytes();
}
/* Just strip out any | characters present. Normal DEVICE strings shouldn't
* have a | but this string can be set by anyone so we should treat it as
* adversarial.
*/
private String sanatize(String s) {
StringBuffer buf = new StringBuffer();
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) != '|') {
buf.append(s.charAt(i));
}
}
return buf.toString();
}
private String getMD5(String s){
MessageDigest m = null;
try {
m = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
// Well this sucks...
e.printStackTrace();
return "nohash";
}
m.update(s.getBytes(), 0, s.length());
return new BigInteger(1, m.digest()).toString(16);
}
}