package org.commcare.dalvik.application; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.Vector; import javax.crypto.SecretKey; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteException; import org.commcare.android.database.DbHelper; import org.commcare.android.database.DbUtil; import org.commcare.android.database.SqlStorage; import org.commcare.android.database.SqlStorageIterator; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.global.DatabaseGlobalOpenHelper; import org.commcare.android.database.global.models.ApplicationRecord; import org.commcare.android.database.user.CommCareUserOpenHelper; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.User; import org.commcare.android.db.legacy.LegacyInstallUtils; import org.commcare.android.javarosa.AndroidLogEntry; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.javarosa.PreInitLogger; import org.commcare.android.logic.GlobalConstants; import org.commcare.android.models.AndroidSessionWrapper; import org.commcare.android.models.notifications.NotificationClearReceiver; import org.commcare.android.models.notifications.NotificationMessage; import org.commcare.android.references.ArchiveFileRoot; import org.commcare.android.references.AssetFileRoot; import org.commcare.android.references.JavaHttpRoot; import org.commcare.android.storage.framework.Table; import org.commcare.android.tasks.ExceptionReportTask; import org.commcare.android.tasks.FormRecordCleanupTask; import org.commcare.android.tasks.LogSubmissionTask; import org.commcare.android.util.AndroidCommCarePlatform; import org.commcare.android.util.CallInPhoneListener; import org.commcare.android.util.CommCareExceptionHandler; import org.commcare.android.util.FileUtil; import org.commcare.android.util.ODKPropertyManager; import org.commcare.android.util.SessionUnavailableException; import org.commcare.dalvik.R; import org.commcare.dalvik.activities.MessageActivity; import org.commcare.dalvik.activities.UnrecoverableErrorActivity; import org.commcare.dalvik.odk.provider.InstanceProviderAPI.InstanceColumns; import org.commcare.dalvik.preferences.CommCarePreferences; import org.commcare.dalvik.services.CommCareSessionService; import org.commcare.suite.model.Profile; import org.commcare.util.CommCareSession; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.core.reference.RootTranslator; import org.javarosa.core.services.Logger; import org.javarosa.core.services.PropertyManager; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.services.storage.EntityFilter; import org.javarosa.core.services.storage.Persistable; import org.javarosa.core.services.storage.StorageFullException; import org.javarosa.core.util.PropertyUtils; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.Externalizable; import org.odk.collect.android.application.Collect; import android.annotation.SuppressLint; import android.app.Application; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.provider.Settings.Secure; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.util.Log; import android.util.Pair; import android.widget.Toast; /** * @author ctsims * */ public class CommCareApplication extends Application { public static final int STATE_UNINSTALLED = 0; public static final int STATE_UPGRADE = 1; public static final int STATE_READY = 2; public static final int STATE_CORRUPTED = 4; public static final String ACTION_PURGE_NOTIFICATIONS = "CommCareApplication_purge"; private int dbState; private int resourceState; private static CommCareApplication app; private CommCareApp currentApp; private AndroidSessionWrapper sessionWrapper; /** Generalize **/ private Object globalDbHandleLock = new Object(); private SQLiteDatabase globalDatabase; //Kind of an odd way to do this boolean updatePending = false; private ArchiveFileRoot mArchiveFileRoot; /* * (non-Javadoc) * @see android.app.Application#onCreate() */ @Override public void onCreate() { super.onCreate(); Collect.setStaticApplicationContext(this); CommCareApplication.app = this; //TODO: Make this robust PreInitLogger pil = new PreInitLogger(); Logger.registerLogger(pil); //Workaround because android is written by 7 year olds. //(reuses http connection pool improperly, so the second https //request in a short time period will flop) System.setProperty("http.keepAlive", "false"); Thread.setDefaultUncaughtExceptionHandler(new CommCareExceptionHandler(Thread.getDefaultUncaughtExceptionHandler())); PropertyManager.setPropertyManager(new ODKPropertyManager()); SQLiteDatabase.loadLibs(this); setRoots(); prepareTemporaryStorage(); //Init global storage (Just application records, logs, etc) dbState = initGlobalDb(); //This is where we go through and check for updates between major transitions. //Soon we should start doing this differently, and actually go to an activity //first which tells the user what's going on. // //The rule about this transition is that if the user had logs pending, we still want them in order, so //we aren't going to dump our logs from the Pre-init logger until after this transition occurs. try { LegacyInstallUtils.checkForLegacyInstall(this, this.getGlobalStorage(ApplicationRecord.class)); } catch(StorageFullException sfe) { throw new RuntimeException(sfe); } finally { //No matter what happens, set up our new logger, we want those logs! Logger.registerLogger(new AndroidLogger(this.getGlobalStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class))); pil.dumpToNewLogger(); } // PreferenceChangeListener listener = new PreferenceChangeListener(this); // PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(listener); intializeDefaultLocalizerData(); //The fallback in case the db isn't installed resourceState = STATE_UNINSTALLED; //We likely want to do this for all of the storage, this is just a way to deal with fixtures //temporarily. //StorageManager.registerStorage("fixture", this.getStorage("fixture", FormInstance.class)); // Logger.registerLogger(new AndroidLogger(CommCareApplication._().getStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class))); // // //Dump any logs we've been keeping track of in memory to storage // pil.dumpToNewLogger(); resourceState = initializeAppResources(); } public void triggerHandledAppExit(Context c, String message) { triggerHandledAppExit(c, message, Localization.get("app.handled.error.title")); } public void triggerHandledAppExit(Context c, String message, String title) { Intent i = new Intent(c, UnrecoverableErrorActivity.class); i.putExtra(UnrecoverableErrorActivity.EXTRA_ERROR_TITLE, title); i.putExtra(UnrecoverableErrorActivity.EXTRA_ERROR_MESSAGE, message); //start a new stack and forget where we were (so we don't restart the app from there) i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_ACTIVITY_CLEAR_TOP); c.startActivity(i); } public void logout() { synchronized(serviceLock) { if(this.sessionWrapper != null) { sessionWrapper.reset(); } doUnbindService(); } } public void logIn(byte[] symetricKey, UserKeyRecord record) { synchronized(serviceLock) { if(this.mIsBound) { logout(); } doBindService(symetricKey, record); } } public SecretKey createNewSymetricKey() { return getSession().createNewSymetricKey(); } private CallInPhoneListener listener = null; private void attachCallListener() { TelephonyManager tManager = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE); listener = new CallInPhoneListener(this, this.getCommCarePlatform()); listener.startCache(); tManager.listen(listener, PhoneStateListener.LISTEN_CALL_STATE); } private void detachCallListener() { if(listener != null) { TelephonyManager tManager = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE); tManager.listen(listener, PhoneStateListener.LISTEN_NONE); listener = null; } } public CallInPhoneListener getCallListener() { return listener; } public int versionCode() { try { PackageManager pm = this.getPackageManager(); PackageInfo pi = pm.getPackageInfo(getPackageName(), 0); return pi.versionCode; } catch(NameNotFoundException e) { throw new RuntimeException("Android package name not available."); } } public int[] getCommCareVersion() { return this.getResources().getIntArray(R.array.commcare_version); } public AndroidCommCarePlatform getCommCarePlatform() { if(this.currentApp == null) { throw new RuntimeException("No App installed!!!"); } else { return this.currentApp.getCommCarePlatform(); } } public CommCareApp getCurrentApp() { return this.currentApp; } /** * Get the current CommCare session that's being executed * * @return * @throws SessionUnavailableException If there is no session current being * executed */ public CommCareSession getCurrentSession() { return getCurrentSessionWrapper().getSession(); } public AndroidSessionWrapper getCurrentSessionWrapper() { if(sessionWrapper == null) { throw new SessionUnavailableException(); } return sessionWrapper; } public int getDatabaseState() { return dbState; } public int getAppResourceState() { return resourceState; } public void initializeGlobalResources(CommCareApp app) { if(dbState != STATE_UNINSTALLED) { resourceState = initializeAppResources(app); } } public String getPhoneId() { TelephonyManager manager = (TelephonyManager)this.getSystemService(TELEPHONY_SERVICE); String imei = manager.getDeviceId(); if(imei == null) { imei = Secure.getString(getContentResolver(),Secure.ANDROID_ID); } return imei; } public void intializeDefaultLocalizerData() { Localization.init(true); Localization.registerLanguageReference("default", "jr://asset/locales/messages_ccodk_default.txt"); Localization.setDefaultLocale("default"); //For now. Possibly handle this better in the future Localization.setLocale("default"); } private void setRoots() { JavaHttpRoot http = new JavaHttpRoot(); AssetFileRoot afr = new AssetFileRoot(this); ArchiveFileRoot arfr = new ArchiveFileRoot(); mArchiveFileRoot = arfr; ReferenceManager._().addReferenceFactory(http); ReferenceManager._().addReferenceFactory(afr); ReferenceManager._().addReferenceFactory(arfr); ReferenceManager._().addRootTranslator(new RootTranslator("jr://media/",GlobalConstants.MEDIA_REF)); } private int initializeAppResources() { //There should be exactly one of these for now for(ApplicationRecord record : getGlobalStorage(ApplicationRecord.class)) { if(record.getStatus() == ApplicationRecord.STATUS_INSTALLED) { //We have an app record ready to go return initializeAppResources(new CommCareApp(record)); } } return STATE_UNINSTALLED; } private int initializeAppResources(CommCareApp app) { try { currentApp = app; if(currentApp.initializeApplication()) { return STATE_READY; } else { //???? return STATE_CORRUPTED; } } catch(Exception e) { Log.i("FAILURE", "Problem with loading"); Log.i("FAILURE", "E: " + e.getMessage()); e.printStackTrace(); ExceptionReportTask ert = new ExceptionReportTask(); ert.execute(e); return STATE_CORRUPTED; } } private int initGlobalDb() { SQLiteDatabase database; try { database = new DatabaseGlobalOpenHelper(this).getWritableDatabase("null"); database.close(); return STATE_READY; } catch(SQLiteException e) { //Only thrown in DB isn't there return STATE_UNINSTALLED; } } public SQLiteDatabase getUserDbHandle() { return this.getSession().getUserDbHandle(); } public <T extends Persistable> SqlStorage<T> getGlobalStorage(Class<T> c) { return getGlobalStorage(c.getAnnotation(Table.class).value(), c); } public <T extends Persistable> SqlStorage<T> getGlobalStorage(String table, Class<T> c) { return new SqlStorage<T>(table, c, new DbHelper(this.getApplicationContext()){ /* * (non-Javadoc) * @see org.commcare.android.database.DbHelper#getHandle() */ @Override public SQLiteDatabase getHandle() { synchronized(globalDbHandleLock) { if(globalDatabase == null || !globalDatabase.isOpen()) { globalDatabase = new DatabaseGlobalOpenHelper(this.c).getWritableDatabase("null"); } return globalDatabase; } } }); } public <T extends Persistable> SqlStorage<T> getAppStorage(Class<T> c) throws SessionUnavailableException { return getAppStorage(c.getAnnotation(Table.class).value(), c); } public <T extends Persistable> SqlStorage<T> getAppStorage(String name, Class<T> c) throws SessionUnavailableException { return currentApp.getStorage(name, c); } public <T extends Persistable> SqlStorage<T> getUserStorage(Class<T> c) throws SessionUnavailableException { return getUserStorage(c.getAnnotation(Table.class).value(), c); } public <T extends Persistable> SqlStorage<T> getUserStorage(String storage, Class<T> c) throws SessionUnavailableException { return new SqlStorage<T>(storage, c, new DbHelper(this.getApplicationContext()){ /* * (non-Javadoc) * @see org.commcare.android.database.DbHelper#getHandle() */ @Override public SQLiteDatabase getHandle() { SQLiteDatabase database = getUserDbHandle(); if(database == null) { throw new NullPointerException("Somehow didn't get a database handle!"); } return database; } }); } public <T extends Persistable> SqlStorage<T> getRawStorage(String storage, Class<T> c, final SQLiteDatabase handle) { return new SqlStorage<T>(storage, c, new DbHelper(this.getApplicationContext()){ /* * (non-Javadoc) * @see org.commcare.android.database.DbHelper#getHandle() */ @Override public SQLiteDatabase getHandle() { return handle; } }); } public void serializeToIntent(Intent i, String name, Externalizable data) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { data.writeExternal(new DataOutputStream(baos)); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } i.putExtra(name, baos.toByteArray()); } public <T extends Externalizable> T deserializeFromIntent(Intent i, String name, Class<T> type) { if(!i.hasExtra(name)) { return null;} T t; try { t = type.newInstance(); t.readExternal(new DataInputStream(new ByteArrayInputStream(i.getByteArrayExtra(name))), DbUtil.getPrototypeFactory(this)); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (DeserializationException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (IllegalAccessException e1) { e1.printStackTrace(); throw new RuntimeException(e1); } catch (InstantiationException e1) { e1.printStackTrace(); throw new RuntimeException(e1); } return t; } /* * (non-Javadoc) * @see android.app.Application#onLowMemory() */ @Override public void onLowMemory() { super.onLowMemory(); } /* * (non-Javadoc) * @see android.app.Application#onTerminate() */ @Override public void onTerminate() { super.onTerminate(); } public static CommCareApplication _() { return app; } /** * This method goes through and identifies whether there are elements in the * database which point to/expect files to exist on the file system, and clears * out any records which refer to files that don't exist. */ public void cleanUpDatabaseFileLinkages() throws SessionUnavailableException{ Vector<Integer> toDelete = new Vector<Integer>(); SqlStorage<FormRecord> storage = getUserStorage(FormRecord.class); //Can't load the records outright, since we'd need to be logged in (The key is encrypted) for(SqlStorageIterator iterator = storage.iterate(); iterator.hasMore();) { int id = iterator.nextID(); String instanceRecordUri = storage.getMetaDataFieldForRecord(id, FormRecord.META_INSTANCE_URI); if(instanceRecordUri == null) { toDelete.add(id); continue; } //otherwise, grab this record and see if the file's around Cursor c = this.getContentResolver().query(Uri.parse(instanceRecordUri), new String[] { InstanceColumns.INSTANCE_FILE_PATH}, null, null, null); if(!c.moveToFirst()) { toDelete.add(id);} else { String path = c.getString(c.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)); if(path == null || !new File(path).exists()) { toDelete.add(id); } } c.close(); } for(int recordid : toDelete) { //this should go to the form record wipe cleanup task storage.remove(recordid); } } /** * This method wipes out all local user data (users, referrals, etc) but leaves * application resources in place. * * It makes no attempt to make sure this is a safe operation when called, so * it shouldn't be used lightly. */ public void clearUserData() throws SessionUnavailableException { // //First clear anything that will require the user's key, since we're going to wipe it out! // getStorage(ACase.STORAGE_KEY, ACase.class).removeAll(); // // //TODO: We should really be wiping out the _stored_ instances here, too // getStorage(FormRecord.STORAGE_KEY, FormRecord.class).removeAll(); // // //Also, any of the sessions we've got saved // getStorage(SessionStateDescriptor.STORAGE_KEY, SessionStateDescriptor.class).removeAll(); // // //Now we wipe out the user entirely // getStorage(User.STORAGE_KEY, User.class).removeAll(); // // //Get rid of any user fixtures // getStorage("fixture", FormInstance.class).removeAll(); // // getStorage(GeocodeCacheModel.STORAGE_KEY, GeocodeCacheModel.class).removeAll(); final String username = this.getSession().getLoggedInUser().getUsername(); final Set<String> dbIdsToRemove = new HashSet<String>(); this.getAppStorage(UserKeyRecord.class).removeAll(new EntityFilter<UserKeyRecord>() { /* * (non-Javadoc) * @see org.javarosa.core.services.storage.EntityFilter#matches(java.lang.Object) */ @Override public boolean matches(UserKeyRecord ukr) { if(ukr.getUsername().equalsIgnoreCase(username.toLowerCase())) { dbIdsToRemove.add(ukr.getUuid()); return true; } return false; } }); //TODO: We can just delete the db entirely. //Should be good to go. The app'll log us out now that there's no user details in memory logout(); Editor sharedPreferencesEditor = CommCareApplication._().getCurrentApp().getAppPreferences().edit(); sharedPreferencesEditor.putString(CommCarePreferences.LAST_LOGGED_IN_USER, null); sharedPreferencesEditor.commit(); for(String id : dbIdsToRemove) { //TODO: We only wanna do this if the user is the _last_ one with a key to this id, actually. //(Eventually) this.getDatabasePath(CommCareUserOpenHelper.getDbName(id)).delete(); } } public void prepareTemporaryStorage(){ String tempRoot = this.getAndroidFsTemp(); FileUtil.deleteFileOrDir(tempRoot); boolean success = FileUtil.createFolder(tempRoot); if(!success){ Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Couldn't create temp folder"); } } public String getCurrentVersionString() { PackageManager pm = this.getPackageManager(); PackageInfo pi; try { pi = pm.getPackageInfo(getPackageName(), 0); } catch (NameNotFoundException e) { e.printStackTrace(); return "ERROR! Incorrect package version requested"; } int[] versions = this.getCommCareVersion(); String ccv = ""; for(int vn: versions) { if(ccv != "") { ccv +="."; } ccv += vn; } String profileVersion = ""; Profile p = this.currentApp == null ? null : this.getCommCarePlatform().getCurrentProfile(); if(p != null) { profileVersion = String.valueOf(p.getVersion()); } String buildDate = getString(R.string.app_build_date); String buildNumber = getString(R.string.app_build_number); return Localization.get(getString(R.string.app_version_string), new String[] {pi.versionName, String.valueOf(pi.versionCode), ccv, buildNumber, buildDate, profileVersion}); } //Start Service code. Will be changed in the future private CommCareSessionService mBoundService; private ServiceConnection mConnection; private Object serviceLock = new Object(); boolean mIsBound = false; boolean mIsBinding = false; void doBindService(final byte[] key, final UserKeyRecord record) { mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // This is called when the connection with the service has been // established, giving us the service object we can use to // interact with the service. Because we have bound to a explicit // service that we know is running in our own process, we can // cast its IBinder to a concrete class and directly access it. User user = null; synchronized(serviceLock) { mBoundService = ((CommCareSessionService.LocalBinder)service).getService(); //Don't let anyone touch this until it's logged in mBoundService.prepareStorage(key, record); if(record != null) { //Ok, so we have a login that was successful, but do we have a user model in the DB? //We need to check before we're logged in, so we get the handle raw, here for(User u : getRawStorage("USER", User.class, mBoundService.getUserDbHandle()) ){ if(record.getUsername().equals(u.getUsername())) { user = u; } } } //service available mIsBound = true; //Don't signal bind completion until the db is initialized. mIsBinding = false; if(user != null) { getSession().logIn(user); attachCallListener(); CommCareApplication.this.sessionWrapper = new AndroidSessionWrapper(CommCareApplication.this.getCommCarePlatform()); //See if there's an auto-update pending. We only want to be able to turn this //to "True" on login, not any other time //TODO: this should be associated with the app itself, not the global settings updatePending = getPendingUpdateStatus(); syncPending = getPendingSyncStatus(); doReportMaintenance(false); //Register that this user was the last to successfully log in if it's a real user if(!User.TYPE_DEMO.equals(user.getUserType())) { getCurrentApp().getAppPreferences().edit().putString(CommCarePreferences.LAST_LOGGED_IN_USER, record.getUsername()).commit(); performArchivedFormPurge(getCurrentApp(), user); } } } } public void onServiceDisconnected(ComponentName className) { // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. // Because it is running in our same process, we should never // see this happen. mBoundService = null; } }; // Establish a connection with the service. We use an explicit // class name because we want a specific service implementation that // we know will be running in our own process (and thus won't be // supporting component replacement by other applications). bindService(new Intent(this, CommCareSessionService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBinding = true; } @SuppressLint("NewApi") protected void doReportMaintenance(boolean force) { //OK. So for now we're going to daily report sends and not bother with any of the frequency properties. //Create a new submission task no matter what. If nothing is pending, it'll see if there are unsent reports //and try to send them. Otherwise, it'll create the report SharedPreferences settings = CommCareApplication._().getCurrentApp().getAppPreferences(); String url = settings.getString("PostURL", null); if(url == null) { Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, "PostURL isn't set. This should never happen"); return; } LogSubmissionTask task = new LogSubmissionTask(this, force || isPending(settings.getLong(CommCarePreferences.LOG_LAST_DAILY_SUBMIT, 0), DateUtils.DAY_IN_MILLIS), CommCareApplication.this.getSession().startDataSubmissionListener(R.string.submission_logs_title), url); //Execute on a true multithreaded chain, since this is an asynchronous process if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ) { task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { task.execute(); } } private boolean getPendingUpdateStatus() { SharedPreferences preferences = getCurrentApp().getAppPreferences(); //Establish whether or not an AutoUpdate is Pending String autoUpdateFreq = preferences.getString(CommCarePreferences.AUTO_UPDATE_FREQUENCY, CommCarePreferences.FREQUENCY_NEVER); boolean autoUpdateTriggered = preferences.getBoolean(CommCarePreferences.AUTO_TRIGGER_UPDATE, false); if(autoUpdateTriggered){ preferences.edit().putBoolean(CommCarePreferences.AUTO_TRIGGER_UPDATE, false); return true; } //See if auto update is even turned on if(!autoUpdateFreq.equals(CommCarePreferences.FREQUENCY_NEVER)) { long lastUpdateCheck = preferences.getLong(CommCarePreferences.LAST_UPDATE_ATTEMPT, 0); long duration = (24*60*60*100) * (autoUpdateFreq == CommCarePreferences.FREQUENCY_DAILY ? 1 : 7); return isPending(lastUpdateCheck, duration); } return false; } /** * Check through user storage and identify whether there are any forms which can be purged * from the device. * * @param app The current app * @param user The user who's storage we're reviewing */ private void performArchivedFormPurge(CommCareApp app, User user) { int daysForReview = -1; String daysToPurge = app.getAppPreferences().getString("cc-days-form-retain", "-1"); try { daysForReview = Integer.parseInt(daysToPurge); } catch(NumberFormatException nfe) { Logger.log(AndroidLogger.TYPE_ERROR_CONFIG_STRUCTURE, "Invalid days to purge: " + daysToPurge); } //If we don't define a days for review flag, we should just keep the forms around //indefinitely if(daysForReview == -1) { return; } SqlStorage<FormRecord> forms = this.getUserStorage(FormRecord.class); //Get the last date for froms to be valid (n days prior to today) long lastValidDate = new Date().getTime() - daysForReview * 24 * 60 * 60 * 1000; Vector<Integer> toPurge = new Vector<Integer>(); //Get all saved forms currently in storage for(int id : forms.getIDsForValue(FormRecord.META_STATUS, FormRecord.STATUS_SAVED)) { String date = forms.getMetaDataFieldForRecord(id, FormRecord.META_LAST_MODIFIED); try { //If the date the form was saved is before the last valid date, we can purge it if(lastValidDate > Date.parse(date)) { toPurge.add(id); } } catch(Exception e) { //Catch all for now, we know that at least "" and null //are causing problems (neither of which should be acceptable //but if we see them, we should consider the form //purgable. toPurge.add(id); } } if(toPurge.size() > 0 ) { Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Purging " + toPurge.size() + " archived forms for being before the last valid date " + new Date(lastValidDate).toString()); //Actually purge the old forms for(int formRecord : toPurge) { FormRecordCleanupTask.wipeRecord(this, formRecord); } } } private boolean isPending(long last, long period) { Date current = new Date(); //There are a couple of conditions in which we want to trigger pending maintenance ops. long now = current.getTime(); //1) Straightforward - Time is greater than last + duration long diff = now - last; if( diff > period) { return true; } Calendar lastRestoreCalendar = Calendar.getInstance(); lastRestoreCalendar.setTimeInMillis(last); //2) For daily stuff, we want it to be the case that if the last time you synced was the day prior, //you still sync, so people can get into the cycle of doing it once in the morning, which //is more valuable than syncing mid-day. if(period == DateUtils.DAY_IN_MILLIS && (lastRestoreCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.getInstance().get(Calendar.DAY_OF_WEEK))) { return true; } //3) Major time change - (Phone might have had its calendar day manipulated). //for now we'll simply say that if last was more than a day in the future (timezone blur) //we should also trigger if(now < (last - DateUtils.DAY_IN_MILLIS)) { return true; } //TODO: maaaaybe trigger all if there's a substantial time difference //noted between calls to a server //Otherwise we're fine return false; } /** * Whether automated stuff like autoupdates/syncing are valid and should be triggered. * * @return */ private boolean areAutomatedActionsValid() { try { if(User.TYPE_DEMO.equals(getSession().getLoggedInUser().getUserType())) { return false; } } catch(SessionUnavailableException sue) { return false; } return true; } public boolean isUpdatePending() { if(!areAutomatedActionsValid()) { return false;} //We only set this to true occasionally, but in theory it could be set to false //from other factors, so turn it off if it is. if(getPendingUpdateStatus() == false) { updatePending = false; } return updatePending; } void doUnbindService() { synchronized(serviceLock) { if (mIsBound) { mIsBound = false; mBoundService.logout(); // Detach our existing connection. unbindService(mConnection); } } } //Milliseconds to wait for bind private static final int MAX_BIND_TIMEOUT = 5000; public CommCareSessionService getSession() throws SessionUnavailableException { long started = System.currentTimeMillis(); //If binding is currently in process, just wait for it. while(mIsBinding) { if(System.currentTimeMillis() - started > MAX_BIND_TIMEOUT) { //Something bad happened doUnbindService(); throw new SessionUnavailableException("Timeout binding to session service"); } } if(mIsBound) { synchronized(serviceLock) { return mBoundService; } } else { throw new SessionUnavailableException(); } } public Pair<Long, int[]> getSyncDisplayParameters() { SharedPreferences prefs = CommCareApplication._().getCurrentApp().getAppPreferences(); long lastSync = prefs.getLong("last-succesful-sync", 0); int unsentForms = this.getUserStorage(FormRecord.class).getIDsForValue(FormRecord.META_STATUS, FormRecord.STATUS_UNSENT).size(); int incompleteForms = this.getUserStorage(FormRecord.class).getIDsForValue(FormRecord.META_STATUS, FormRecord.STATUS_INCOMPLETE).size(); return new Pair<Long,int[]>(lastSync, new int[] {unsentForms, incompleteForms}); } // Start - Error message Hooks private int MESSAGE_NOTIFICATION = org.commcare.dalvik.R.string.notification_message_title; ArrayList<NotificationMessage> pendingMessages = new ArrayList<NotificationMessage>(); Handler toaster = new Handler(){ /* * (non-Javadoc) * @see android.os.Handler#handleMessage(android.os.Message) */ @Override public void handleMessage(Message m) { NotificationMessage message = m.getData().getParcelable("message"); Toast.makeText(CommCareApplication.this, Localization.get("notification.for.details.wrapper", new String[] {message.getTitle()}), Toast.LENGTH_LONG).show(); } }; public void reportNotificationMessage(NotificationMessage message) { reportNotificationMessage(message, false); } public void reportNotificationMessage(final NotificationMessage message, boolean notifyUser) { synchronized(pendingMessages) { //make sure there is no matching message pending for(NotificationMessage msg : pendingMessages) { if(msg.equals(message)) { //If so, bail. return; } } if(notifyUser) { Bundle b = new Bundle(); b.putParcelable("message", message); Message m = Message.obtain(toaster); m.setData(b); toaster.sendMessage(m); } //Otherwise, add it to the queue, and update the notification pendingMessages.add(message); updateMessageNotification(); } } public void updateMessageNotification() { NotificationManager mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); synchronized(pendingMessages) { if(pendingMessages.size() == 0) { mNM.cancel(MESSAGE_NOTIFICATION); return; } String title = pendingMessages.get(0).getTitle(); Notification messageNotification = new Notification(org.commcare.dalvik.R.drawable.notification, title, System.currentTimeMillis()); messageNotification.number = pendingMessages.size(); // The PendingIntent to launch our activity if the user selects this notification Intent i = new Intent(this, MessageActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i, 0); String additional = pendingMessages.size() > 1 ? Localization.get("notifications.prompt.more", new String[] {String.valueOf(pendingMessages.size() - 1)}) : ""; // Set the info for the views that show in the notification panel. messageNotification.setLatestEventInfo(this, title, Localization.get("notifications.prompt.details", new String[] {additional}), contentIntent); messageNotification.deleteIntent = PendingIntent.getBroadcast(this, 0, new Intent(this, NotificationClearReceiver.class), 0); //Send the notification. mNM.notify(MESSAGE_NOTIFICATION, messageNotification); } } public ArrayList<NotificationMessage> purgeNotifications() { synchronized(pendingMessages) { this.sendBroadcast(new Intent(ACTION_PURGE_NOTIFICATIONS)); ArrayList<NotificationMessage> cloned = (ArrayList<NotificationMessage>)pendingMessages.clone(); clearNotifications(null); return cloned; } } public void clearNotifications(String category) { synchronized(pendingMessages) { NotificationManager mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); Vector<NotificationMessage> toRemove = new Vector<NotificationMessage>(); for(NotificationMessage message : pendingMessages) { if(category == null || message.getCategory() == category) { toRemove.add(message); } } for(NotificationMessage message : toRemove) { pendingMessages.remove(message); } if(pendingMessages.size() == 0) { mNM.cancel(MESSAGE_NOTIFICATION); } else { updateMessageNotification(); } } } // End - Error Message Hooks private boolean syncPending = false; /** * @return True if there is a sync action pending. False otherwise. */ private boolean getPendingSyncStatus() { SharedPreferences prefs = CommCareApplication._().getCurrentApp().getAppPreferences(); long period = -1; //Old flag, use a day by default if("true".equals(prefs.getString("cc-auto-update","false"))) { period = DateUtils.DAY_IN_MILLIS;} //new flag, read what it is. String periodic = prefs.getString(CommCarePreferences.AUTO_SYNC_FREQUENCY,CommCarePreferences.FREQUENCY_NEVER); if(!periodic.equals(CommCarePreferences.FREQUENCY_NEVER)) { period = DateUtils.DAY_IN_MILLIS * (periodic.equals(CommCarePreferences.FREQUENCY_DAILY) ? 1 : 7); } //If we didn't find a period, bail if(period == -1 ) { return false; } long lastRestore = prefs.getLong(CommCarePreferences.LAST_SYNC_ATTEMPT, 0); if(isPending(lastRestore, period)) { return true; } return false; } public synchronized boolean isSyncPending(boolean clearFlag) { if(!areAutomatedActionsValid()) { return false;} //We only set this to true occasionally, but in theory it could be set to false //from other factors, so turn it off if it is. if(getPendingSyncStatus() == false) { syncPending = false; } if(!syncPending) { return false; } if(clearFlag) { syncPending = false; } return true; } public boolean isStorageAvailable() { try { File storageRoot = new File(getAndroidFsRoot()); return storageRoot.exists(); } catch(Exception e) { return false; } } /** * Notify the application that something has occurred which has been logged, and which should * cause log submission to occur as soon as possible. */ public void notifyLogsPending() { doReportMaintenance(true); } public String getAndroidFsRoot() { return Environment.getExternalStorageDirectory().toString() + "/Android/data/"+ getPackageName() +"/files/"; } public String getAndroidFsTemp() { return Environment.getExternalStorageDirectory().toString() + "/Android/data/"+ getPackageName() +"/temp/"; } /** the * @return a path to a file location that can be used to store a file temporarily and will be cleaned up as part of * CommCare's application lifecycle */ public String getTempFilePath() { return getAndroidFsTemp() + PropertyUtils.genUUID(); } public ArchiveFileRoot getArchiveFileRoot(){ return mArchiveFileRoot; } }