package org.commcare;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.provider.Settings.Secure;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.util.Log;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.Tracker;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteException;
import org.acra.annotation.ReportsCrashes;
import org.commcare.activities.LoginActivity;
import org.commcare.android.logging.ForceCloseLogEntry;
import org.commcare.android.logging.ForceCloseLogger;
import org.commcare.core.network.ModernHttpRequester;
import org.commcare.dalvik.BuildConfig;
import org.commcare.dalvik.R;
import org.commcare.engine.references.ArchiveFileRoot;
import org.commcare.engine.references.AssetFileRoot;
import org.commcare.engine.references.JavaHttpRoot;
import org.commcare.engine.resource.ResourceInstallUtils;
import org.commcare.android.javarosa.AndroidLogEntry;
import org.commcare.heartbeat.HeartbeatLifecycleManager;
import org.commcare.logging.AndroidLogger;
import org.commcare.logging.PreInitLogger;
import org.commcare.logging.XPathErrorEntry;
import org.commcare.logging.XPathErrorLogger;
import org.commcare.google.services.analytics.GoogleAnalyticsUtils;
import org.commcare.logging.analytics.TimedStatsTracker;
import org.commcare.models.AndroidClassHasher;
import org.commcare.models.AndroidSessionWrapper;
import org.commcare.models.database.AndroidDbHelper;
import org.commcare.models.database.HybridFileBackedSqlHelpers;
import org.commcare.models.database.HybridFileBackedSqlStorage;
import org.commcare.models.database.MigrationException;
import org.commcare.models.database.AndroidPrototypeFactorySetup;
import org.commcare.models.database.SqlStorage;
import org.commcare.models.database.app.DatabaseAppOpenHelper;
import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.models.database.global.DatabaseGlobalOpenHelper;
import org.commcare.android.database.global.models.ApplicationRecord;
import org.commcare.models.database.user.DatabaseUserOpenHelper;
import org.commcare.models.framework.Table;
import org.commcare.models.legacy.LegacyInstallUtils;
import org.commcare.modern.util.Pair;
import org.commcare.network.AndroidModernHttpRequester;
import org.commcare.network.DataPullRequester;
import org.commcare.network.DataPullResponseFactory;
import org.commcare.network.HttpUtils;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.preferences.DevSessionRestorer;
import org.commcare.provider.ProviderUtils;
import org.commcare.services.CommCareSessionService;
import org.commcare.session.CommCareSession;
import org.commcare.tasks.DataSubmissionListener;
import org.commcare.tasks.LogSubmissionTask;
import org.commcare.tasks.PurgeStaleArchivedFormsTask;
import org.commcare.tasks.UpdateTask;
import org.commcare.tasks.templates.ManagedAsyncTask;
import org.commcare.utils.ACRAUtil;
import org.commcare.utils.AndroidCacheDirSetup;
import org.commcare.utils.AndroidCommCarePlatform;
import org.commcare.utils.CommCareExceptionHandler;
import org.commcare.utils.FileUtil;
import org.commcare.utils.GlobalConstants;
import org.commcare.utils.MultipleAppsUtil;
import org.commcare.utils.DummyPropertyManager;
import org.commcare.utils.SessionActivityRegistration;
import org.commcare.utils.SessionStateUninitException;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.utils.PendingCalcs;
import org.javarosa.core.model.User;
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.Persistable;
import org.javarosa.core.util.PropertyUtils;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import java.io.File;
import java.net.URL;
import java.util.HashMap;
import javax.crypto.SecretKey;
@ReportsCrashes(
formUri = "https://your/cloudant/report",
formUriBasicAuthLogin = "your_username",
formUriBasicAuthPassword = "your_password",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT)
public class CommCareApplication extends Application {
private static final String TAG = CommCareApplication.class.getSimpleName();
// Tracking ids for Google Analytics
private static final String LIVE_TRACKING_ID = BuildConfig.ANALYTICS_TRACKING_ID_LIVE;
private static final String DEV_TRACKING_ID = BuildConfig.ANALYTICS_TRACKING_ID_DEV;
private static final int STATE_UNINSTALLED = 0;
private static final int STATE_READY = 2;
public static final int STATE_CORRUPTED = 4;
public static final int STATE_MIGRATION_FAILED = 16;
public static final int STATE_MIGRATION_QUESTIONABLE = 32;
private int dbState;
private static CommCareApplication app;
private CommCareApp currentApp;
// stores current state of application: the session, form
private AndroidSessionWrapper sessionWrapper;
private final Object globalDbHandleLock = new Object();
private SQLiteDatabase globalDatabase;
private ArchiveFileRoot mArchiveFileRoot;
// A bound service is created out of the CommCareSessionService to ensure it stays in memory.
private CommCareSessionService mBoundService;
private ServiceConnection mConnection;
private final Object serviceLock = new Object();
private boolean sessionServiceIsBound = false;
// Important so we don't use the service before the db is initialized.
private boolean sessionServiceIsBinding = false;
// Milliseconds to wait for bind
private static final int MAX_BIND_TIMEOUT = 5000;
private int mCurrentServiceBindTimeout = MAX_BIND_TIMEOUT;
private GoogleAnalytics analyticsInstance;
private Tracker analyticsTracker;
private String messageForUserOnDispatch;
private String titleForUserMessage;
// Indicates that a build refresh action has been triggered, but not yet completed
private boolean latestBuildRefreshPending;
private boolean invalidateCacheOnRestore;
private CommCareNoficationManager noficationManager;
@Override
public void onCreate() {
super.onCreate();
// Sets the static strategy for the deserialization code to be based on an optimized
// md5 hasher. Major speed improvements.
AndroidClassHasher.registerAndroidClassHashStrategy();
CommCareApplication.app = this;
noficationManager = new CommCareNoficationManager(this);
//TODO: Make this robust
PreInitLogger pil = new PreInitLogger();
Logger.registerLogger(pil);
// Workaround because android is written by 7 year-olds (re-uses 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(), this));
PropertyManager.setPropertyManager(new DummyPropertyManager());
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));
} finally {
// No matter what happens, set up our new logger, we want those logs!
setupLoggerStorage(false);
pil.dumpToNewLogger();
}
initializeDefaultLocalizerData();
if (dbState != STATE_MIGRATION_FAILED && dbState != STATE_MIGRATION_QUESTIONABLE) {
AppUtils.checkForIncompletelyUninstalledApps();
initializeAnAppOnStartup();
}
ACRAUtil.initACRA(this);
if (!GoogleAnalyticsUtils.versionIncompatible()) {
analyticsInstance = GoogleAnalytics.getInstance(this);
GoogleAnalyticsUtils.reportAndroidApiLevelAtStartup();
}
}
public void startUserSession(byte[] symmetricKey, UserKeyRecord record, boolean restoreSession) {
synchronized (serviceLock) {
// if we already have a connection established to
// CommCareSessionService, close it and open a new one
SessionActivityRegistration.unregisterSessionExpiration();
if (this.sessionServiceIsBound) {
releaseUserResourcesAndServices();
}
bindUserSessionService(symmetricKey, record, restoreSession);
}
}
/**
* Closes down the user service, resources, and background tasks. Used for
* manual user log-outs.
*/
public void closeUserSession() {
synchronized (serviceLock) {
// Cancel any running tasks before closing down the user database.
ManagedAsyncTask.cancelTasks();
releaseUserResourcesAndServices();
// Switch loggers back over to using global storage, now that we don't have a session
setupLoggerStorage(false);
}
}
/**
* Closes down the user service, resources, and background tasks,
* broadcasting an intent to redirect the user to the login screen. Used
* for session-expiration related user logouts.
*/
public void expireUserSession() {
synchronized (serviceLock) {
closeUserSession();
SessionActivityRegistration.registerSessionExpiration();
sendBroadcast(new Intent(SessionActivityRegistration.USER_SESSION_EXPIRED));
}
}
public void releaseUserResourcesAndServices() {
String userBeingLoggedOut = CommCareApplication.instance().getCurrentUserId();
try {
CommCareApplication.instance().getSession().closeServiceResources();
} catch (SessionUnavailableException e) {
Log.w(TAG, "User's session services have unexpectedly already " +
"been closed down. Proceeding to close the session.");
}
unbindUserSessionService();
TimedStatsTracker.registerEndSession(userBeingLoggedOut);
}
public SecretKey createNewSymmetricKey() {
return getSession().createNewSymmetricKey();
}
synchronized public Tracker getDefaultTracker() {
if (analyticsTracker == null) {
if (BuildConfig.DEBUG) {
analyticsTracker = analyticsInstance.newTracker(DEV_TRACKING_ID);
} else {
analyticsTracker = analyticsInstance.newTracker(LIVE_TRACKING_ID);
}
analyticsTracker.enableAutoActivityTracking(true);
}
String userId = getCurrentUserId();
if (!"".equals(userId)) {
analyticsTracker.set("&uid", userId);
} else {
analyticsTracker.set("&uid", null);
}
return analyticsTracker;
}
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
*/
public CommCareSession getCurrentSession() {
return getCurrentSessionWrapper().getSession();
}
public AndroidSessionWrapper getCurrentSessionWrapper() {
if (sessionWrapper == null) {
throw new SessionStateUninitException("CommCare user session isn't available");
}
return sessionWrapper;
}
public int getDatabaseState() {
return dbState;
}
public void initializeGlobalResources(CommCareApp app) {
if (dbState != STATE_UNINSTALLED) {
initializeAppResources(app);
}
}
public @NonNull String getPhoneId() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) {
return "000000000000000";
}
TelephonyManager manager = (TelephonyManager)this.getSystemService(TELEPHONY_SERVICE);
String imei = manager.getDeviceId();
if (imei == null) {
imei = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
}
if (imei == null) {
imei = "----";
}
return imei;
}
public void initializeDefaultLocalizerData() {
Localization.init(true);
Localization.registerLanguageReference("default",
"jr://asset/locales/android_translatable_strings.txt");
Localization.registerLanguageReference("default",
"jr://asset/locales/android_startup_strings.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.instance().addReferenceFactory(http);
ReferenceManager.instance().addReferenceFactory(afr);
ReferenceManager.instance().addReferenceFactory(arfr);
ReferenceManager.instance().addRootTranslator(new RootTranslator("jr://media/",
GlobalConstants.MEDIA_REF));
}
/**
* Performs the appropriate initialization of an application when this CommCareApplication is
* first launched
*/
private void initializeAnAppOnStartup() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String lastAppId = prefs.getString(LoginActivity.KEY_LAST_APP, "");
if (!"".equals(lastAppId)) {
ApplicationRecord lastApp = MultipleAppsUtil.getAppById(lastAppId);
if (lastApp == null || !lastApp.isUsable()) {
AppUtils.initFirstUsableAppRecord();
} else {
initializeAppResources(new CommCareApp(lastApp));
}
} else {
AppUtils.initFirstUsableAppRecord();
}
}
/**
* Initialize all of the given app's resources, and set the state of its resources accordingly
*
* @param app the CC app to initialize
*/
public void initializeAppResources(CommCareApp app) {
int resourceState;
try {
currentApp = app;
if (currentApp.initializeApplication()) {
resourceState = STATE_READY;
this.sessionWrapper = new AndroidSessionWrapper(this.getCommCarePlatform());
} else {
resourceState = STATE_CORRUPTED;
}
} catch (Exception e) {
Log.i("FAILURE", "Problem with loading");
Log.i("FAILURE", "E: " + e.getMessage());
e.printStackTrace();
ForceCloseLogger.reportExceptionInBg(e);
resourceState = STATE_CORRUPTED;
}
app.setAppResourceState(resourceState);
}
/**
* @return if the given ApplicationRecord is the currently seated one
*/
public boolean isSeated(ApplicationRecord record) {
return currentApp != null && currentApp.getUniqueId().equals(record.getUniqueId());
}
/**
* If the given record is the currently seated app, unseat it
*/
public void unseat(ApplicationRecord record) {
if (isSeated(record)) {
this.currentApp.teardownSandbox();
this.currentApp = null;
}
}
/**
* Completes a full uninstall of the CC app that the given ApplicationRecord represents.
* This method should be idempotent and should be capable of completing an uninstall
* regardless of previous failures
*/
public void uninstall(ApplicationRecord record) {
CommCareApp app = new CommCareApp(record);
// 1) If the app we are uninstalling is the currently-seated app, tear down its sandbox
if (isSeated(record)) {
getCurrentApp().teardownSandbox();
unseat(record);
}
// 2) Set record's status to delete requested, so we know if we have left it in a bad
// state later
record.setStatus(ApplicationRecord.STATUS_DELETE_REQUESTED);
getGlobalStorage(ApplicationRecord.class).write(record);
// 3) Delete the directory containing all of this app's resources
if (!FileUtil.deleteFileOrDir(app.storageRoot())) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "App storage root was unable to be " +
"deleted during app uninstall. Aborting uninstall process for now.");
return;
}
// 4) Delete all the user databases associated with this app
SqlStorage<UserKeyRecord> userDatabase = app.getStorage(UserKeyRecord.class);
for (UserKeyRecord user : userDatabase) {
File f = getDatabasePath(DatabaseUserOpenHelper.getDbName(user.getUuid()));
if (!FileUtil.deleteFileOrDir(f)) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "A user database was unable to be " +
"deleted during app uninstall. Aborting uninstall process for now.");
// If we failed to delete a file, it is likely because there is an open pointer
// to that db still in use, so stop the uninstall for now, and rely on it to
// complete the next time the app starts up
return;
}
}
// 5) Delete the forms database for this app
File formsDb = getDatabasePath(ProviderUtils.getProviderDbName(
ProviderUtils.ProviderType.FORMS,
app.getAppRecord().getApplicationId()));
if (!FileUtil.deleteFileOrDir(formsDb)) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "The app's forms database was unable to be " +
"deleted during app uninstall. Aborting uninstall process for now.");
return;
}
// 6) Delete the instances database for this app
File instancesDb = getDatabasePath(ProviderUtils.getProviderDbName(
ProviderUtils.ProviderType.INSTANCES,
app.getAppRecord().getApplicationId()));
if (!FileUtil.deleteFileOrDir(instancesDb)) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "The app's instances database was unable to" +
" be deleted during app uninstall. Aborting uninstall process for now.");
return;
}
// 7) Delete the app database
File f = getDatabasePath(DatabaseAppOpenHelper.getDbName(app.getAppRecord().getApplicationId()));
if (!FileUtil.deleteFileOrDir(f)) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "The app database was unable to be deleted" +
"during app uninstall. Aborting uninstall process for now.");
return;
}
// 8) Delete the ApplicationRecord
getGlobalStorage(ApplicationRecord.class).remove(record.getID());
}
private int initGlobalDb() {
SQLiteDatabase database;
try {
database = new DatabaseGlobalOpenHelper(this).getWritableDatabase("null");
database.close();
return STATE_READY;
} catch (SQLiteException e) {
// Only thrown if DB isn't there
return STATE_UNINSTALLED;
} catch (MigrationException e) {
if (e.isDefiniteFailure()) {
return STATE_MIGRATION_FAILED;
} else {
return STATE_MIGRATION_QUESTIONABLE;
}
}
}
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<>(table, c, new AndroidDbHelper(this.getApplicationContext()) {
@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) {
return getAppStorage(c.getAnnotation(Table.class).value(), c);
}
public <T extends Persistable> SqlStorage<T> getAppStorage(String name, Class<T> c) {
return currentApp.getStorage(name, c);
}
public <T extends Persistable> HybridFileBackedSqlStorage<T> getFileBackedAppStorage(String name, Class<T> c) {
return currentApp.getFileBackedStorage(name, c);
}
public <T extends Persistable> SqlStorage<T> getUserStorage(Class<T> c) {
return getUserStorage(c.getAnnotation(Table.class).value(), c);
}
public <T extends Persistable> SqlStorage<T> getUserStorage(String storage, Class<T> c) {
return new SqlStorage<>(storage, c, buildUserDbHandle());
}
public <T extends Persistable> HybridFileBackedSqlStorage<T> getFileBackedUserStorage(String storage, Class<T> c) {
return new HybridFileBackedSqlStorage<>(storage, c, buildUserDbHandle(),
getUserKeyRecordId(), CommCareApplication.instance().getCurrentApp());
}
public String getUserKeyRecordId() {
return getSession().getUserKeyRecordUUID();
}
protected AndroidDbHelper buildUserDbHandle() {
return new AndroidDbHelper(this.getApplicationContext()) {
@Override
public SQLiteDatabase getHandle() {
SQLiteDatabase database = getUserDbHandle();
if (database == null) {
throw new SessionUnavailableException("The user database has been closed!");
}
return database;
}
};
}
public <T extends Persistable> SqlStorage<T> getRawStorage(String storage, Class<T> c, final SQLiteDatabase handle) {
return new SqlStorage<>(storage, c, new AndroidDbHelper(this.getApplicationContext()) {
@Override
public SQLiteDatabase getHandle() {
return handle;
}
});
}
public static CommCareApplication instance() {
return app;
}
public String getCurrentUserId() {
try {
return this.getSession().getLoggedInUser().getUniqueId();
} catch (SessionUnavailableException e) {
return "";
}
}
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");
}
String externalRoot = this.getAndroidFsExternalTemp();
FileUtil.deleteFileOrDir(externalRoot);
success = FileUtil.createFolder(externalRoot);
if (!success) {
Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Couldn't create external file folder");
}
}
/**
* Allows something within the current service binding to update the app to let it
* know that the bind may take longer than the current timeout can allow
*/
public void setCustomServiceBindTimeout(int timeout) {
synchronized (serviceLock) {
this.mCurrentServiceBindTimeout = timeout;
}
}
private void bindUserSessionService(final byte[] key, final UserKeyRecord record,
final boolean restoreSession) {
mConnection = new ServiceConnection() {
@Override
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) {
mCurrentServiceBindTimeout = MAX_BIND_TIMEOUT;
mBoundService = ((CommCareSessionService.LocalBinder)service).getService();
// Don't let anyone touch this until it's logged in
// Open user database
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;
}
}
}
// Switch all loggers over to using user storage while there is a session
setupLoggerStorage(true);
sessionServiceIsBound = true;
// Don't signal bind completion until the db is initialized.
sessionServiceIsBinding = false;
if (user != null) {
mBoundService.startSession(user, record);
if (restoreSession) {
CommCareApplication.this.sessionWrapper = DevSessionRestorer.restoreSessionFromPrefs(getCommCarePlatform());
} else {
CommCareApplication.this.sessionWrapper = new AndroidSessionWrapper(CommCareApplication.this.getCommCarePlatform());
}
if (shouldAutoUpdate()) {
startAutoUpdate();
}
syncPending = PendingCalcs.getPendingSyncStatus();
doReportMaintenance(false);
mBoundService.initHeartbeatLifecycle();
// 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();
// clear any files orphaned by file-backed db transaction failures
HybridFileBackedSqlHelpers.removeOrphanedFiles(mBoundService.getUserDbHandle());
PurgeStaleArchivedFormsTask.launchPurgeTask();
}
}
TimedStatsTracker.registerStartSession();
}
}
@Override
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).
startService(new Intent(this, CommCareSessionService.class));
bindService(new Intent(this, CommCareSessionService.class), mConnection, Context.BIND_AUTO_CREATE);
sessionServiceIsBinding = true;
}
@SuppressLint("NewApi")
private void doReportMaintenance(boolean force) {
// 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.instance().getCurrentApp().getAppPreferences();
String url = LogSubmissionTask.getSubmissionUrl(settings);
if (url == null) {
Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, "PostURL isn't set. This should never happen");
return;
}
DataSubmissionListener dataListener = getSession().getListenerForSubmissionNotification(R.string.submission_logs_title);
LogSubmissionTask task = new LogSubmissionTask(
force || PendingCalcs.isPending(settings.getLong(CommCarePreferences.LOG_LAST_DAILY_SUBMIT, 0), DateUtils.DAY_IN_MILLIS),
dataListener,
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();
}
}
/**
* @return True if we aren't a demo user and the time to check for an
* update has elapsed or we logged out while an auto-update was downlaoding
* or queued for retry.
*/
private static boolean shouldAutoUpdate() {
CommCareApp currentApp = CommCareApplication.instance().getCurrentApp();
return (!areAutomatedActionsInvalid() &&
(ResourceInstallUtils.shouldAutoUpdateResume(currentApp) ||
PendingCalcs.isUpdatePending(currentApp.getAppPreferences())));
}
private static void startAutoUpdate() {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Auto-Update Triggered");
String ref = ResourceInstallUtils.getDefaultProfileRef();
try {
UpdateTask updateTask = UpdateTask.getNewInstance();
updateTask.startPinnedNotification(CommCareApplication.instance());
updateTask.setAsAutoUpdate();
updateTask.executeParallel(ref);
} catch (IllegalStateException e) {
Log.w(TAG, "Trying trigger auto-update when it is already running. " +
"Should only happen if the user triggered a manual update before this fired.");
}
}
/**
* Whether automated stuff like auto-updates/syncing are valid and should
* be triggered.
*/
private static boolean areAutomatedActionsInvalid() {
try {
return User.TYPE_DEMO.equals(CommCareApplication.instance().getSession().getLoggedInUser().getUserType());
} catch (SessionUnavailableException sue) {
return true;
}
}
private void unbindUserSessionService() {
synchronized (serviceLock) {
if (sessionServiceIsBound) {
if (sessionWrapper != null) {
sessionWrapper.reset();
}
sessionServiceIsBound = false;
// Detach our existing connection.
unbindService(mConnection);
stopService(new Intent(this, CommCareSessionService.class));
}
}
}
public CommCareSessionService getSession() {
long started = System.currentTimeMillis();
while (sessionServiceIsBinding) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
throw new SessionUnavailableException(
"Trying to access session on UI thread while session is binding");
}
if (System.currentTimeMillis() - started > mCurrentServiceBindTimeout) {
// Something bad happened
Log.e(TAG, "WARNING: Timed out while binding to session service, " +
"this may cause serious problems.");
unbindUserSessionService();
throw new SessionUnavailableException("Timeout binding to session service");
}
}
if (sessionServiceIsBound) {
synchronized (serviceLock) {
return mBoundService;
}
} else {
throw new SessionUnavailableException();
}
}
public UserKeyRecord getRecordForCurrentUser() {
return getSession().getUserKeyRecord();
}
private boolean syncPending = false;
public synchronized boolean isSyncPending(boolean clearFlag) {
if (areAutomatedActionsInvalid()) {
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 (!PendingCalcs.getPendingSyncStatus()) {
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/";
}
public String getAndroidFsExternalTemp() {
return getAndroidFsRoot() + "/temp/external/";
}
/**
* @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();
}
/**
* @param fileStem a relative file path to reference in the temp storage space
* @return A file that can be shared with external applications via URI
*/
public String getExternalTempPath(String fileStem) {
return getAndroidFsExternalTemp() + fileStem;
}
public ArchiveFileRoot getArchiveFileRoot() {
return mArchiveFileRoot;
}
/**
* Used for manually linking to a session service during tests
*/
public void setTestingService(CommCareSessionService service) {
sessionServiceIsBound = true;
mBoundService = service;
mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName className) {
}
};
}
public void storeMessageForUserOnDispatch(String title, String message) {
this.titleForUserMessage = title;
this.messageForUserOnDispatch = message;
}
public String[] getPendingUserMessage() {
if (messageForUserOnDispatch != null) {
return new String[]{messageForUserOnDispatch, titleForUserMessage};
}
return null;
}
public void clearPendingUserMessage() {
messageForUserOnDispatch = null;
titleForUserMessage = null;
}
private static void setupLoggerStorage(boolean userStorageAvailable) {
CommCareApplication app = CommCareApplication.instance();
if (userStorageAvailable) {
Logger.registerLogger(new AndroidLogger(app.getUserStorage(AndroidLogEntry.STORAGE_KEY,
AndroidLogEntry.class)));
ForceCloseLogger.registerStorage(app.getUserStorage(ForceCloseLogEntry.STORAGE_KEY,
ForceCloseLogEntry.class));
XPathErrorLogger.registerStorage(app.getUserStorage(XPathErrorEntry.STORAGE_KEY,
XPathErrorEntry.class));
} else {
Logger.registerLogger(new AndroidLogger(
app.getGlobalStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class)));
ForceCloseLogger.registerStorage(
app.getGlobalStorage(ForceCloseLogEntry.STORAGE_KEY, ForceCloseLogEntry.class));
}
}
public void setPendingRefreshToLatestBuild(boolean b) {
this.latestBuildRefreshPending = b;
}
public boolean checkPendingBuildRefresh() {
if (this.latestBuildRefreshPending) {
this.latestBuildRefreshPending = false;
return true;
}
return false;
}
public ModernHttpRequester buildHttpRequesterForLoggedInUser(Context context, URL url,
HashMap<String, String> params,
boolean isAuthenticatedRequest,
boolean isPostRequest) {
Pair<User, String> userAndDomain =
HttpUtils.getUserAndDomain(isAuthenticatedRequest);
return new AndroidModernHttpRequester(new AndroidCacheDirSetup(context), url, params,
userAndDomain.first, userAndDomain.second, isAuthenticatedRequest, isPostRequest);
}
public DataPullRequester getDataPullRequester() {
return DataPullResponseFactory.INSTANCE;
}
/**
* A consumer app is a CommCare build flavor in which the .ccz and restore file for a specific
* app and user have been pre-packaged along with CommCare into a custom .apk, and placed on
* the Play Store under a custom name/branding scheme.
*/
public boolean isConsumerApp() {
return BuildConfig.IS_CONSUMER_APP;
}
public boolean shouldInvalidateCacheOnRestore() {
return invalidateCacheOnRestore;
}
public void setInvalidateCacheFlag(boolean b) {
invalidateCacheOnRestore = b;
}
public PrototypeFactory getPrototypeFactory(Context c) {
return AndroidPrototypeFactorySetup.getPrototypeFactory(c);
}
public static CommCareNoficationManager notificationManager() {
return app.noficationManager;
}
}