/************************************************************************************************** * Copyright (C) 2010 Sense Observation Systems, Rotterdam, the Netherlands. All rights reserved. * *************************************************************************************************/ package nl.sense_os.service; import java.net.URLEncoder; import java.util.Map; import nl.sense_os.service.commonsense.SenseApi; import nl.sense_os.service.constants.SensePrefs; import nl.sense_os.service.constants.SensePrefs.Auth; import nl.sense_os.service.constants.SensePrefs.Main.Advanced; import nl.sense_os.service.constants.SensePrefs.Status; import nl.sense_os.service.constants.SenseUrls; import nl.sense_os.service.ctrl.Controller; import nl.sense_os.service.provider.SNTP; import nl.sense_os.service.scheduler.ScheduleAlarmTool; import org.json.JSONObject; import android.app.Activity; import android.app.Notification; import android.app.Service; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.util.Log; import android.widget.Toast; /** * Main Sense service class.<br/> * <br/> * Activities can bind to this service and call functions to: * <ul> * <li>log in;</li> * <li>register;</li> * <li>start sensing;</li> * <li>start/stop individual sensor modules;</li> * <li>set and get properties;</li> * </ul> * When the {@link #toggleMain(boolean)} method is called to start the sensing, the service starts * itself and registers itself as a foreground service so it does not get easily killed by Android. * * @author Steven Mulder <steven@sense-os.nl> */ public class SenseService extends Service { /** * Class used for the client Binder. Because we know this service always runs in the same * process as its clients, we don't need to deal with IPC. * * @see http://developer.android.com/guide/components/bound-services.html */ public class SenseBinder extends Binder { public SenseServiceStub getService() { return new SenseServiceStub(SenseService.this); } } private static final String TAG = "Sense Service"; /** * Intent action to force a re-login attempt when the service is started. */ public static final String EXTRA_RELOGIN = "relogin"; /** * Intent action to notify that the service is started, * boolean extra for of the status changed broadcast R.string.action_sense_service_broadcast. */ public static final String EXTRA_SERVICE_STARTED = "service_started"; private IBinder binder = new SenseBinder(); private ServiceStateHelper state; private Controller controller; private DataTransmitter transmitter; /** * Handler on main application thread to display toasts to the user. */ private static Handler toastHandler = new Handler(Looper.getMainLooper()); private static Handler initHandler; /** * Changes login of the Sense service. Removes "private" data of the previous user from the * preferences. Can be called by Activities that are bound to the service. * * @param username * Username * @param password * Hashed password * @return 0 if login completed successfully, -2 if login was forbidden, and -1 for any other * errors. */ int changeLogin(String username, String password) { Log.v(TAG, "Change login"); logout(); // save new username and password in the preferences Editor authEditor = getSharedPreferences(SensePrefs.AUTH_PREFS, MODE_PRIVATE).edit(); authEditor.putString(Auth.LOGIN_USERNAME, username); authEditor.putString(Auth.LOGIN_PASS, password); authEditor.commit(); return login(); } /** * Checks if the installed Sense Platform application has an update available, alerting the user * via a Toast message. */ private void checkVersion() { try { String packageName = getPackageName(); if ("nl.sense_os.app".equals(packageName)) { PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); String versionName = URLEncoder.encode(packageInfo.versionName, "UTF-8"); Log.i(TAG, "Running Sense App version '" + versionName + "'"); if (versionName.contains("unstable") || versionName.contains("testing")) { return; } String url = SenseUrls.VERSION + "?version=" + versionName; Map<String, String> response = SenseApi.request(this, url, null, null); JSONObject content = new JSONObject(response.get(SenseApi.RESPONSE_CONTENT)); if (content.getString("message").length() > 0) { Log.i(TAG, "Newer Sense App version available: " + content.toString()); showToast(content.getString("message")); } } else { // this is a third party app } } catch (Exception e) { Log.w(TAG, "Failed to get Sense App version: " + e); } } /** * Tries to login using the username and password from the private preferences and updates the * {@link #isLoggedIn} status accordingly. Can also be called from Activities that are bound to * the service. * * @return 0 if login completed successfully, -2 if login was forbidden, and -1 for any other * errors. */ synchronized int login() { if (state.isLoggedIn()) { // we are already logged in Log.v(TAG, "Skip login: already logged in"); return 0; } // check that we are actually allowed to log in SharedPreferences mainPrefs = getSharedPreferences(SensePrefs.MAIN_PREFS, MODE_PRIVATE); boolean allowed = mainPrefs.getBoolean(Advanced.USE_COMMONSENSE, true); if (!allowed) { Log.w(TAG, "Not logging in. Use of CommonSense is disabled."); return -1; } Log.v(TAG, "Try to log in"); // get login parameters from the preferences SharedPreferences authPrefs = getSharedPreferences(SensePrefs.AUTH_PREFS, MODE_PRIVATE); final String username = authPrefs.getString(Auth.LOGIN_USERNAME, null); final String pass = authPrefs.getString(Auth.LOGIN_PASS, null); // try to log in int result = -1; if ((username != null) && (pass != null)) { try { result = SenseApi.login(this, username, pass); } catch (Exception e) { Log.w(TAG, "Exception during login! " + e + ": '" + e.getMessage() + "'"); // handle result below } } else { Log.w(TAG, "Cannot login: username or password unavailable"); } // handle the result switch (result) { case 0: // logged in successfully onLogIn(); break; case -1: // error Log.w(TAG, "Login failed!"); onLogOut(); break; case -2: // forbidden Log.w(TAG, "Login forbidden!"); onLogOut(); break; default: Log.e(TAG, "Unexpected login result: " + result); onLogOut(); } return result; } void logout() { Log.v(TAG, "Log out"); // clear cached settings of the previous user (e.g. device id) Editor authEditor = getSharedPreferences(SensePrefs.AUTH_PREFS, MODE_PRIVATE).edit(); authEditor.clear(); authEditor.commit(); // log out before changing to a new user onLogOut(); } @Override public IBinder onBind(Intent intent) { Log.v(TAG, "Some component is binding to Sense Platform service"); return binder; } /** * Does nothing except poop out a log message. The service is really started in onStart, * otherwise it would also start when an activity binds to it. * * {@inheritDoc} */ @Override public void onCreate() { Log.v(TAG, "Sense Platform service is being created"); state = ServiceStateHelper.getInstance(this); } /** * Stops sensing, logs out, removes foreground status. * * {@inheritDoc} */ @Override public void onDestroy() { Log.v(TAG, "Sense Platform service is being destroyed"); // update login status // onLogOut(); // stop the main service stopForeground(true); super.onDestroy(); } /** * Performs tasks after successful login: update status bar notification; start transmitting * collected sensor data and register the gcm_id. */ private void onLogIn() { Log.i(TAG, "Logged in."); // update ntp time SNTP.getInstance().requestTime(SNTP.HOST_WORLDWIDE, 2000); // update login status state.setLoggedIn(true); state.setStarted(true); // store this login SharedPreferences prefs = getSharedPreferences(SensePrefs.MAIN_PREFS, MODE_PRIVATE); prefs.edit().putLong(SensePrefs.Main.LAST_LOGGED_IN, System.currentTimeMillis()).commit(); checkVersion(); onSyncRateChange(); //called to start the scheduler } /** * Performs cleanup tasks when the service is logged out: updates the status bar notification; * stops the periodic alarms for data transmission. */ private void onLogOut() { Log.i(TAG, "Logged out"); // update login status state.setLoggedIn(false); transmitter = DataTransmitter.getInstance(this); transmitter.stopTransmissions(); // completely stop the MsgHandler service stopService(new Intent(this, MsgHandler.class)); } void onSampleRateChange() { Log.v(TAG, "Sample rate changed"); if (state.isStarted()) { ScheduleAlarmTool.getInstance(this).resetNextExecution(); } } /** * Starts the Sense service. Tries to log in and start sensing; starts listening for network * connectivity broadcasts. * * @param intent * The Intent supplied to {@link Activity#startService(Intent)}. This may be null if * the service is being restarted after its process has gone away. * @param flags * Additional data about this start request. Currently either 0, * {@link Service#START_FLAG_REDELIVERY} , or {@link Service#START_FLAG_RETRY}. * @param startId * A unique integer representing this specific request to start. Use with * {@link #stopSelfResult(int)}. */ @Override public int onStartCommand(final Intent intent, int flags, int startId) { Log.i(TAG, "Sense Platform service is being started"); if (null == initHandler) { HandlerThread startThread = new HandlerThread("Start thread"); startThread.start(); initHandler = new Handler(startThread.getLooper()); } initHandler.post(new Runnable() { @Override public void run() { boolean mainStatus = getSharedPreferences(SensePrefs.STATUS_PREFS, MODE_PRIVATE) .getBoolean(Status.MAIN, true); if (false == mainStatus) { Log.w(TAG, "Sense service was started when the main status is not set!"); AliveChecker.stopChecks(SenseService.this); stopForeground(true); state.setForeground(false); } else { // make service as important as regular activities if (false == state.isForeground()) { Notification n = state.getStateNotification(); startForeground(ServiceStateHelper.NOTIF_ID, n); state.setForeground(true); AliveChecker.scheduleChecks(SenseService.this); } // re-login if necessary boolean relogin = !state.isLoggedIn(); relogin |= (null == intent); // intent is null when Service // was killed relogin |= (null != intent) && intent.getBooleanExtra(EXTRA_RELOGIN, false); if (relogin) { login(); } else { checkVersion(); } } } }); return START_STICKY; } void onSyncRateChange() { Log.v(TAG, "Sync rate changed"); if (state.isStarted()) { controller = Controller.getController(this); transmitter = DataTransmitter.getInstance(this); transmitter.stopTransmissions(); ScheduleAlarmTool.getInstance(this).resetNextExecution(); controller.scheduleTransmissions(); } // update any widgets // startService(new Intent(getString(R.string.action_widget_update))); } /** * Tries to register a new user using the username and password from the private preferences and * updates the {@link #isLoggedIn} status accordingly. Can also be called from Activities that * are bound to the service. * * @param username * @param password * Hashed password * @param email * @param address * @param zipCode * @param country * @param name * @param surname * @param mobile * @return 0 if registration completed successfully, -2 if the user already exists, and -1 for * any other unexpected responses. */ synchronized int register(String username, String password, String email, String address, String zipCode, String country, String name, String surname, String mobile) { Log.v(TAG, "Try to register new user"); // log out before registering a new user logout(); // save username and password in preferences Editor authEditor = getSharedPreferences(SensePrefs.AUTH_PREFS, MODE_PRIVATE).edit(); authEditor.putString(Auth.LOGIN_USERNAME, username); authEditor.putString(Auth.LOGIN_PASS, password); authEditor.commit(); // try to register int registered = -1; if ((null != username) && (null != password)) { // Log.v(TAG, "Registering: " + username + // ", password hash: " + hashPass); try { registered = SenseApi.registerUser(this, username, password, name, surname, email, mobile); } catch (Exception e) { Log.w(TAG, "Exception during registration: '" + e.getMessage() + "'. Connection problems?"); // handle result below } } else { Log.w(TAG, "Cannot register: username or password unavailable"); } // handle result switch (registered) { case 0: Log.i(TAG, "Successful registration for '" + username + "'"); login(); break; case -1: Log.w(TAG, "Registration failed"); state.setLoggedIn(false); break; case -2: Log.w(TAG, "Registration failed: user already exists"); state.setLoggedIn(false); break; default: Log.w(TAG, "Unexpected registration result: " + registered); } return registered; } /** * Displays a Toast message using the process's main Thread. * * @param message * Toast message to display to the user */ private void showToast(final String message) { toastHandler.post(new Runnable() { @Override public void run() { Toast.makeText(SenseService.this, message, Toast.LENGTH_LONG).show(); } }); } }