package org.commcare.dalvik.services; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.android.crypt.CipherPool; import org.commcare.android.crypt.CryptUtil; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.user.CommCareUserOpenHelper; import org.commcare.android.database.user.UserSandboxUtils; import org.commcare.android.database.user.models.User; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.tasks.DataSubmissionListener; import org.commcare.android.tasks.ProcessAndSendTask; import org.commcare.android.util.SessionUnavailableException; import org.commcare.dalvik.R; import org.commcare.dalvik.activities.CommCareHomeActivity; import org.commcare.dalvik.activities.LoginActivity; import org.commcare.dalvik.application.CommCareApplication; import org.javarosa.core.services.Logger; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.text.format.DateFormat; import android.widget.RemoteViews; /** * The CommCare Session Service is a persistent service which maintains * a CommCare login session * * @author ctsims * */ public class CommCareSessionService extends Service { private NotificationManager mNM; private static long MAINTENANCE_PERIOD = 1000; private static long SESSION_LENGTH = 1000*60*60*24; private Timer maintenanceTimer; private CipherPool pool; private byte[] key = null; private boolean multimediaIsVerified=false; private Date sessionExpireDate; private Object lock = new Object(); private User user; private SQLiteDatabase userDatabase; // Unique Identification Number for the Notification. // We use it on Notification start, and to cancel it. private int NOTIFICATION = org.commcare.dalvik.R.string.notificationtitle; private int SUBMISSION_NOTIFICATION = org.commcare.dalvik.R.string.submission_notification_title; /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ public class LocalBinder extends Binder { public CommCareSessionService getService() { return CommCareSessionService.this; } } /* * (non-Javadoc) * @see android.app.Service#onCreate() */ @Override public void onCreate() { mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); pool = new CipherPool() { /* * (non-Javadoc) * @see org.commcare.android.crypt.CipherPool#generateNewCipher() */ @Override public Cipher generateNewCipher() { synchronized(lock) { try { synchronized(key) { SecretKeySpec spec = new SecretKeySpec(key, "AES"); Cipher decrypter = Cipher.getInstance("AES"); decrypter.init(Cipher.DECRYPT_MODE, spec); return decrypter; } } catch (InvalidKeyException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchPaddingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } }; } /* * (non-Javadoc) * @see android.app.Service#onStartCommand(android.content.Intent, int, int) */ @Override public int onStartCommand(Intent intent, int flags, int startId) { // We want this service to continue running until it is explicitly // stopped, so return sticky. return START_STICKY; } /* * (non-Javadoc) * @see android.app.Service#onDestroy() */ @Override public void onDestroy() { // Cancel the persistent notification. this.stopForeground(true); // TODO: Create a notification which the user can click to restart the session } /* * (non-Javadoc) * @see android.app.Service#onBind(android.content.Intent) */ @Override public IBinder onBind(Intent intent) { return mBinder; } // This is the object that receives interactions from clients. See // RemoteService for a more complete example. private final IBinder mBinder = new LocalBinder(); /** * Show a notification while this service is running. */ private void showLoggedInNotification(User user) { //mNM.cancel(org.commcare.dalvik.R.string.expirenotification); CharSequence text = "Session Expires: " + DateFormat.format("MMM dd h:mmaa", sessionExpireDate); // Set the icon, scrolling text and timestamp Notification notification = new Notification(org.commcare.dalvik.R.drawable.notification, text, System.currentTimeMillis()); //We always want this click to simply bring the live stack back to the top Intent callable = new Intent(this, CommCareHomeActivity.class); callable.setAction("android.intent.action.MAIN"); callable.addCategory("android.intent.category.LAUNCHER"); // The PendingIntent to launch our activity if the user selects this notification PendingIntent contentIntent = PendingIntent.getActivity(this, 0, callable, 0); // Set the info for the views that show in the notification panel. notification.setLatestEventInfo(this, this.getString(org.commcare.dalvik.R.string.notificationtitle), text, contentIntent); if(user != null) { //Send the notification. this.startForeground(NOTIFICATION, notification); } } /* * Notify the user that they've been timed out and need to relog in */ private void showLoggedOutNotification() { this.stopForeground(true); String text = "Click here to log back into your session"; // Set the icon, scrolling text and timestamp Notification notification = new Notification(org.commcare.dalvik.R.drawable.notification, text, System.currentTimeMillis()); // The PendingIntent to launch our activity if the user selects this notification Intent i = new Intent(this, LoginActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i, notification.flags |= Notification.FLAG_AUTO_CANCEL); // Set the info for the views that show in the notification panel. notification.setLatestEventInfo(this, this.getString(org.commcare.dalvik.R.string.expirenotification), text, contentIntent); // Send the notification. mNM.notify(NOTIFICATION, notification); } //Start CommCare Specific Functionality public SQLiteDatabase getUserDbHandle() { synchronized(lock){ return userDatabase; } } public void prepareStorage(byte[] symetricKey, UserKeyRecord record) { synchronized(lock){ this.key = symetricKey; pool.init(); if(userDatabase != null && userDatabase.isOpen()) { userDatabase.close(); } userDatabase = new CommCareUserOpenHelper(CommCareApplication._(), record.getUuid()).getWritableDatabase(UserSandboxUtils.getSqlCipherEncodedKey(key)); } } public void logIn(User user) { synchronized(lock){ if(user != null) { Logger.log(AndroidLogger.TYPE_USER, "login|" + user.getUsername() + "|" + user.getUniqueId()); //Let anyone who is listening know! Intent i = new Intent("org.commcare.dalvik.api.action.session.login"); this.sendBroadcast(i); } this.user = user; this.sessionExpireDate = new Date(new Date().getTime() + SESSION_LENGTH); // Display a notification about us starting. We put an icon in the status bar. showLoggedInNotification(user); maintenanceTimer = new Timer("CommCareService"); maintenanceTimer.schedule(new TimerTask() { /* * (non-Javadoc) * @see java.util.TimerTask#run() */ @Override public void run() { maintenance(); } }, MAINTENANCE_PERIOD, MAINTENANCE_PERIOD); } } private void maintenance() { boolean logout = false; long time = new Date().getTime(); // If we're either past the session expire time, or the session expires more than its period in the future, // we need to log the user out if(time > sessionExpireDate.getTime() || (sessionExpireDate.getTime() - time > SESSION_LENGTH )) { logout = true; } if(logout) { logout(); showLoggedOutNotification(); } } public void logout() { synchronized(lock){ key = null; String username = null; if(user != null) { username = user.getUsername(); //Let anyone who is listening know! Intent i = new Intent("org.commcare.dalvik.api.action.session.logout"); this.sendBroadcast(i); } String msg = username != null ? "Logging out user " + username : "Logging out service login"; Logger.log(AndroidLogger.TYPE_MAINTENANCE, msg); if(userDatabase != null && userDatabase.isOpen()) { userDatabase.close(); } userDatabase = null; user = null; //this is null if we aren't actually in the foreground if(maintenanceTimer != null) { maintenanceTimer.cancel(); } pool.expire(); this.stopForeground(true); } } public boolean isLoggedIn() { synchronized(lock){ if(key == null) { return false;} return true; } } public Cipher getEncrypter() throws SessionUnavailableException { synchronized(lock){ if(key == null) { throw new SessionUnavailableException(); } synchronized(key) { SecretKeySpec spec = new SecretKeySpec(key, "AES"); try{ Cipher encrypter = Cipher.getInstance("AES"); encrypter.init(Cipher.ENCRYPT_MODE, spec); return encrypter; } catch (InvalidKeyException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchPaddingException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } } } public CipherPool getDecrypterPool() throws SessionUnavailableException{ synchronized(lock){ if(key == null) { throw new SessionUnavailableException(); } return pool; } } public SecretKey createNewSymetricKey() { return CryptUtil.generateSymetricKey(CryptUtil.uniqueSeedFromSecureStatic(key)); } public User getLoggedInUser() throws SessionUnavailableException { if(user == null) { throw new SessionUnavailableException(); } return user; } public DataSubmissionListener startDataSubmissionListener() { return this.startDataSubmissionListener(SUBMISSION_NOTIFICATION); } public DataSubmissionListener startDataSubmissionListener(final int notificationId) { return new DataSubmissionListener() { // START - Submission Listening Hooks int totalItems = -1; long currentSize = -1; long totalSent = -1; Notification submissionNotification; int lastUpdate = 0; /* * (non-Javadoc) * @see org.commcare.android.tasks.DataSubmissionListener#beginSubmissionProcess(int) */ @Override public void beginSubmissionProcess(int totalItems) { this.totalItems = totalItems; String text = getSubmissionText(1, totalItems); // Set the icon, scrolling text and timestamp submissionNotification = new Notification(org.commcare.dalvik.R.drawable.notification, getTickerText(1, totalItems), System.currentTimeMillis()); submissionNotification.flags |= (Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT); //We always want this click to simply bring the live stack back to the top Intent callable = new Intent(CommCareSessionService.this, CommCareHomeActivity.class); callable.setAction("android.intent.action.MAIN"); callable.addCategory("android.intent.category.LAUNCHER"); // The PendingIntent to launch our activity if the user selects this notification //TODO: Put something here that will, I dunno, cancel submission or something? Maybe show it live? PendingIntent contentIntent = PendingIntent.getActivity(CommCareSessionService.this, 0, callable, 0); RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.submit_notification); contentView.setImageViewResource(R.id.image, R.drawable.notification); contentView.setTextViewText(R.id.submitTitle, getString(notificationId)); contentView.setTextViewText(R.id.progressText, text); contentView.setTextViewText(R.id.submissionDetails,"0b transmitted"); // Set the info for the views that show in the notification panel. submissionNotification.setLatestEventInfo(CommCareSessionService.this, getString(notificationId), text, contentIntent); submissionNotification.contentView = contentView; if(user != null) { //Send the notification. mNM.notify(notificationId, submissionNotification); } } /* * (non-Javadoc) * @see org.commcare.android.tasks.DataSubmissionListener#startSubmission(int, long) */ @Override public void startSubmission(int itemNumber, long length) { currentSize = length; submissionNotification.contentView.setTextViewText(R.id.progressText, getSubmissionText(itemNumber + 1, totalItems)); submissionNotification.contentView.setProgressBar(R.id.submissionProgress, 100, 0, false); mNM.notify(notificationId, submissionNotification); } /* * (non-Javadoc) * @see org.commcare.android.tasks.DataSubmissionListener#notifyProgress(int, long) */ @Override public void notifyProgress(int itemNumber, long progress) { int progressPercent = (int)Math.floor((progress * 1.0 / currentSize) * 100); if(progressPercent - lastUpdate > 5) { String progressDetails = ""; if(progress < 1024) { progressDetails = progress + "b transmitted"; } else if (progress < 1024 * 1024) { progressDetails = String.format("%1$,.1f", (progress / 1024.0))+ "kb transmitted"; } else { progressDetails = String.format("%1$,.1f", (progress / (1024.0 * 1024.0)))+ "mb transmitted"; } int pending = ProcessAndSendTask.pending(); if(pending > 1) { submissionNotification.contentView.setTextViewText(R.id.submissionsPending, pending -1 + " Pending"); } submissionNotification.contentView.setTextViewText(R.id.submissionDetails,progressDetails); submissionNotification.contentView.setProgressBar(R.id.submissionProgress, 100, progressPercent, false); mNM.notify(notificationId, submissionNotification); lastUpdate = progressPercent; } } /* * (non-Javadoc) * @see org.commcare.android.tasks.DataSubmissionListener#endSubmissionProcess() */ @Override public void endSubmissionProcess() { mNM.cancel(notificationId); submissionNotification = null; totalItems = -1; currentSize = -1; totalSent = -1; lastUpdate = 0; } private String getSubmissionText(int current, int total) { return current + "/" + total; } private String getTickerText(int current, int total) { return "CommCare submitting " + total +" forms"; } // END - Submission Listening Hooks }; } public boolean isMultimediaVerified(){ return multimediaIsVerified; } public void setMultiMediaVerified(boolean toggle){ multimediaIsVerified = toggle; } }