package edu.illinois.geoalarm;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.TrafficStats;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ToggleButton;
/**
* This Activity is used to set various user options
* for the GeoAlarm app
* @author GeoAlarm
*/
public class Options extends Activity
{
GeoAlarmDB database;
TextView sessionUsageTxView;
TextView sessionUsageRxView;
TextView totalUsageTxView;
TextView totalUsageRxView;
EditText ringLengthEdit;
EditText vibrateLengthEdit;
Spinner backgroundColorSelectSpinner;
ToggleButton toggleSplashScreenButton;
ProgressDialog updateProgressDialog;
boolean firstCopy;
private static final String DB_PATH = "/data/data/edu.illinois.geoalarm/databases/";
private static String DB_NAME = "geoAlarmDB.sqlite";
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.options);
sessionUsageTxView = (TextView)findViewById(R.id.sessionDataTxShow);
sessionUsageRxView = (TextView)findViewById(R.id.sessionDataRxShow);
totalUsageTxView = (TextView)findViewById(R.id.totalDataTxShow);
totalUsageRxView = (TextView)findViewById(R.id.totalDataRxShow);
backgroundColorSelectSpinner = (Spinner)findViewById(R.id.backgroundColorSelectSpinner);
ringLengthEdit = (EditText)findViewById(R.id.ringtoneLengthEditText);
vibrateLengthEdit = (EditText)findViewById(R.id.vibrationLengthEditText);
toggleSplashScreenButton = (ToggleButton)findViewById(R.id.toggleSplashScreenButton);
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
toggleSplashScreenButton.setChecked(settings.getBoolean("splash_screen", false));
ringLengthEdit.setText(String.valueOf(settings.getInt("ring_length", 3)));
vibrateLengthEdit.setText(String.valueOf(settings.getInt("vibrate_length", 3)));
updateProgressDialog = new ProgressDialog(Options.this);
updateProgressDialog.setMessage("Updating GeoAlarm Database");
updateProgressDialog.setIndeterminate(false);
updateProgressDialog.setMax(100);
updateProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
}
@Override
public void onStart()
{
populateBackgroundColorSelectSpinner();
setBackgroundColorSelectSpinnerEventListeners();
setToggleButtonEventListeners();
super.onStart();
}
@Override
public void onResume()
{
super.onResume();
// Update database if first run
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
View v = findViewById(R.id.optionsTopLayout);
v.setBackgroundColor(settings.getInt("color_value", R.color.Blue));
firstCopy = settings.getBoolean("geo_alarm_first_run", true);
if(firstCopy)
{
if(isOnline())
{
onClickUpdateDatabase(null);
}
}
else
{
loadDatabase();
showUsageData();
}
}
@Override
public void onStop()
{
if(database != null)
{
database.close();
}
super.onStop();
}
/**
* Checks whether we have a network connection
* @return true if connected, false otherwise
*/
public boolean isOnline()
{
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnectedOrConnecting()) {
return true;
}
return false;
}
/**
* Helper function to load the database
*/
public void loadDatabase()
{
// Instantiate the database
database = new GeoAlarmDB(this.getApplicationContext());
// Check the custom SQLite helper functions that load existing DB
try
{
database.createDataBase();
}
catch (IOException e)
{
throw new Error("Unable to create/find database");
}
// Open the SQLite database
try
{
database.openDataBase();
}
catch (SQLException sql)
{
throw new Error("Unable to execute sql in: " + sql.toString());
}
}
/**
* This function updates the usage data labels when its launched.
* It uses the data stored in the UsageTable of GeoAlarmDB.
*/
public void showUsageData()
{
long numBytesLastReceivedSession = database.getBytes(GeoAlarmDB.DB_RX_SESSION);
long numBytesLastTransmittedSession = database.getBytes(GeoAlarmDB.DB_TX_SESSION);
long numBytesReceived = database.getBytes(GeoAlarmDB.DB_RX);
long numBytesTransmitted = database.getBytes(GeoAlarmDB.DB_TX);
database.close();
double numMegaBytesReceivedSession = ((double) numBytesLastReceivedSession) / 1E6;
double numMegaBytesTransmittedSession = ((double) numBytesLastTransmittedSession) / 1E6;
double numMegaBytesReceived = ((double) numBytesReceived) / 1E6;
double numMegaBytesTransmitted = ((double) numBytesTransmitted) / 1E6;
DecimalFormat df = new DecimalFormat("#.###");
String displaySessionRx = " " + df.format(numMegaBytesReceivedSession) + " MB";
String displaySessionTx = " " + df.format(numMegaBytesTransmittedSession) + " MB";
String displayTotalRx = " " + df.format(numMegaBytesReceived) + " MB";
String displayTotalTx = " " + df.format(numMegaBytesTransmitted) + " MB";
sessionUsageRxView.setText(displaySessionRx);
sessionUsageTxView.setText(displaySessionTx);
totalUsageRxView.setText(displayTotalRx);
totalUsageTxView.setText(displayTotalTx);
sessionUsageRxView.getRootView().invalidate();
}
/**
* Updates the stored usage data with the most up-to-date numbers
*/
public void updateUsageData()
{
if(database != null)
{
long numBytesLastReceivedSession = database.getBytes(GeoAlarmDB.DB_RX_SESSION);
long numBytesLastTransmittedSession = database.getBytes(GeoAlarmDB.DB_TX_SESSION);
long numBytesReceived = database.getBytes(GeoAlarmDB.DB_RX);
long numBytesTransmitted = database.getBytes(GeoAlarmDB.DB_TX);
long numBytesReceivedDelta = TrafficStats.getUidRxBytes(Process.myUid()) - database.getBytes(GeoAlarmDB.DB_RX_TARE_SESSION) - numBytesLastReceivedSession;
long numBytesTransmittedDelta = TrafficStats.getUidTxBytes(Process.myUid()) - database.getBytes(GeoAlarmDB.DB_TX_TARE_SESSION) - numBytesLastTransmittedSession;
database.setBytes(GeoAlarmDB.DB_RX_SESSION, numBytesLastReceivedSession + numBytesReceivedDelta);
database.setBytes(GeoAlarmDB.DB_TX_SESSION, numBytesLastTransmittedSession + numBytesTransmittedDelta);
database.setBytes(GeoAlarmDB.DB_RX, numBytesReceived + numBytesReceivedDelta);
database.setBytes(GeoAlarmDB.DB_TX, numBytesTransmitted + numBytesTransmittedDelta);
}
}
/**
* This function populates the color select spinner
*/
public void populateBackgroundColorSelectSpinner()
{
String[] colorList = this.getResources().getStringArray(R.array.color_array);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this.getBaseContext(), android.R.layout.simple_spinner_item, colorList);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
backgroundColorSelectSpinner.setAdapter(adapter);
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
backgroundColorSelectSpinner.setSelection(settings.getInt("color_number", 0));
}
/**
* This function sets the event listener for the color select spinner, which sets the color and saves it when clicked
*/
public void setBackgroundColorSelectSpinnerEventListeners()
{
/* Set a new event listener for the Spinner item selection */
backgroundColorSelectSpinner.setOnItemSelectedListener(new OnItemSelectedListener()
{
/* Implement the onItemSelected method to handle item selections */
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id)
{
String selectedColor = (String) backgroundColorSelectSpinner.getSelectedItem();
int color;
int number;
if(selectedColor.equals("Black"))
{
color = R.color.Black;
number = 2;
}
else if(selectedColor.equals("Pink"))
{
color = R.color.Pink;
number = 3;
}
else if(selectedColor.equals("Red"))
{
color = R.color.Red;
number = 1;
}
else
{
color = R.color.Blue;
number = 0;
}
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putInt("color_value", color);
editor.putInt("color_number", number);
editor.commit();
View v = findViewById(R.id.optionsTopLayout);
v.setBackgroundResource(settings.getInt("color_value", R.color.Blue));
}
/* We do nothing here. May want to change behavior so the last selected item behavior is used */
public void onNothingSelected(AdapterView<?> parentView)
{
// do nothing
}
});
}
/**
* Method called when Save button is clicked, writes preferences to private preferences
* file
* @param view The clicked button
*/
public void saveButton(View view)
{
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
int ringLength = 0;
int vibrateLength = 0;
try
{
ringLength = Integer.parseInt(ringLengthEdit.getText().toString());
vibrateLength = Integer.parseInt(vibrateLengthEdit.getText().toString());
}
catch (NumberFormatException ex)
{
ex.printStackTrace();
ringLength = 3;
vibrateLength = 3;
}
if(ringLength < 0)
{
ringLength = 3;
}
if(vibrateLength < 0)
{
vibrateLength = 3;
}
editor.putInt("ring_length", ringLength);
editor.putInt("vibrate_length", vibrateLength);
editor.commit();
ringLengthEdit.setText(String.valueOf(ringLength));
vibrateLengthEdit.setText(String.valueOf(vibrateLength));
}
/**
* Method called to register event listener for toggle button
*/
public void setToggleButtonEventListeners()
{
toggleSplashScreenButton.setOnCheckedChangeListener(new OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
{
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("splash_screen", isChecked);
editor.commit();
}
});
}
/**
* Method called when user clicks on update database button.
* @param view The button that's clicked clicked
*/
public void onClickUpdateDatabase(View view)
{
if(isOnline())
{
DownloadNewDatabase download = new DownloadNewDatabase(this, firstCopy);
download.execute("http://deflume1.projects.cs.illinois.edu/geoAlarmDB.sqlite", "http://deflume1.projects.cs.illinois.edu/db_version.txt");
}
else
{
AlertDialog.Builder failureBuilder = new AlertDialog.Builder(this);
failureBuilder.setMessage("Network connection failure! GeoAlarm must be connected to the internet to update the database!");
failureBuilder.setTitle("Sorry!");
failureBuilder.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// do nothing
}
});
AlertDialog failure = failureBuilder.create();
failure.show();
}
}
/**
* A private class, used to download the new database asynchronously. Checks the database for the version number,
* and returns an error code if we already have the latest version, or there is an IO error
* @author GeoAlarm
*
*/
private class DownloadNewDatabase extends AsyncTask<String, Integer, Integer>
{
Context mContext;
final int DOWNLOAD_OK = 0;
final int ERROR_URL = 1;
final int ERROR_IO = 2;
final int OLD_VERSION = 3;
final String tempFileName = "/data/data/edu.illinois.geoalarm/dbdownload.temp";
long fileSize;
boolean firstCopy;
int versionNumber;
long numBytesLastReceivedSession;
long numBytesLastTransmittedSession;
long numBytesReceived;
long numBytesTransmitted;
long numBytesReceivedTare;
long numBytesTransmittedTare;
/**
* Constructs a new async task, to download the database
* @param taskContext The context this async task was launched in
* @param firstCopy A flag indicating whether this is the first run of GeoAlarm
*/
public DownloadNewDatabase(Context taskContext, boolean firstCopy)
{
super();
this.firstCopy = firstCopy;
if(!firstCopy)
{
loadDatabase();
numBytesLastReceivedSession = database.getBytes(GeoAlarmDB.DB_RX_SESSION);
numBytesLastTransmittedSession = database.getBytes(GeoAlarmDB.DB_TX_SESSION);
numBytesReceived = database.getBytes(GeoAlarmDB.DB_RX);
numBytesTransmitted = database.getBytes(GeoAlarmDB.DB_TX);
numBytesReceivedTare = database.getBytes(GeoAlarmDB.DB_RX_TARE_SESSION);
numBytesTransmittedTare = database.getBytes(GeoAlarmDB.DB_TX_TARE_SESSION);
}
else
{
numBytesLastReceivedSession = 0;
numBytesLastTransmittedSession = 0;
numBytesReceived = 0;
numBytesTransmitted = 0;
numBytesReceivedTare = 0;
numBytesTransmittedTare = 0;
}
mContext = taskContext;
}
/**
* Checks the database version stored on the server. If the database version is old,
* it returns an error code. Otherwise, responds with DOWNLOAD_OK
* @param sUrl The array of URLs passed to this async task
* @return A status code for the download
*/
protected Integer checkDBVersion(String... sUrl)
{
try
{
// Read version file from server
URL versionUrl = new URL(sUrl[1]);
HttpURLConnection versionUrlConnection = (HttpURLConnection) versionUrl.openConnection();
versionUrlConnection.setRequestMethod("GET");
versionUrlConnection.setDoOutput(true);
versionUrlConnection.connect();
InputStream versionInputStream = versionUrlConnection.getInputStream();
versionNumber = 0;
int vSize = versionUrlConnection.getContentLength();
int vDownloadedSize = 0;
byte[] vBuffer = new byte[12];
int vBufferLength = 0;
while((vBufferLength = versionInputStream.read(vBuffer)) > 0)
{
vDownloadedSize += vBufferLength;
}
versionInputStream.close();
if(vDownloadedSize != vSize)
{
return ERROR_IO;
}
// Use DataInputStream(ByteArrayInputStream) to read the downloaded version number
DataInputStream dStream = new DataInputStream(new ByteArrayInputStream(vBuffer));
versionNumber = dStream.readInt();
dStream.close();
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
if(versionNumber <= settings.getInt("db_version_number", 0))
{
return OLD_VERSION;
}
}
catch(MalformedURLException e)
{
e.printStackTrace();
return ERROR_URL;
}
catch (IOException e)
{
e.printStackTrace();
return ERROR_IO;
}
return DOWNLOAD_OK;
}
/**
* The main body of this async task. Checks to see if we need to perform a database download,
* performs the download, and then copies the new database
*/
@Override
protected Integer doInBackground(String... sUrl)
{
try
{
// check database version
int downloadOK = checkDBVersion(sUrl);
if(downloadOK != DOWNLOAD_OK) return downloadOK;
// open connection to database on server
URL url = new URL(sUrl[0]);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setDoOutput(true);
urlConnection.connect();
// Delete old temp file
File tempFile = new File(tempFileName);
if(tempFile.exists())
{
tempFile.delete();
}
// Read in database, write to temp file
OutputStream tempFileOutputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
InputStream inputStream = urlConnection.getInputStream();
int totalSize = urlConnection.getContentLength();
int downloadedSize = 0;
byte[] buffer = new byte[1024];
int bufferLength = 0;
while((bufferLength = inputStream.read(buffer)) > 0)
{
tempFileOutputStream.write(buffer, 0, bufferLength);
downloadedSize += bufferLength;
publishProgress((int) ((downloadedSize * 100) / totalSize));
}
tempFileOutputStream.flush();
tempFileOutputStream.close();
inputStream.close();
// Check for download error
if(totalSize != downloadedSize)
{
if(tempFile.exists())
{
tempFile.delete();
}
return ERROR_IO;
}
fileSize = totalSize;
}
catch(MalformedURLException e)
{
e.printStackTrace();
return ERROR_URL;
}
catch (IOException e)
{
e.printStackTrace();
return ERROR_IO;
}
return DOWNLOAD_OK;
}
/**
* Executed prior to async task execution. Closes the database, then
* shows the progress dialog
*/
@Override
protected void onPreExecute()
{
super.onPreExecute();
if(!firstCopy)
{
database.close();
}
updateProgressDialog.show();
}
/**
* Called when the task calls publishProgress. Updates the progress
* dialog
*/
@Override
protected void onProgressUpdate(Integer... progress)
{
super.onProgressUpdate(progress);
updateProgressDialog.setProgress(progress[0]);
}
/**
* Called after the task is finished. Examines the status code for success/error,
* and then displays result to user.
*/
@Override
protected void onPostExecute(Integer result)
{
switch(result)
{
case DOWNLOAD_OK:
copyNewDatabase();
AlertDialog.Builder successBuilder = new AlertDialog.Builder(mContext);
successBuilder.setMessage("Database successfully updated");
successBuilder.setTitle("Success!");
successBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// Do nothing
}
});
AlertDialog success = successBuilder.create();
success.show();
break;
case ERROR_URL:
AlertDialog.Builder errorURLbuilder = new AlertDialog.Builder(mContext);
errorURLbuilder.setMessage("Couldn't reach update server");
errorURLbuilder.setTitle("URL Error");
errorURLbuilder.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// Do nothing
}
});
AlertDialog urlError = errorURLbuilder.create();
urlError.show();
updateProgressDialog.dismiss();
return;
case ERROR_IO:
AlertDialog.Builder errorIObuilder = new AlertDialog.Builder(mContext);
errorIObuilder.setMessage("A problem was encountered downloading the file");
errorIObuilder.setTitle("Download Error");
errorIObuilder.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// Do nothing
}
});
AlertDialog ioError = errorIObuilder.create();
ioError.show();
updateProgressDialog.dismiss();
return;
case OLD_VERSION:
AlertDialog.Builder versionIObuilder = new AlertDialog.Builder(mContext);
versionIObuilder.setMessage("Database is already the most recent version!");
versionIObuilder.setTitle("Success!");
versionIObuilder.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// Do nothing
}
});
AlertDialog versionError = versionIObuilder.create();
versionError.show();
updateProgressDialog.dismiss();
return;
}
updateProgressDialog.dismiss();
// Some housekeeping, to ensure data usage is correct
loadDatabase();
database.setupUsageDataTable();
database.setBytes(GeoAlarmDB.DB_RX_SESSION, numBytesLastReceivedSession + fileSize);
database.setBytes(GeoAlarmDB.DB_TX_SESSION, numBytesLastTransmittedSession);
database.setBytes(GeoAlarmDB.DB_RX, numBytesReceived + fileSize);
database.setBytes(GeoAlarmDB.DB_TX, numBytesTransmitted);
database.setBytes(GeoAlarmDB.DB_RX_TARE_SESSION, numBytesReceivedTare);
database.setBytes(GeoAlarmDB.DB_TX_TARE_SESSION, numBytesTransmittedTare);
tareSessionDataValues(fileSize);
database.setBytes(GeoAlarmDB.DB_RX_SESSION, numBytesLastReceivedSession + fileSize);
updateUsageData();
showUsageData();
SharedPreferences settings = getSharedPreferences("GeoAlarm", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("geo_alarm_first_run", false);
editor.putInt("db_version_number", versionNumber);
editor.commit();
}
/**
* A helper method, used to copy the database from the temporary file to
* the actual database file
*/
private void copyNewDatabase()
{
try
{
//delete old database
File databaseFile = new File(DB_PATH + DB_NAME);
if(databaseFile.exists())
{
databaseFile.delete();
}
mContext.deleteDatabase(DB_NAME);
// create empty database
SQLiteDatabase d = mContext.openOrCreateDatabase(DB_NAME, 0, null);
d.close();
// copy database
File tempFile = new File(tempFileName);
InputStream newDatabaseInputStream = new BufferedInputStream(new FileInputStream(tempFile));
File newDatabaseFile = new File(DB_PATH + DB_NAME);
OutputStream newDatabaseOutputStream = new BufferedOutputStream(new FileOutputStream(newDatabaseFile));
int sizeSoFar = 0;
byte[] buffer = new byte[1024];
int bufferLength = 0;
while((bufferLength = newDatabaseInputStream.read(buffer)) > 0)
{
newDatabaseOutputStream.write(buffer, 0, bufferLength);
sizeSoFar += bufferLength;
}
newDatabaseOutputStream.flush();
newDatabaseOutputStream.close();
newDatabaseInputStream.close();
// delete old database file
if(tempFile.exists())
{
tempFile.delete();
}
if(sizeSoFar != fileSize)
{
// fatal error
AlertDialog.Builder errorIObuilder = new AlertDialog.Builder(mContext);
errorIObuilder.setMessage("No more space available in internal storage. Please redownload GeoAlarm");
errorIObuilder.setTitle("Fatal Error");
errorIObuilder.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
// Do nothing
}
});
AlertDialog ioError = errorIObuilder.create();
ioError.show();
throw new Error("No more space available in internal storage");
}
}
catch(FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
/**
* This method gets the tare data values for the session, and stores them in the DB
*/
public void tareSessionDataValues(long fileSize)
{
/* Get tare data values for this session and store them */
long numBytesReceivedAtStart = 0;
numBytesReceivedAtStart = TrafficStats.getUidRxBytes(Process.myUid()) - fileSize;
long numBytesTransmittedAtStart = 0;
numBytesTransmittedAtStart = TrafficStats.getUidTxBytes(Process.myUid());
database.setupUsageDataTable();
database.setBytes(GeoAlarmDB.DB_RX_TARE_SESSION, numBytesReceivedAtStart);
database.setBytes(GeoAlarmDB.DB_TX_TARE_SESSION, numBytesTransmittedAtStart);
}
}