uk.ac.ucl.excites.sapelli.collector.CollectorApp.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ucl.excites.sapelli.collector.CollectorApp.java

Source

/**
 * Sapelli data collection platform: http://sapelli.org
 * 
 * Copyright 2012-2016 University College London - ExCiteS group
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package uk.ac.ucl.excites.sapelli.collector;

import java.io.File;
import java.util.Collections;
import java.util.List;

import com.crashlytics.android.Crashlytics;

import android.app.Application;
import android.content.res.Configuration;
import android.os.Environment;
import android.support.v4.os.EnvironmentCompat;
import android.util.Log;
import uk.ac.ucl.excites.sapelli.collector.db.CollectorPreferences;
import uk.ac.ucl.excites.sapelli.collector.db.CollectorSQLRecordStoreUpgrader;
import uk.ac.ucl.excites.sapelli.collector.db.ProjectRecordStore;
import uk.ac.ucl.excites.sapelli.collector.db.ProjectStore;
import uk.ac.ucl.excites.sapelli.collector.io.AndroidFileStorageProvider;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageRemovedException;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageUnavailableException;
import uk.ac.ucl.excites.sapelli.collector.util.CrashReporter;
import uk.ac.ucl.excites.sapelli.collector.util.ProjectRunHelpers;
import uk.ac.ucl.excites.sapelli.shared.db.StoreHandle;
import uk.ac.ucl.excites.sapelli.shared.db.StoreHandle.StoreSetter;
import uk.ac.ucl.excites.sapelli.shared.db.exceptions.DBException;
import uk.ac.ucl.excites.sapelli.shared.io.FileHelpers;
import uk.ac.ucl.excites.sapelli.shared.io.FileStorageException;
import uk.ac.ucl.excites.sapelli.shared.util.TimeUtils;
import uk.ac.ucl.excites.sapelli.shared.util.android.Debug;
import uk.ac.ucl.excites.sapelli.shared.util.android.DeviceControl;
import uk.ac.ucl.excites.sapelli.storage.db.RecordStore;
import uk.ac.ucl.excites.sapelli.storage.db.sql.SQLRecordStoreUpgrader;
import uk.ac.ucl.excites.sapelli.storage.db.sql.sqlite.android.AndroidSQLiteRecordStore;

/**
 * Application App to keep the db4o object throughout the life-cycle of the Collector
 * 
 * @author Michalis Vitos, mstevens
 * 
 */
public class CollectorApp extends Application {

    // STATICS------------------------------------------------------------
    static protected final String TAG = "CollectorApp";

    static private final String DATABASE_BASENAME = "Sapelli";
    static private final String DEMO_PREFIX = "Demo_";

    static private final String CRASHLYTICS_VERSION_INFO = "VERSION_INFO";
    static private final String CRASHLYTICS_BUILD_INFO = "BUILD_INFO";
    static public final String CRASHLYTICS_DEVICE_ID_CRC32 = "SAPELLI_DEVICE_ID_CRC32";
    static public final String CRASHLYTICS_DEVICE_ID_MD5 = "SAPELLI_DEVICE_ID_MD5";

    /**
     * Used as a System property as well as on Crashlytics.
     */
    static public final String PROPERTY_LAST_PROJECT = "SAPELLI_LAST_RUNNING_PROJECT";

    public static enum StorageStatus {
        UNKNOWN, STORAGE_OK, STORAGE_UNAVAILABLE, STORAGE_REMOVED
    }

    // DYNAMICS-----------------------------------------------------------
    private BuildInfo buildInfo;

    private CollectorPreferences preferences;

    public final AndroidCollectorClient collectorClient = new AndroidCollectorClient();

    // Files storage:
    private FileStorageProvider fileStorageProvider;
    private FileStorageException fileStorageException = null;

    @Override
    public void onCreate() {
        super.onCreate();

        // Build info:
        this.buildInfo = BuildInfo.GetInstance(getApplicationContext());

        Debug.d("CollectorApp started.\nBuild info:\n" + buildInfo.getAllInfo());

        // Start Crashlytics for bugs reporting
        if (!BuildConfig.DEBUG) {
            Crashlytics.start(this);
            Crashlytics.setString(CRASHLYTICS_VERSION_INFO,
                    buildInfo.getNameAndVersion() + " [" + buildInfo.getExtraVersionInfo() + "]");
            Crashlytics.setString(CRASHLYTICS_BUILD_INFO, buildInfo.getBuildInfo());
        }

        // Get collector preferences:
        preferences = new CollectorPreferences(getApplicationContext());

        // Initialise file storage:
        try {
            this.fileStorageProvider = initialiseFileStorage(); // throws FileStorageException
        } catch (FileStorageException fse) {
            this.fileStorageException = fse; // postpone throwing until getFileStorageProvider() is called!
        }

        // Set up a CrashReporter (will use dumps folder):
        if (fileStorageProvider != null)
            Thread.setDefaultUncaughtExceptionHandler(
                    new CrashReporter(fileStorageProvider, getResources().getString(R.string.app_name)));

        // Create shortcut to Sapelli Collector on Home Screen:
        if (preferences.isFirstInstallation()) {
            // Create shortcut
            ProjectRunHelpers.createCollectorShortcut(getApplicationContext());
            // Set first installation to false
            preferences.setFirstInstallation(false);
        }
    }

    /**
     * @return
     * @throws FileStorageException
     */
    private FileStorageProvider initialiseFileStorage() throws FileStorageException {
        File sapelliFolder = null;

        // Try to get Sapelli folder path from preferences:
        try {
            sapelliFolder = new File(preferences.getSapelliFolderPath());
        } catch (NullPointerException npe) {
        }

        // Did we get the folder path from preferences? ...
        if (sapelliFolder == null) { // No: first installation or reset

            // Find appropriate files dir (using application-specific folder, which is removed upon app uninstall!):
            File[] paths = DeviceControl.getExternalFilesDirs(this, null);
            if (paths != null && paths.length != 0) {
                // We count backwards because we prefer secondary external storage (which is likely to be on an SD card rather unremovable memory)
                for (int p = paths.length - 1; p >= 0; p--)
                    if (isMountedReadableWritableDir(paths[p])) {
                        sapelliFolder = paths[p];
                        break;
                    }
            }

            // Do we have a path?
            if (sapelliFolder != null)
                // Yes: store it in the preferences:
                preferences.setSapelliFolder(sapelliFolder.getAbsolutePath());
            else
                // No :-(
                throw new FileStorageUnavailableException();
        } else { // Yes, we got path from preferences, check if it is available ...
            if (!isMountedReadableWritableDir(sapelliFolder)) // (will also attempt to create the directory if it doesn't exist)
                // No :-(
                throw new FileStorageRemovedException(sapelliFolder.getAbsolutePath());
        }

        // If we get here this means we have a non-null sapelliFolder object representing an accessible path...

        // Try to get the Android Downloads folder...
        File downloadsFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        if (!isMountedReadableWritableDir(downloadsFolder)) // check if we can access it (will also attempt to create the directory if it doesn't exist)
            // No :-(
            throw new FileStorageException("Cannot access downloads folder: " + downloadsFolder.getAbsolutePath());

        // Return path provider
        return new AndroidFileStorageProvider(sapelliFolder, downloadsFolder); // Android specific subclass of FileStorageProvider, which generates .nomedia files
    }

    /**
     * Returns a FileStorageProvider when file storage is available or throws an FileStorageUnavailableException or an FileStorageRemovedException if it is not
     * 
     * @return a PathProvider object
     * @throws FileStorageException
     */
    public FileStorageProvider getFileStorageProvider() throws FileStorageException {
        if (fileStorageProvider != null)
            return fileStorageProvider;
        if (fileStorageException != null)
            throw fileStorageException;
        else //if(fileStorageProvider == null && fileStorageException == null
            throw new FileStorageUnavailableException(
                    "FileStorageProvider has not been initialised yet, please call initialiseFileStorage() first."); // this shouldn't happen
    }

    /**
     * @return the preferences
     */
    public CollectorPreferences getPreferences() {
        return preferences;
    }

    /**
     * Check if a directory is on a mounted storage and writable/readable
     * 
     * @param dir
     * @return
     * @throws FileStorageException
     */
    private boolean isMountedReadableWritableDir(File dir) throws FileStorageException {
        try {
            return // Null check:
            (dir != null)
                    // Try to create the directory if it is not there
                    && FileHelpers.createDirectory(dir)
                    /* Check storage state, accepting both MEDIA_MOUNTED and MEDIA_UNKNOWN.
                    *    The MEDIA_UNKNOWN state occurs when a path isn't backed by known storage media; e.g. the SD Card on
                    * the Samsung Xcover 2 (the detection of which we have to force in DeviceControl#getExternalFilesDirs()). */
                    && (Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(dir))
                            || EnvironmentCompat.MEDIA_UNKNOWN.equals(EnvironmentCompat.getStorageState(dir)))
                    // Check whether we have read & write access to the directory:
                    && FileHelpers.isReadableWritableDirectory(dir);
        } catch (Exception e) {
            throw new FileStorageException("Unable to create or determine status of directory: "
                    + (dir != null ? dir.getAbsolutePath() : "null"), e);
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // Debug.d(newConfig.toString());
    }

    public BuildInfo getBuildInfo() {
        return buildInfo;
    }

    /**
     * Returns a prefix to be used on storage identifiers (DB4O filenames, SharedPref's names, etc.) when in demo mode
     * (if not in demo mode the prefix is empty).
     * The goal is to separate demo-mode storage from non-demo-mode installations and previous demo installations.
     * 
     * @return
     */
    public String getDemoPrefix() {
        return (buildInfo.isDemoBuild()
                ? DEMO_PREFIX
                        + FileHelpers.makeValidFileName(TimeUtils.getTimestampForFileName(buildInfo.getTimeStamp()))
                : "");
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        Debug.d("onLowMemory() called!");
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        // This method is for use in emulated process environments. It will never be called on
        // a production Android device, where processes are removed by simply killing them; no
        // user code (including this callback) is executed when doing so.
        Debug.d("Should never be called!");
    }

    /**
     * @return handles for all Stores that need to be backed-up
     */
    public StoreHandle<?>[] getStoreHandlesForBackup() {
        return new StoreHandle[] { collectorClient.recordStoreHandle, collectorClient.transmissionStoreHandle,
                collectorClient.projectStoreHandle };
    }

    /**
     * @author mstevens
     *
     */
    public final class AndroidCollectorClient extends CollectorClient
            implements SQLRecordStoreUpgrader.UpgradeCallback {

        private int oldDatabaseVersion = CURRENT_COLLECTOR_RECORDSTORE_VERSION;
        private List<String> upgradeWarnings = Collections.<String>emptyList();

        @Override
        public FileStorageProvider getFileStorageProvider() {
            return fileStorageProvider;
        }

        @Override
        protected void createAndSetRecordStore(StoreSetter<RecordStore> setter) throws DBException {
            @SuppressWarnings("resource")
            RecordStore recordStore = new AndroidSQLiteRecordStore(this, CollectorApp.this,
                    getFileStorageProvider().getDBFolder(true),
                    getDemoPrefix() /*will be "" if not in demo mode*/ + DATABASE_BASENAME,
                    CURRENT_COLLECTOR_RECORDSTORE_VERSION,
                    new CollectorSQLRecordStoreUpgrader(this, this, getFileStorageProvider()));
            //RecordStore recordStore = new DB4ORecordStore(this, getFileStorageProvider().getDBFolder(true), getDemoPrefix() /*will be "" if not in demo mode*/ + DATABASE_BASENAME);
            setter.setAndInitialise(recordStore);

            // Enable logging if in debug mode (will display SQL statements being executed):
            if (BuildConfig.DEBUG)
                recordStore.setLoggingEnabled(true);
        }

        @Override
        protected void createAndSetProjectStore(StoreSetter<ProjectStore> setter) throws DBException {
            setter.setAndInitialise(new ProjectRecordStore(this, getFileStorageProvider()));
            //setter.setAndInitialise(new PrefProjectStore(CollectorApp.this, getFileStorageProvider(), getDemoPrefix()));
            //setter.setAndInitialise(new DB4OProjectStore(getFileStorageProvider().getDBFolder(true), getDemoPrefix() /*will be "" if not in demo mode*/ + "ProjectStore"));
        }

        @Override
        public void upgradePerformed(int fromVersion, int toVersion, List<String> warnings) {
            oldDatabaseVersion = fromVersion;
            upgradeWarnings = warnings;
        }

        public boolean hasDatabaseBeenUpgraded() {
            return oldDatabaseVersion != CURRENT_COLLECTOR_RECORDSTORE_VERSION;
        }

        /**
         * @return the oldDatabaseVersion
         */
        public final int getOldDatabaseVersion() {
            return oldDatabaseVersion;
        }

        /**
         * @return the upgradeWarnings
         */
        public final List<String> getUpgradeWarnings() {
            return upgradeWarnings;
        }

        public final void forgetAboutUpgrade() {
            oldDatabaseVersion = CURRENT_COLLECTOR_RECORDSTORE_VERSION;
            upgradeWarnings = Collections.<String>emptyList();
        }

        @Override
        public void logError(String msg, Throwable throwable) {
            if (throwable != null)
                Log.e(getClass().getSimpleName(), msg, throwable);
            else
                Log.e(getClass().getSimpleName(), msg);
        }

        @Override
        public void logWarning(String msg) {
            Log.w(getClass().getSimpleName(), msg);
        }

        @Override
        public void logInfo(String msg) {
            Log.i(getClass().getSimpleName(), msg);
        }

    }

}