/* Copyright (C) 2004-2006 Nokia Corporation Copyright (C) 2008-2011, Dirk Trossen, airs@dirk-trossen.de Copyright (C) 2013, TecVis LP, support@tecvis.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation as version 2.1 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.airs; import java.util.*; import java.io.*; import android.app.Activity; import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.format.DateFormat; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import com.airs.database.AIRS_database; import com.airs.helper.SerialPortLogger; import com.airs.helper.Waker; import com.airs.platform.HandlerManager; import com.airs.platform.Sensor; import com.airs.platform.SensorRepository; /** * Class to implement the local recording * * @see AIRS_remote * @see AIRS_record_tab * @see android.app.Service */ public class AIRS_local extends Service { // states for handler private static final int REFRESH_VALUES = 1; private static final int SHOW_NOTIFICATION = 2; private static final int BATTERY_KILL = 3; private static final String LINE = "LINE"; private static final String TEXT = "TEXT"; public boolean show_values=false; private HandlerThread[] threads = null; private int no_threads = 0; /** * Context of the main Service * @see android.content.Context */ public Context airs = null; /** * Template being used for starting the recording, if any used */ public String template; private int no_values = 0; private boolean localIntent_b; private boolean localStore_b; private boolean localDisplay_b; private int Reminder_i; private boolean Vibrate, Lights; private String LightCode; private NotificationManager mNotificationManager; private int BatteryKill_i; private boolean Wakeup_b; private String url = "AIRS_values"; private File mconn, path; private BufferedOutputStream os2; private int numberSensors = 0; private ListView sensors; private boolean shutdown = false; /** * Flag if discovery has been done */ public boolean discovered = false; /** * Flag if AIRS is recording */ public boolean running = false; /** * Flag if AIRS has been restarted */ public boolean restarted = false; /** * Flag if AIRS has been started as a service already */ public boolean started = false; /** * Flag that AIRS should be started initially */ public boolean start = false; /** * Flag that recording should be paused (where possible) */ public boolean paused = false; /** * Flag that sensors have been registered */ public boolean registered = false; private ArrayList<SensorEntry> mSensorsArrayList; private MyCustomBaseAdapter mSensorsArrayAdapter; /** * List of Values, being used in visualisation activity * @see AIRS_measurements */ public ArrayAdapter<String> mValuesArrayAdapter; private long nextDay; // milliseconds for next day starting // This is the object that receives interactions from clients private final IBinder mBinder = new LocalBinder(); private VibrateThread Vibrator; private Notification notification; private WakeLock wl = null; // database variables /** * Reference to current AIRS_database * @see AIRS_database */ static public AIRS_database database_helper; /** * Reference to AIRS database * @see android.database.sqlite.SQLiteDatabase */ static public SQLiteDatabase airs_storage; // private thread for reading the sensor handlers private class HandlerThread implements Runnable { private Sensor current; private int line; private String values_output = null; private long values_time; private int number_values = 0; public Thread thread; private boolean interrupted = false, pause = false;; private boolean started = true; private String value_intent; private long nextDay; // milliseconds for next day starting protected void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException ignore) { interrupted = true; } } /*********************************************************************** Function : HandlerThread() Input : current sensor, number for sensor in UI element Output : Return : Description : stores parameters of this query ***********************************************************************/ HandlerThread(Sensor current, int j) { // copy parameters this.current = current; line = j; values_output = new String(current.Symbol + " : - [" + current.Unit + "]"); // get current day and set time to last millisecond of that day for next day indicator Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.MILLISECOND, 999); nextDay = cal.getTimeInMillis(); (thread = new Thread(this, "AIRS: " + current.Symbol)).start();; } // return true if sensor has historical data public boolean hasHistory() { return current.hasHistory; } public String share() { return current.handler.Share(current.Symbol); } public String info() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(values_time); return new String("'" + current.Description + "'" + getString(R.string.sensed) + " " + number_values + " " + getString(R.string.sensed2) + " " + DateFormat.format("dd MMM yyyy k:mm:ss", cal) + " " + getString(R.string.sensed3) + values_output); } private void output(String text) { this.output(text, false); } private void output(String text, boolean refresh) { // save output for later if (refresh == false) { values_output = new String(text); // store output for later number_values++; // count number of sensed values // store timestamp values_time = System.currentTimeMillis(); } // shall values been shown? if (show_values == true) { if (text != null) { Message msg = mHandler.obtainMessage(REFRESH_VALUES); Bundle bundle = new Bundle(); bundle.putString(TEXT, text); bundle.putInt(LINE, line); msg.setData(bundle); mHandler.sendMessage(msg); } } } public void refresh() { output(values_output, true); } /*********************************************************************** Function : HandlerTask() Input : Output : Return : Description : task for resolving a query - to be started by the Acquisition component (usually in the callback for a dialog)!! if sending NOTIFY fails, thread returns, i.e., ends NOTIFY could fail due to termination of dialog (e.g., BYE) ***********************************************************************/ public void run() { byte[] sensor_data=null; int integer; double scaler; int i; String fileOut = null, fileIMG = null; scaler = 1; if (current.scaler>0) for (i=0;i<current.scaler;i++) scaler *=10; else for (i=current.scaler;i<0;i++) scaler /=10; try { while(interrupted == false) { // pause while told to while(pause == true) sleep(500); // handle sensor status switch(current.status) { // is sensor not valid anymore -> terminate! case Sensor.SENSOR_INVALID: if (current.statusString != null) output(current.Symbol + " : " + getString(R.string.Sensor_invalid) + " " + current.statusString, false); else output(current.Symbol + " : " + getString(R.string.Sensor_invalid2), true); // now wait wait(); break; case Sensor.SENSOR_SUSPEND: if (current.statusString != null) output(current.Symbol + " : " + getString(R.string.Sensor_suspended) + " " + current.statusString, false); else output(current.Symbol + " : " + getString(R.string.Sensor_suspended2), false); SerialPortLogger.debug("HandlerThread for " + current.Symbol + ": suspended, now waiting"); while (current.status == Sensor.SENSOR_SUSPEND) sleep(5000); output(current.Symbol + " : - [" + current.Unit + "]", false); SerialPortLogger.debug("HandlerThread for " + current.Symbol + ": woken up again"); break; case Sensor.SENSOR_VALID: // acquire latest value sensor_data = current.handler.Acquire(current.Symbol, null); // anything? if (sensor_data!=null) { // here we handle int/float sensor values if(current.type.equals("int") || current.type.equals("float")) { // do long int first integer = ((int)(sensor_data[2] & 0xFF) << 24) | ((int)(sensor_data[3] & 0xff) << 16) | ((int)(sensor_data[4] & 0xFF) << 8) | ((int)sensor_data[5] & 0xFF); // set text item in value field if (localDisplay_b == true) { if (current.scaler != 0) output(current.Symbol + " : " + String.valueOf((double)integer*scaler) + " [" + current.Unit + "]"); else output(current.Symbol + " : " + String.valueOf(integer) + " [" + current.Unit + "]"); } else { no_values++; output("# of values : " + String.valueOf(no_values)); } // need to store locally? if (localStore_b == true) { if (current.scaler !=0) fileOut = new String("'"+ String.valueOf(System.currentTimeMillis()) + "','" + current.Symbol + "','" + String.valueOf((double)integer*scaler) + "'"); else fileOut = new String("'" + String.valueOf(System.currentTimeMillis()) + "','" + current.Symbol + "','" + String.valueOf(integer) + "'"); } // need to create AIRS intent? if (localIntent_b == true) if (current.scaler != 0) value_intent = new String(String.valueOf((double)integer*scaler)); else value_intent = new String(String.valueOf((double)integer)); } // here we handle txt sensor values if(current.type.equals("txt") || current.type.equals("str")) { // set text item in value field if (localDisplay_b == true) { output(current.Symbol + " : " + new String(sensor_data, 2, sensor_data.length - 2) + " [" + current.Unit + "]"); } else { no_values++; output("# of values : " + String.valueOf(no_values)); } // need to store locally? if (localStore_b == true) fileOut = new String("'" + String.valueOf(System.currentTimeMillis()) + "','" + current.Symbol + "','" + new String(sensor_data, 2, sensor_data.length - 2) + "'"); // need to create AIRS intent? if (localIntent_b == true) value_intent = new String(new String(sensor_data, 2, sensor_data.length - 2)); } // here we handle img and arr sensor values if(current.type.equals("img") || current.type.equals("arr")) { // set text item in value field, here only the length of the sensor value field if (localDisplay_b == true) { output(current.Symbol + " : " + Integer.toString(sensor_data.length)); } else { no_values++; output("# of values : " + String.valueOf(no_values)); } // need to store locally? if (localStore_b == true) { try { fileIMG = new String(url + String.valueOf(System.currentTimeMillis()) + "_" + current.Symbol + ".jpg" ); // open file with read/write - otherwise, it will through a security exception (no idea why) mconn = new File(path, fileIMG); os2 = new BufferedOutputStream(new FileOutputStream(mconn, true)); // store sensor data os2.write(sensor_data, 2, sensor_data.length-2); os2.close(); // store filename in recording file fileOut = new String("'" + String.valueOf(System.currentTimeMillis()) + "','" + current.Symbol + "','" + fileIMG + "'"); // need to create AIRS intent? if (localIntent_b == true) value_intent = new String(fileIMG); } catch(Exception e) { debug("Exception in opening file connection"); } } } // anything to send via intents? if (localIntent_b == true) if (value_intent != null) { // send broadcast intent to signal end of selection to mood button handler Intent intent = new Intent("com.airs.sensor." + current.Symbol); intent.putExtra("Value", value_intent); sendBroadcast(intent); // free memory value_intent = null; } // anything to write to file? if (fileOut != null) { try { long timemilli = System.currentTimeMillis(); // write into database execStorage(timemilli, "INSERT into airs_values (Timestamp, Symbol, Value) VALUES ("+ fileOut + ")"); // write sensor value in table for faster retrieval later! if (started == true || timemilli > nextDay) { // save time of writing current.time_saved = timemilli; // is the current data at the next day? if (current.time_saved > nextDay) { // get current day and set time to last millisecond of that day Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.MILLISECOND, 999); nextDay = cal.getTimeInMillis(); } // try to enter into database try { execStorage(0, "INSERT into airs_sensors_used (Timestamp, Symbol) VALUES ('" + String.valueOf(current.time_saved) + "','" + current.Symbol + "')"); } catch(Exception e) { execStorage(0, AIRS_database.DATABASE_TABLE_CREATE3); execStorage(0, AIRS_database.DATABASE_TABLE_INDEX3); execStorage(0, "INSERT into airs_sensors_used (Timestamp, Symbol) VALUES ('" + String.valueOf(current.time_saved) + "','" + current.Symbol + "')"); } started = false; } else // if it's GPS or media watcher, data can be removed in Storica, so check if it's still there! if(current.Symbol.compareTo("GI") == 0 || current.Symbol.compareTo("MW") == 0) { try { // try to retrieve the previously saved entry String query = new String("SELECT Symbol from 'airs_sensors_used' WHERE Timestamp = " + String.valueOf(current.time_saved) + " AND Symbol='" + current.Symbol + "'"); Cursor values = airs_storage.rawQuery(query, null); // has old entry been deleted -> then save again if (values.getCount() == 0) { current.time_saved = System.currentTimeMillis(); try { execStorage(0, "INSERT into airs_sensors_used (Timestamp, Symbol) VALUES ('" + String.valueOf(current.time_saved) + "','" + current.Symbol + "')"); } catch(Exception e) { } } // and return memory values.close(); } catch(Exception e) { Log.e("AIRS", "error re-writing removed sensors:" + e.toString()); current.time_saved = System.currentTimeMillis(); try { execStorage(0, "INSERT into airs_sensors_used (Timestamp, Symbol) VALUES ('" + String.valueOf(current.time_saved) + "','" + current.Symbol + "')"); } catch(Exception ex) { } } } } catch(Exception e) { } // free memory fileOut = null; } } // are we waiting for the next poll? if (current.polltime>0 && interrupted ==false) Thread.sleep(current.polltime); break; } } } catch(Exception e) { debug("HandlerThread: interrupted and terminating 1..." + current.Symbol); return; } debug("HandlerThread: interrupted and terminating 2..." + current.Symbol); return; } } /** * Sleep function * @param millis */ private void sleep(long millis) { Waker.sleep(millis); } private void debug(String msg) { SerialPortLogger.debug(msg); } public class LocalBinder extends Binder { AIRS_local getService() { return AIRS_local.this; } } /** * Returns current instance of AIRS_local Service to anybody binding to AIRS_local * @param intent Reference to calling {@link android.content.Intent} * @return current instance to service */ @Override public IBinder onBind(Intent intent) { SerialPortLogger.debug("AIRS_local::bound service!"); return mBinder; } /** * Called when system is running low on memory * @see android.app.Service */ @Override public void onLowMemory() { SerialPortLogger.debugForced("AIRS_local::low memory warning from system - about to get killed!"); } /** * Called when starting the service the first time around * @see android.app.Service */ @Override public void onCreate() { SerialPortLogger.debugForced("AIRS_local::created service!"); // let's see if we need to restart Restart(true); } /** * Synchronised method for writing to AIRS database from the recording threads being created * @param milli timestamp of the recording * @param query the SQL query being executed, formed in the recording threads */ public synchronized void execStorage(long milli, String query) { if (airs_storage == null) return; synchronized(airs_storage) { if (airs_storage == null) return; airs_storage.execSQL(query); // is the current data at the next day? if (milli > nextDay) { // get current day and set time to last millisecond of that day Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.MILLISECOND, 999); int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); nextDay = cal.getTimeInMillis(); // mark date now as having values airs_storage.execSQL("INSERT into airs_dates (Year, Month, Day, Types) VALUES ('"+ String.valueOf(year) + "','" + String.valueOf(month) + "','" + String.valueOf(day) + "','1')"); } } } private void start_AIRS_local() { // find out whether or not to remind of running Reminder_i = HandlerManager.readRMS_i("Reminder", 0) * 1000; Vibrate = HandlerManager.readRMS_b("Vibrator", true); Lights = HandlerManager.readRMS_b("Lights", true); LightCode = HandlerManager.readRMS("LightCode", "00ff00"); // find out whether or not to kill based on battery condition BatteryKill_i = HandlerManager.readRMS_i("BatteryKill", 0); // find out whether or not to wakeup the sensing on user activity Wakeup_b = HandlerManager.readRMS_b("Wakeup", false); // find out whether or not to store locally localIntent_b = HandlerManager.readRMS_b("AIRSIntents", true); // find out whether or not to store locally localStore_b = HandlerManager.readRMS_b("LocalStore", true); // find out whether or not to display locally localDisplay_b = HandlerManager.readRMS_b("localDisplay", true); // if to store locally -> try to open write file on memory card if (localStore_b == true) { try { // get database database_helper = new AIRS_database(this.getApplicationContext()); airs_storage = database_helper.getWritableDatabase(); // airs_storage = SQLiteDatabase.openDatabase(this.getDatabasePath(database_helper.DATABASE_NAME).toString(), null, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.NO_LOCALIZED_COLLATORS); // have tables been created? if (HandlerManager.readRMS_b("AIRS_local::TablesExists", false) == false) { execStorage(0, AIRS_database.DATABASE_TABLE_CREATE); execStorage(0, AIRS_database.DATABASE_TABLE_CREATE2); HandlerManager.writeRMS_b("AIRS_local::TablesExists", true); } } catch(Exception e) { debug("AIRS_local::Exception in opening file connection"); // localStore_b = false; } } } /** * Called when service is destroyed, e.g., by stopService() * Here, we tear down all recording threads, close all handlers, unregister receivers for battery signal and close the thread for indicating the recording */ @Override public void onDestroy() { int i; SerialPortLogger.debug("AIRS_local::destroying service!"); // get database sync to squeeze out all other threads for killing them if (airs_storage != null) { synchronized(airs_storage) { // kill Handlers and threads if (started == true) { SerialPortLogger.debug("AIRS_local::terminating handlers"); // first kill handlers! HandlerManager.destroyHandlers(); // then kill threads try { SerialPortLogger.debug("AIRS_local::terminating " + String.valueOf(no_threads) + " HandlerThreads"); // interrupt all threads for terminated for (i = 0; i<no_threads;i++) if (threads[i] != null) { if (threads[i] != null) { threads[i].interrupted = true; threads[i].thread.interrupt(); } threads[i] = null; } // if Vibrator is running, stop it! if (Vibrator!=null) { Vibrator.stop = true; Vibrator.thread.interrupt(); Vibrator.thread = null; } } catch(Exception e) { SerialPortLogger.debugForced("AIRS_local::Exception when terminating Handlerthreads!"); } } // is local storage ongoing -> close file! if (localStore_b == true) { try { airs_storage.close(); airs_storage = null; } catch(Exception e) { SerialPortLogger.debugForced("AIRS_local::Exception when closing DB!"); } } } } // and kill persistent flag HandlerManager.writeRMS_b("AIRS_local::running", false); // kill internal flag running = false; // anything to send via intents? if (localIntent_b == true) { // send broadcast intent that AIRS has started Intent intent = new Intent("com.airs.local.stopped"); sendBroadcast(intent); } // release wake lock if held if (wl != null) wl.release(); // if registered for screen activity or battery level -> unregister if (registered == true) unregisterReceiver(mReceiver); // signal shutdown shutdown = true; // remove all pending messages mHandler.removeMessages(BATTERY_KILL); mHandler.removeMessages(REFRESH_VALUES); mHandler.removeMessages(SHOW_NOTIFICATION); SerialPortLogger.debug("AIRS_local::finished destroying service!"); } @Override /** * Called when startService() is invoked by other parts of AIRS (AIRS_record_tab as well as AIRS_shortcut) * The sequence of calling this appropriately (be careful to not change this) * 1. startService() * 2. bindService() * 3. set start = true and call startService() again * 4. service.Discover() -> sets discovered == true * 5. startService() again for measurements * @params intent Reference to the calling {@link Intent} * @params flags Flags set by the caller * @params startId ID created per calling of the service (identifying the caller) * @see AIRS_shortcut * @see AIRS_record_tab */ public int onStartCommand(Intent intent, int flags, int startId) { SerialPortLogger.debugForced("AIRS_local::started service ID " + Integer.toString(startId)); // return if intent is null -> service was restarted if (intent == null) { // let's see if we need to restart Restart(true); return Service.START_STICKY; } return Service.START_STICKY; } /** * Asks threads to refresh latest value */ public void refresh_values() { int i; for (i=0; i<no_threads;i++) threads[i].refresh(); } /** * Asks threads to pause */ public void pause_threads() { int i; for (i=0; i<no_threads;i++) threads[i].pause = true; // signal paused sate paused = true; } /** * Gets Value of the j-th thread * @param j index to thread which should provide the value * @return String of the current recording */ public String getValue(int j) { return threads[j].values_output; } /** * Gets Sensor symbol that the j-th thread is recording * @param j index to thread which should provide the symbol * @return String of the sensor symbol, see http://tecvis.co.uk/software/airs/internal-workings for list of supported sensor symbols */ public String getSymbol(int j) { return threads[j].current.Symbol; } /** * Sharing info for sensor entry that j-th thread is recording * @param j index to thread which should provide the sharing string * @return String to be shared or null if nothing to share */ public String share(int j) { return threads[j].share(); } /** * Show timeline, map or similar for sensor entry that j-th thread is recording * @param j index to thread which should show the information */ public void show_info(int j) { if (threads[j].hasHistory() == true) threads[j].current.handler.History(threads[j].current.Symbol); else { String info = threads[j].info(); Toast.makeText(getApplicationContext(), info, Toast.LENGTH_LONG).show(); } } /** * Ask threads to pause recording. Where callback receivers are used, such pausing is not supported! */ public void resume_threads() { int i; for (i=0; i<no_threads;i++) threads[i].pause = false; // signal paused sate paused = false; } /** * Sets all sensors being selected in the sensor list - called from UI in {@link AIRS_record_tab} */ public void selectall() { int i; SensorEntry entry; // clear checkboxes in sensor list and adapter for (i=0;i<numberSensors;i++) { sensors.setItemChecked(i, true); entry = mSensorsArrayList.get(i); entry.checked = true; mSensorsArrayList.set(i, entry); } mSensorsArrayAdapter.notifyDataSetChanged(); } /** * Unselects all sensors in the sensor list - called from UI in {@link AIRS_record_tab} */ public void unselectall() { int i; SensorEntry entry; // clear checkboxes in sensor list and adapter for (i=0;i<numberSensors;i++) { sensors.setItemChecked(i, false); entry = mSensorsArrayList.get(i); entry.checked = false; mSensorsArrayList.set(i, entry); } mSensorsArrayAdapter.notifyDataSetChanged(); } /** * Builds {@link android.app.AlertDialog} with sensor information */ public void sensor_info() { float scaler; int i; Sensor current = SensorRepository.root_sensor; String infoText = new String(); while(current != null) { scaler = 1; if (current.scaler>0) for (i=0;i<current.scaler;i++) scaler *=10; else for (i=current.scaler;i<0;i++) scaler /=10; infoText = infoText + current.Symbol + " : " + current.Description + " [" + current.Unit + "] from " + (float)current.min*scaler + " to " + (float)current.max*scaler + " \n\n"; current = current.next; } AlertDialog.Builder builder = new AlertDialog.Builder(airs); builder.setMessage(infoText) .setTitle("Sensor Repository") .setNeutralButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); AlertDialog alert = builder.create(); alert.show(); } /** * Sets up the main threads to run, acquire values, store them locally and display them (if wanted) * @param restarted is this measurements restarted (true) or not (false). * @return true if successfully started, false otherwise */ @SuppressWarnings("deprecation") public boolean startMeasurements(boolean restarted) { int i, j =0; Sensor current = null; // count values to be displayed for (i=0;i<sensors.getCount();i++) if (mSensorsArrayList.get(i).checked == true) j++; if (j == 0) { Toast.makeText(getApplicationContext(), getString(R.string.Enable_at_least_one_sensor), Toast.LENGTH_LONG).show(); return false; } notification = new Notification(R.drawable.notification_icon, getString(R.string.Started_AIRS), System.currentTimeMillis()); // create pending intent for starting the activity PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, AIRS_measurements.class), Intent.FLAG_ACTIVITY_NEW_TASK); // has it been started or restarted? if (restarted == false) notification.setLatestEventInfo(getApplicationContext(), getString(R.string.AIRS_Local_Sensing), getString(R.string.running_since), contentIntent); else notification.setLatestEventInfo(getApplicationContext(), getString(R.string.AIRS_Local_Sensing), getString(R.string.restarted_since), contentIntent); // set the time again for ICS notification.when = System.currentTimeMillis(); // don't allow clearing the notification notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; startForeground(1, notification); // store start timestamp HandlerManager.writeRMS_l("AIRS_local::time_started", System.currentTimeMillis()); // Find and set up the ListView for values mValuesArrayAdapter = new ArrayAdapter<String>(this, R.layout.sensor_list); // create handler threads for measurements no_threads = j; threads = new HandlerThread[j]; // do I need local display of values? if (localDisplay_b == true) { for (i=0;i<sensors.getCount();i++) { if (mSensorsArrayList.get(i).checked == true) { current = SensorRepository.findSensor(mSensorsArrayList.get(i).description.substring(0,2)); if (current != null) { if (current.type.equals("float") || current.type.equals("int") || current.type.equals("txt") || current.type.equals("str") ) mValuesArrayAdapter.add(current.Symbol + " : - [" + current.Unit + "]"); else mValuesArrayAdapter.add(current.Symbol + " : - "); } } } } else // if no individual values, show at least number of values acquired and memory available mValuesArrayAdapter.add("# of values : - "); // now create measurement threads for (i=0, j=0;i<sensors.getCount();i++) { current = SensorRepository.findSensor(mSensorsArrayList.get(i).description.substring(0,2)); if (current != null) { if (mSensorsArrayList.get(i).checked == true) { // create reading thread if (localDisplay_b == true) threads[j] = new HandlerThread(current, j); else threads[j] = new HandlerThread(current, 0); j++; // save setting in RMS HandlerManager.writeRMS("AIRS_local::" + current.Symbol, "On"); } else HandlerManager.writeRMS("AIRS_local::" + current.Symbol, "Off"); } } if (Reminder_i>0) Vibrator = new VibrateThread(); // if wakeup by user activity -> register receiver for screen on/off if (Wakeup_b == true) { IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); registerReceiver(mReceiver, filter); filter = new IntentFilter(Intent.ACTION_SCREEN_ON); registerReceiver(mReceiver, filter); registered = true; } else // otherwise get wake lock for keeping running all the time! { // create new wakelock PowerManager pm = (PowerManager)this.getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AIRS Local Lock"); wl.acquire(); } // need battery monitor? if (BatteryKill_i > 0) { // register intent for watching battery IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); registerReceiver(mReceiver, filter); registered = true; } // store persistently that AIRS is running HandlerManager.writeRMS_b("AIRS_local::running", true); running = true; // get current day and set time to last millisecond of that day Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.MILLISECOND, 999); int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); nextDay = cal.getTimeInMillis(); // mark date now as having values try { execStorage(0, "INSERT into airs_dates (Year, Month, Day, Types) VALUES ('"+ String.valueOf(year) + "','" + String.valueOf(month) + "','" + String.valueOf(day) + "','1')"); } catch(Exception e) { execStorage(0, "CREATE TABLE IF NOT EXISTS airs_dates (Year INT, Month INT, Day INT, Types INT);"); execStorage(0, "INSERT into airs_dates (Year, Month, Day, Types) VALUES ('"+ String.valueOf(year) + "','" + String.valueOf(month) + "','" + String.valueOf(day) + "','1')"); } // anything to send via intents? if (localIntent_b == true) { // send broadcast intent that AIRS has started Intent intent = new Intent("com.airs.local.started"); sendBroadcast(intent); } return true; } /** * Save all sensor selections in the UI sensor list as persistent values in the preference file */ public void saveSelections() { int i; Sensor current = null; // now create measurement threads for (i=0;i<mSensorsArrayList.size();i++) { current = SensorRepository.findSensor(mSensorsArrayList.get(i).description.substring(0,2)); if (current != null) { // save setting in RMS if (mSensorsArrayList.get(i).checked == true) HandlerManager.writeRMS("AIRS_local::" + current.Symbol, "On"); else HandlerManager.writeRMS("AIRS_local::" + current.Symbol, "Off"); } } } /** * Called to start all sensor handlers and inquire the discovered sensors from these handlers * The function also populates the UI sensor list and shows the appropriate view * This function needs calling from a UI thread, e.g., from AIRS_record_tab * @param airs Reference to the current {@link android.app.Activity} */ @SuppressWarnings("unchecked") public void Discover(Activity airs) { int i; String sensor_setting; Sensor current; this.airs = airs; // create timer/alarm handling Waker.init(this); // create handlers HandlerManager.createHandlers(this.getApplicationContext()); // start other stuff start_AIRS_local(); started = true; airs.setContentView(R.layout.sensors); // populate list sensors = (ListView)airs.findViewById(R.id.sensorList); sensors.setItemsCanFocus(false); sensors.setDividerHeight(2); sensors.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); mSensorsArrayList = new ArrayList<SensorEntry>(); mSensorsArrayAdapter = new MyCustomBaseAdapter(this); sensors.setAdapter(mSensorsArrayAdapter); // do discovery on all handlers SensorRepository.deleteSensor(); // run through all handlers and discover locally first for (i=0; i<HandlerManager.max_handlers;i++) { // is there any handler entry? if (HandlerManager.handlers[i] != null) // call discovery function of handler HandlerManager.handlers[i].Discover(); } // now build actual forms current = SensorRepository.root_sensor; i= 0 ; while(current != null) { // add new sensor choice field SensorEntry entry = new SensorEntry(); entry.description = current.Symbol + " (" + current.Description + ")"; entry.explanation = current.Explanation; // try to read RMS sensor_setting = HandlerManager.readRMS("AIRS_local::" + current.Symbol, "Off"); // set selected index to setting in RMS if (sensor_setting.compareTo("On") == 0) entry.checked = true; else entry.checked = false; mSensorsArrayList.add(entry); // count sensors i++; current = current.next; } Collections.sort(mSensorsArrayList, new SortBasedOnName()); // save number of sensors for later! numberSensors = i; // notify ListView of data set change! mSensorsArrayAdapter.notifyDataSetChanged(); // signal that it is discovered discovered = true; } /** * Similar to the Discover() function. However, no UI is served here as well as the handlers are assumed to have been created already. * This function is usually called in the restart modus. */ @SuppressWarnings("unchecked") private void ReDiscover() { int i; String sensor_setting; Sensor current; airs = this.getApplicationContext(); sensors = (ListView)new ListView(this.getApplicationContext()); sensors.setItemsCanFocus(false); sensors.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); sensors.setDividerHeight(2); mSensorsArrayList = new ArrayList<SensorEntry>(); mSensorsArrayAdapter = new MyCustomBaseAdapter(this); sensors.setAdapter(mSensorsArrayAdapter); // do discovery on all handlers SensorRepository.deleteSensor(); // run through all handlers and discover locally first for (i=0; i<HandlerManager.max_handlers;i++) { // is there any handler entry? if (HandlerManager.handlers[i] != null) // call discovery function of handler HandlerManager.handlers[i].Discover(); } // now build actual forms current = SensorRepository.root_sensor; i= 0 ; while(current != null) { // add new sensor choice field SensorEntry entry = new SensorEntry(); entry.description = current.Symbol + " (" + current.Description + ")"; entry.explanation = current.Explanation; // try to read RMS sensor_setting = HandlerManager.readRMS("AIRS_local::" + current.Symbol, "Off"); // set selected index to setting in RMS if (sensor_setting.compareTo("On") == 0) entry.checked = true; else entry.checked = false; mSensorsArrayList.add(entry); // count sensors i++; current = current.next; } Collections.sort(mSensorsArrayList, new SortBasedOnName()); // save number of sensors for later! numberSensors = i; // notify ListView of data set change! mSensorsArrayAdapter.notifyDataSetChanged(); // signal that it is discovered discovered = true; } /** * Restarts the AIRS_local recording service, calling the Rediscover() function, restarting all recordings threads and then make the service sticky * @param check Should the service been running (true) and was it therefore restarted? */ public void Restart(boolean check) { boolean p_running; SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()); // return if it's already running if (running == true) return; try { p_running = settings.getBoolean("AIRS_local::running", false); // should the service been running and was it therefore re-started? if (p_running == check) { SerialPortLogger.debugForced("AIRS_local::service was running before, trying to restart recording!"); if (started == false) { // set debug state SerialPortLogger.setDebugging(settings.getBoolean("Debug", false)); // init Waker resources Waker.init(this); // create handlers HandlerManager.createHandlers(this.getApplicationContext()); started = true; // start other stuff start_AIRS_local(); } SerialPortLogger.debugForced("AIRS_local::re-created handlers"); // re-discover the sensors ReDiscover(); SerialPortLogger.debugForced("AIRS_local::re-discovered sensors"); // start the measurements as being restarted, i.e., it takes the latest discovery and selection that is stored persistently running = startMeasurements(check); SerialPortLogger.debugForced("AIRS_local::re-started measurements"); // restart service in order to make it service startService(new Intent(this, AIRS_local.class)); SerialPortLogger.debugForced("AIRS_local::re-started service to make it sticky"); } } catch(Exception e) { SerialPortLogger.debugForced("AIRS_local::Restart():ERROR " + "Exception: " + e.toString()); } } // Vibrate watchdog private class VibrateThread implements Runnable { public Thread thread; public boolean stop = false; VibrateThread() { // save thread for later to stop (thread = new Thread(this)).start(); } public void run() { long vibration[] = {0,200,0}; // get power manager PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); // get notification manager mNotificationManager = (NotificationManager)airs.getSystemService(Context.NOTIFICATION_SERVICE); while (stop == false) { // sleep for the agreed time try { Thread.sleep(Reminder_i); // only shows "still running" notification when screen is off if (pm.isScreenOn() == false) { // prepare notification to user by changing the existing notification if (Vibrate == true) notification.vibrate = vibration; if (Lights == true) { notification.ledOffMS = 0; notification.ledOnMS = 750; notification.ledARGB = 0xff000000 | Integer.decode(LightCode); // Integer.valueOf(LightCode, 16); notification.flags |= Notification.FLAG_SHOW_LIGHTS; } // now shoot off alert by updating the existing recording notification mNotificationManager.notify(1, notification); sleep(750); // switch off vibrate and lights and update notification again notification.vibrate = null; notification.flags &= ~Notification.FLAG_SHOW_LIGHTS; mNotificationManager.notify(1, notification); } } catch(Exception e) { debug("AIRS_local::Vibrate thread terminated : " + e.toString()); } } } } // The Handler that gets information back from the other threads, updating the values for the UI private final Handler mHandler = new Handler() { @SuppressWarnings("deprecation") @Override public void handleMessage(Message msg) { String position; int j; // shutting down? if (shutdown == true) return; switch (msg.what) { case REFRESH_VALUES: // parse line from message j = msg.getData().getInt(LINE); // refresh appropriate line with given text mValuesArrayAdapter.setNotifyOnChange(false); position = mValuesArrayAdapter.getItem(j); mValuesArrayAdapter.insert(msg.getData().getString(TEXT), j); mValuesArrayAdapter.remove(position); mValuesArrayAdapter.notifyDataSetChanged(); break; case SHOW_NOTIFICATION: Toast.makeText(getApplicationContext(), msg.getData().getString(TEXT), Toast.LENGTH_LONG).show(); break; case BATTERY_KILL: // stop foreground service stopForeground(true); // now create new notification NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); Notification notification = new Notification(R.drawable.notification_icon, getString(R.string.AIRS_killed), System.currentTimeMillis()); Intent notificationIntent = new Intent(getApplicationContext(), AIRS_tabs.class); PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, notificationIntent, Intent.FLAG_ACTIVITY_NEW_TASK); notification.setLatestEventInfo(getApplicationContext(), getString(R.string.AIRS_Local_Sensing), getString(R.string.killed_at) + " " + Integer.toString(BatteryKill_i) + "% " + getString(R.string.battery) + "...", contentIntent); // give full fanfare notification.flags |= Notification.DEFAULT_SOUND | Notification.FLAG_AUTO_CANCEL | Notification.FLAG_SHOW_LIGHTS; notification.ledARGB = 0xffff0000; notification.ledOffMS = 1000; notification.ledOnMS = 1000; mNotificationManager.notify(17, notification); // stop service now! stopSelf(); break; default: break; } } }; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int Battery = 100; // screen off -> pause sensing if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) pause_threads(); // screen on -> resume sensing if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) resume_threads(); // if battery changed... if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) { int rawlevel = intent.getIntExtra("level", -1); int scale = intent.getIntExtra("scale", -1); if (rawlevel >= 0 && scale > 0) Battery = (rawlevel * 100) / scale; // need to trigger battery kill action? if (Battery < BatteryKill_i) { Message msg = mHandler.obtainMessage(BATTERY_KILL); mHandler.sendMessage(msg); } } } }; private class ViewHolder { TextView description; TextView explanation; CheckBox checked; } private class SensorEntry { String description; String explanation; boolean checked; } @SuppressWarnings("rawtypes") public class SortBasedOnName implements Comparator { public int compare(Object o1, Object o2) { return ((SensorEntry)o1).description.compareTo(((SensorEntry)o2).description); } } // Custom adapter for two line text list view with imageview (icon), defined in handlerentry.xml private class MyCustomBaseAdapter extends BaseAdapter { private LayoutInflater mInflater; public MyCustomBaseAdapter(Context context) { mInflater = LayoutInflater.from(context); } public int getCount() { return mSensorsArrayList.size(); } public SensorEntry getItem(int position) { return mSensorsArrayList.get(position); } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { SensorEntry current; ViewHolder holder; // get current sensor entry current = mSensorsArrayList.get(position); if (convertView == null) { // inflate view convertView = mInflater.inflate(R.layout.sensorselection, null); // now get all view information holder = new ViewHolder(); holder.description = (TextView) convertView.findViewById(R.id.selection_description); holder.explanation = (TextView) convertView.findViewById(R.id.selection_explanation); holder.checked = (CheckBox)convertView.findViewById(R.id.selection_check); // and save holder entry in tag convertView.setTag(holder); } else holder = (ViewHolder)convertView.getTag(); // set view information holder.description.setText(current.description); holder.explanation.setText(current.explanation); // take care that we get notified of user selections holder.checked.setTag(current); holder.checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { ((SensorEntry)buttonView.getTag()).checked = isChecked; } }); holder.checked.setChecked(current.checked); return convertView; } } }