opensource.zeocompanion.ZeoCompanionApplication.java Source code

Java tutorial

Introduction

Here is the source code for opensource.zeocompanion.ZeoCompanionApplication.java

Source

package opensource.zeocompanion;

/*
All source code of this ZeoCompanion Android Application, except where otherwise encumbered as stated below,
is dedicated to the Public Domain.  All source code of this ZeoCompanion Android Application,
except where otherwise encumbered as stated below, constitutes prior art as of March 2016.
    
Source code that has known encumberances and therefore CANNOT be dedicated into the Public Domain include:
- GraphView:
Located within this App: com.jjoe64.graphview
License: GPLv2 including "GPL linking exception"
Website: http://www.android-graphview.org/get-support--license.html
Modifications: changes to provide limited enhanced features of specific use to this app
- ZeoDataContract:
Located within the App: com.myzeo.android.api.data
License: Apache License, Version 2.0
Website: https://github.com/zeoeng/zeo-android-api
Modifications: none
- MyZeoExportDataContract:
Located within the App: com.myzeo.android.api.data
License: unknown
Website: offline; copy located at: https://www.gwern.net/docs/zeo/2013-zeo-exportdatasheet.pdf
Modifications: none
- ObscuredPrefs:
Located within the App: com.obscuredPreferences
License: multiple sources; unknown which has first provenance; also unknown if the specific encrypt() and decrypt() methods have further earlier provenance
Probable 1st provenance Website: https://stackoverflow.com/questions/785973/what-is-the-most-appropriate-way-to-store-user-settings-in-android-application/6393502#6393502
Probable later provenance Website: http://www.codeproject.com/Articles/785925/Obscured-Shared-Preferences-for-Android
Modifications: none; however only two methods [encrypt() and decrypt()] are utilized
- Modified widgets: EditTextPreference and Spinner
Located within the App: com.android
License: Apache License, Version 2.0
Website: https://source.android.com/source/licenses.html
Modifications: changes to provide limited enhanced features of specific use to this app
- Modified widgets: TimePreference
Located within the App: com.github
License: no stated license
Website: https://gist.github.com/SamWhited/a2c3c382dcaa3ae17bb4
Modifications: changes to summary string and default style handling
    
Other source code within this application could have at present unknown encumberances, and if so
revealed at some future point are therefore not within the scope of this Public Domain dedication.
 */

import android.Manifest;
import android.app.AlarmManager;
import android.app.Application;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Point;
import android.media.MediaScannerConnection;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
import com.obscuredPreferences.ObscuredPrefs;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.FileWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import opensource.zeocompanion.database.CompanionAlertRec;
import opensource.zeocompanion.database.CompanionDatabase;
import opensource.zeocompanion.database.CompanionSystemRec;
import opensource.zeocompanion.utility.DirectEmailerOutbox;
import opensource.zeocompanion.utility.DirectEmailerThread;
import opensource.zeocompanion.utility.JournalDataCoordinator;
import opensource.zeocompanion.zeo.ZeoAppHandler;

// main application "global" class
public class ZeoCompanionApplication extends Application {
    // member variables
    public static boolean mFirstTIme = false;
    public static int mFirstTimeHintsShown = 0;
    public static int mMaxBitmapDim = 0;
    public static float mScreenDensity = 0;
    public static boolean mAlreadyWarnedAboutHeadband = false;

    // member constants and other static content
    private static final String _CTAG = "APP";
    private static final SimpleDateFormat mFileDateFormatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
    public static ZeoAppHandler mZeoAppHandler = null;
    public static CompanionDatabase mDatabaseHandler = null;
    public static JournalDataCoordinator mCoordinator = null;
    public static DirectEmailerOutbox mEmailOutbox = null;
    private static Context mOurContext = null;
    public static File mBaseExtStorageDir = null;
    public static ZeoCompanionApplication mApp = null;

    // these two are a "cheat" to allow passing of an IntegratedHistoryRec
    // from the MainActivity to either the HistoryDetailActivity or the SharingActivity
    public static JournalDataCoordinator.IntegratedHistoryRec mIrec_HDAonly = null;
    public static JournalDataCoordinator.IntegratedHistoryRec mIrec_SAonly = null;

    // constants for showing one-time hints
    public static final int APP_HINTS_ATTRIBUTES_FRAGMENT_BEFORE = 0x0001;
    public static final int APP_HINTS_ATTRIBUTES_FRAGMENT_AFTER = 0x0002;
    public static final int APP_HINTS_INBED_FRAGMENT = 0x0004;
    public static final int APP_HINTS_GOING_FRAGMENT = 0x0008;
    public static final int APP_HINTS_DURING_FRAGMENT = 0x0010;
    public static final int APP_HINTS_SUMMARY_FRAGMENT = 0x0020;
    public static final int APP_HINTS_HISTORY_FRAGMENT = 0x0040;
    public static final int APP_HINTS_BACKUP = 0x0080;
    public static final int APP_HINTS_SMALLSCREEN = 0x0100;

    // inter-process messaging constants used by various Activities and Handlers
    public static final int MESSAGE_HEADBAND_HBFRAG_LOW = 9000;
    public static final int MESSAGE_HEADBAND_RECV_HB_MSG = MESSAGE_HEADBAND_HBFRAG_LOW;
    public static final int MESSAGE_HEADBAND_BLUETOOTH_HNDLR_ERR = 9001;
    public static final int MESSAGE_HEADBAND_HB_CONNECT_OK = 9002;
    public static final int MESSAGE_HEADBAND_HBFRAG_HIGH = MESSAGE_HEADBAND_HB_CONNECT_OK;
    public static final int MESSAGE_APP_SEND_TOAST = 9100;
    public static final int MESSAGE_ZAH_ZEO_STATE_CHANGED = 9110;
    public static final int MESSAGE_ZAH_ZEO_PROBED_NO_CHANGE = 9111;
    public static final int MESSAGE_MAIN_UPDATE_ALL = 9120;
    public static final int MESSAGE_MAIN_UPDATE_JSB = 9121;
    public static final int MESSAGE_MAIN_ZAH_STATE_CHANGE = 9122;
    public static final int MESSAGE_MAIN_ZAH_PROBE_NO_CHANGE = 9123;
    public static final int MESSAGE_MAIN_UPDATE_HISTORY = 9124;
    public static final int MESSAGE_MAIN_UPDATE_MENU = 9125;
    public static final int MESSAGE_SHARING_DIALOG_TERMINATED = 9130;
    public static final int MESSAGE_SETTINGS_EMAILTEST_RESULTS = 9140;
    public static final int MESSAGE_OUTBOX_EMAILRESEND_RESULTS = 9150;

    // application-custom broadcast message actions
    public static final String ACTION_ALARMMGR_WAKEUP_RTC = "opensource.zeocompanion.intent.action.RTC_WAKEUP"; // this must match what is defined in the Manifest

    // receive inter-process messages to ensure that no inter-nested UI updates cause issues
    // generally this is called by the Direct Email subsystem to display Toasts
    public static Handler mAppHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case ZeoCompanionApplication.MESSAGE_APP_SEND_TOAST:
                Toast.makeText(getContext(), (String) msg.obj, Toast.LENGTH_LONG).show();
                break;
            }
        }
    };

    // setup a Listener for changes in the shared preferences
    SharedPreferences.OnSharedPreferenceChangeListener mPrefsChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if (key.equals("email_auto_enable") || key.equals("email_auto_send_time")
                    || key.equals("database_replicate_zeo")) {
                // preferences that affect the use of the AlarmManager have changed
                Log.d(_CTAG + ".prefChgListen", "Configure Alarm Manager needed");
                configAlarmManagerToPrefs();
            } else if (key.equals("profile_name")) {
                // profile name has changed; need to change it in the DB too
                CompanionSystemRec sRec = mDatabaseHandler.getSystemRec();
                if (sRec != null) {
                    String newName = sharedPreferences.getString(key, "");
                    if (newName.isEmpty()) {
                        sRec.rUserName = null;
                    } else {
                        sRec.rUserName = newName;
                    }
                    sRec.saveToDB(mDatabaseHandler);
                }
            }
        }
    };

    // receiver for timeouts of recurring daily Alarm for automatic emailing;
    // these run in the main thread
    public static class AlarmReceiver extends BroadcastReceiver {
        // constructor
        public AlarmReceiver() {
            super();
        }

        // receive a filtered message (the filtered actions are in the Manifest)
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action == null) {
                return;
            }
            Log.d("APP.AR.onReceive", "Action=" + action);
            if (action.equals(ACTION_ALARMMGR_WAKEUP_RTC)) {
                // Alarm Manager has given the daily wakeup
                if (mEmailOutbox != null) {
                    mEmailOutbox.dailyCheck();
                }
                if (mZeoAppHandler != null) {
                    mZeoAppHandler.dailyCheck();
                }
                ZeoCompanionApplication.mApp.dailyCheck();
            }
        }
    }

    // receive system-wide broadcasts about changes in Zeo Headband state
    private final BroadcastReceiver mZeoAppReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action == null) {
                return;
            }
            Log.d("APP.ZAR.onReceive", "Action=" + action);
            mZeoAppHandler.zeoAppBroadcastReceived(intent);
        }
    };

    // Thread Context: can be called from utility threads, so cannot perform UI actions like Toast
    // setup a master application-wide abort handler usable by all threads
    public static Thread.UncaughtExceptionHandler mMasterAbortHandler = new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            Log.e(_CTAG + ".mstrAbortHdlr", "=====!!!!!=====Unhandled Abort Captured=====!!!!!=====");
            postToErrorLog(null, e, "*UNHANDLED*", t.getName()); // automatically posts a Log.e
            System.exit(0); // force the entire App to terminate else it goes into "ANR" limbo
        }
    };

    // called upon App invocation; however remember that Apps are usually just suspended in the background;
    // so this callback cannot be expected to be invoked every time the end-user brings up the App;
    // also note that Android does NOT provide any application onTerminate or onDestroy callbacks so
    // there is no concept of a "clean termination" that saves state at the Application object level
    @Override
    public void onCreate() {
        super.onCreate();
        //Log.d(_CTAG + ".onCreate", "=====ON-CREATE=====");
        // activate the global unhandled exception handler in this main thread
        Thread.setDefaultUncaughtExceptionHandler(mMasterAbortHandler);

        // initializations
        mApp = this;
        mOurContext = this;
        ObscuredPrefs.init(this);

        // pre-create empty external storage folders
        mBaseExtStorageDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + "Android" + File.separator + "data" + File.separator + getPackageName());
        createExternalStorageFolders(); // note if there are external storage problems, the Application Object does not have a UI Activity in which to report it; MainActivity will re-detect this

        // startup all the global application handlers; the order of these startups is important
        mDatabaseHandler = new CompanionDatabase(this); // database handler must be first and must be initialized
        String msg = mDatabaseHandler.initialize();
        //if (!msg.isEmpty()) { Utilities.showAlertDialog(this, "Error", msg, "Okay"); }      TODO need end-user error reporting but only Toast is available at this stage
        mZeoAppHandler = new ZeoAppHandler(this);
        mCoordinator = new JournalDataCoordinator(this); // ZeoAppHandler must be instantiated first
        mEmailOutbox = new DirectEmailerOutbox(this); // JournalDataCoordinator must be instantiated first

        // detect whether the App is being run the first time after an install (or a data clear from the App Manager)
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        mFirstTIme = prefs.getBoolean("app_firstTime", true);
        mFirstTimeHintsShown = prefs.getInt("app_firstTime_HintsShown", 0);
        if (mFirstTIme) {
            // these obscured preference default values need to be pre-encrypted
            SharedPreferences.Editor editor = prefs.edit();
            editor.putString("profile_goal_hours_per_night", ObscuredPrefs.encryptString("8"));
            editor.putString("profile_goal_percent_deep", ObscuredPrefs.encryptString("15"));
            editor.putString("profile_goal_percent_REM", ObscuredPrefs.encryptString("20"));
            editor.putBoolean("app_firstTime", false);
            editor.commit();
        }

        // setup to receive preference changes that affect the AlarmManager; then configure the AlarmManager
        PreferenceManager.getDefaultSharedPreferences(this)
                .registerOnSharedPreferenceChangeListener(mPrefsChangeListener);
        configAlarmManagerToPrefs();

        // listen to broadcast messages from the Zeo Android App
        IntentFilter filter = new IntentFilter();
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_UNDOCKED");
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_DOCKED");
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_BATTERY_DEAD");
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_BUTTON_PRESS");
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_DISCONNECTED");
        filter.addAction("com.myzeo.android.headband.action.HEADBAND_CONNECTED");
        registerReceiver(mZeoAppReceiver, filter);
    }

    // return the App's context
    public static Context getContext() {
        return mOurContext;
    }

    // close the database; this is only performed for a reload database
    private static boolean closeDatabase() {
        boolean done = mDatabaseHandler.closeDatabase();
        if (done) {
            mDatabaseHandler = null;
        }
        return done;
    }

    // reopen the database; this is only performed for a reload database
    private static String reopenDatabase() {
        mDatabaseHandler = new CompanionDatabase(mOurContext);
        return mDatabaseHandler.initialize();
    }

    // used by various Fragments to indicate that their one-time hint has been shown
    public static void hintShown(int shown) {
        mFirstTimeHintsShown = mFirstTimeHintsShown | shown;
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mOurContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putInt("app_firstTime_HintsShown", mFirstTimeHintsShown);
        editor.commit();
    }

    ///////////////////////////////////////////////////////////////////
    // Methods related to the daily alarm manager
    ///////////////////////////////////////////////////////////////////

    // properly configure the Android AlarmManager depending on the preferences of the end-user;
    // used by both the DirectEmailerOutbox and the ZeoAppHandler
    private void configAlarmManagerToPrefs() {
        // setup a daily alarm if auto-emailing is enabled
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        boolean enabledAutoEmail = prefs.getBoolean("email_auto_enable", false);
        boolean enabledDatabaseReplicate = prefs.getBoolean("database_replicate_zeo", false);
        long desiredTOD = prefs.getLong("email_auto_send_time", 0); // will default to midnight
        long configuredTOD = prefs.getLong("email_auto_send_time_configured", 0); // will default to midnight

        // determine whether there is an active AlarmManager entry that we have established
        AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        Intent intentCheck = new Intent(this, ZeoCompanionApplication.AlarmReceiver.class);
        intentCheck.setAction(ZeoCompanionApplication.ACTION_ALARMMGR_WAKEUP_RTC);
        PendingIntent existingPi = PendingIntent.getBroadcast(this, 0, intentCheck, PendingIntent.FLAG_NO_CREATE);

        if (enabledAutoEmail || enabledDatabaseReplicate) {
            // Daily AlarmManager is needed
            if (existingPi != null && desiredTOD != configuredTOD) {
                // there is an existing AlarmManager entry, but it has the incorrect starting time-of-day;
                // so cancel it, and rebuild a new one
                Intent intent1 = new Intent(this, ZeoCompanionApplication.AlarmReceiver.class);
                intent1.setAction(ZeoCompanionApplication.ACTION_ALARMMGR_WAKEUP_RTC);
                PendingIntent pi1 = PendingIntent.getBroadcast(this, 0, intent1, PendingIntent.FLAG_CANCEL_CURRENT);
                am.cancel(pi1);
                pi1.cancel();
                existingPi = null;
            }
            if (existingPi == null) {
                // there is no existing AlarmManager entry, so create it
                Date dt = new Date(desiredTOD);
                Calendar calendar = Calendar.getInstance();
                calendar.set(Calendar.HOUR_OF_DAY, dt.getHours());
                calendar.set(Calendar.MINUTE, dt.getMinutes());
                calendar.set(Calendar.SECOND, dt.getSeconds());
                Intent intent2 = new Intent(this, ZeoCompanionApplication.AlarmReceiver.class);
                intent2.setAction(ZeoCompanionApplication.ACTION_ALARMMGR_WAKEUP_RTC);
                PendingIntent pi2 = PendingIntent.getBroadcast(this, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT);
                am.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY,
                        pi2);
                SharedPreferences.Editor editor = prefs.edit();
                editor.putLong("email_auto_send_time_configured", desiredTOD);
                editor.commit();
            }

        } else {
            // Daily AlarmManager is not needed
            if (existingPi != null) {
                // there is an AlarmManager entry pending; need to cancel it
                Intent intent3 = new Intent(this, ZeoCompanionApplication.AlarmReceiver.class);
                intent3.setAction(ZeoCompanionApplication.ACTION_ALARMMGR_WAKEUP_RTC);
                PendingIntent pi3 = PendingIntent.getBroadcast(this, 0, intent3, PendingIntent.FLAG_CANCEL_CURRENT);
                am.cancel(pi3);
                pi3.cancel();
            }
        }
    }

    // called daily by the AlarmManager to check for automatic email database backups
    public void dailyCheck() {
        // configured to allow an auto-backup?
        SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        boolean autoEmailEnabled = sPrefs.getBoolean("email_auto_enable", false);
        boolean sendDatabaseEnabled = sPrefs.getBoolean("email_auto_send_database", false);
        if (!autoEmailEnabled || !sendDatabaseEnabled) {
            return;
        }

        // yes, has the proper internal passed?
        long newTimestamp = System.currentTimeMillis();
        long autoBackupLastTimestamp = sPrefs.getLong("email_auto_send_database_dest_timestamp_last_sent", 0);
        String autoBackupRepetitionInterval = sPrefs.getString("email_auto_send_database_repetition", "Weekly");
        if (autoBackupLastTimestamp > 0) {
            long interval = 86400000L;
            if (autoBackupRepetitionInterval.equals("Weekly")) {
                interval = interval * 7L;
            }
            if (autoBackupLastTimestamp + interval > newTimestamp) {
                return;
            }
        }
        Log.d(_CTAG + ".dailyCheck", "Auto-backup of ZeoCompanion database invoked");

        // yes, backup the database
        BackupReturnResults results = saveCopyOfDB("auto_"); // will not return null

        // is there a destination email address?
        String dest = "";
        if (sPrefs.contains("email_auto_send_database_dest")) {
            dest = ObscuredPrefs.decryptString(sPrefs.getString("email_auto_send_database_dest", ""));
        }
        if (dest == null) {
            if (!results.rAnErrorMessage.isEmpty()) {
                results.rAnErrorMessage = results.rAnErrorMessage + ". ";
            }
            results.rAnErrorMessage = results.rAnErrorMessage
                    + "A destination email address is not configured for the database backup; the backup is only stored on-device.";
        } else if (dest.isEmpty()) {
            if (!results.rAnErrorMessage.isEmpty()) {
                results.rAnErrorMessage = results.rAnErrorMessage + ". ";
            }
            results.rAnErrorMessage = results.rAnErrorMessage
                    + "A destination email address is not configured for the database backup; the backup is only stored on-device.";
        }

        // send a database backup via direct email in a separate thread
        String subject = "ZeoCompanion database auto backup";
        String body = subject + "; see attachment.";
        if (results.rTheBackupFile == null || !results.rAnErrorMessage.isEmpty()) {
            mEmailOutbox.postToOutbox(dest, subject, body, results.rTheBackupFile, results.rAnErrorMessage, null);
        } else {
            DirectEmailerThread de = new DirectEmailerThread(this);
            de.setName("DirectEmailerThread via " + _CTAG + ".dailyCheck");
            de.configure(subject, body, results.rTheBackupFile, true);
            de.configureToAddress(dest);
            de.start();
        }

        // remember the current timestamp of this auto-backup
        SharedPreferences.Editor editor = sPrefs.edit();
        editor.putLong("email_auto_send_database_dest_timestamp_last_sent", newTimestamp);
        editor.commit();
    }

    ///////////////////////////////////////////////////////////////////
    // Methods related to managing the external storage in-general
    ///////////////////////////////////////////////////////////////////

    // creates the App's external storage folders
    private void createExternalStorageFolders() {
        // is external storage available, read/write, and App has been granted permission
        int r = checkExternalStorage();
        if (r != 0) {
            return;
        }

        // external storage is available and read/write
        mBaseExtStorageDir.mkdirs();
        File myExtFilesInternalsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        myExtFilesInternalsDir.mkdirs();
        File myExtFilesOutboxDir = new File(mBaseExtStorageDir + File.separator + "outbox");
        myExtFilesOutboxDir.mkdirs();
        File myExtFilesExportsDir = new File(mBaseExtStorageDir + File.separator + "exports");
        myExtFilesExportsDir.mkdirs();
    }

    // force Android to let an attached PC know the file has been created
    public static void forceShowOnPC(File theFile) {
        MediaScannerConnection.scanFile(mOurContext, new String[] { theFile.getPath() }, null, null);
    }

    // determine if the external storage is accesible
    public static int checkExternalStorage() {
        // does App still have permission to write to external storage?
        int permissionCheck = ContextCompat.checkSelfPermission(mOurContext,
                Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if (permissionCheck == PackageManager.PERMISSION_DENIED) {
            return -2;
        } // nope

        // is external storage is available and read-write?
        String state = Environment.getExternalStorageState();
        if (!state.equals(Environment.MEDIA_MOUNTED)) {
            return -1;
        } // not mounted or is MEDIA_MOUNTED_READ_ONLY or is in some other non-usable condition
        return 0;
    }

    ///////////////////////////////////////////////////////////////////
    // Methods related to backup and restore of the ZeoCompanion database
    ///////////////////////////////////////////////////////////////////

    // return class
    public class BackupReturnResults {
        public File rTheBackupFile = null;
        public String rAnErrorMessage = "";

        public BackupReturnResults(File theBackupFile, String anErrorMessage) {
            rTheBackupFile = theBackupFile;
            rAnErrorMessage = anErrorMessage;
        }
    }

    // copies the ZeoCompanion database to external storage;
    // do not return null
    public BackupReturnResults saveCopyOfDB(String includePrefix) {
        // is external storage available, read/write, and App has been granted permission
        int r = checkExternalStorage();
        if (r == -2) {
            return new BackupReturnResults(null,
                    "Permission for App to write to external storage has not been granted; please grant the permission");
        } else if (r != 0) {
            return new BackupReturnResults(null, "No writable external storage is available");
        }

        // get the name and folder preference
        SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        String name = sPrefs.getString("profile_name", "");
        String folder = sPrefs.getString("backup_directory", "Android/data/opensource.zeocompanion/internals");

        // ensure the destination directory structure is present
        File backupsDir = null;
        if (folder != null) {
            backupsDir = new File(
                    Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + folder);
        } else {
            backupsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        }
        backupsDir.mkdirs();

        // compose source and destination File instances
        File source = getDatabasePath(CompanionDatabase.DATABASE_NAME);
        String newName = FilenameUtils.removeExtension(CompanionDatabase.DATABASE_NAME);
        if (name != null) {
            if (!name.isEmpty()) {
                newName = newName + "_" + name;
            }
        }
        newName = newName + "_DBVer" + mDatabaseHandler.mVersion + "_AppVer" + BuildConfig.VERSION_NAME + "_"
                + mFileDateFormatter.format(new Date()) + ".db";
        File dest = new File(backupsDir.getPath() + File.separator + includePrefix + newName);
        Log.d(_CTAG + ".saveCopyOfDB", "Dest=" + dest.getAbsolutePath());

        // perform the copy
        try {
            FileUtils.copyFile(source, dest);
            ZeoCompanionApplication.forceShowOnPC(dest);
        } catch (Exception e) {
            return new BackupReturnResults(null,
                    "Failed to backup database because of filesystem error: " + e.getMessage());
        }
        return new BackupReturnResults(dest, "");
    }

    // determines whether a restorage of the ZeoCompanion database from external storage is possible
    public CompanionDatabase.ValidateDatabaseResults restoreCopyOfDB_prep(String theSourceFile) {
        // is external storage available, read/write, and App has been granted permission
        int r = checkExternalStorage();
        if (r == -2) {
            return new CompanionDatabase.ValidateDatabaseResults(
                    "Permission for App to write to external storage has not been granted; please grant the permission");
        } else if (r != 0) {
            return new CompanionDatabase.ValidateDatabaseResults("No writable external storage is available");
        }

        // get the folder preference
        SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        String folder = sPrefs.getString("backup_directory", "Android/data/opensource.zeocompanion/internals");

        // compose source File instances and ensure it actually exists on-disk
        File backupsDir = null;
        if (folder != null) {
            backupsDir = new File(
                    Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + folder);
        } else {
            backupsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        }
        backupsDir.mkdirs();

        File source = new File(backupsDir + File.separator + theSourceFile);
        if (!source.exists()) {
            return new CompanionDatabase.ValidateDatabaseResults(
                    "File does not exist or the folder path does not exist: " + source.getAbsolutePath());
        }

        // perform validation of the database itself
        return ZeoCompanionApplication.mDatabaseHandler.validateDatabaseFromFile(source);
    }

    // restores the ZeoCompanion database from external storage
    public String restoreCopyOfDB(String theSourceFile) {
        // is external storage available, read/write, and App has been granted permission
        int r = checkExternalStorage();
        if (r == -2) {
            return "Permission for App to write to external storage has not been granted; please grant the permission";
        } else if (r != 0) {
            return "No writable external storage is available";
        }

        // get the folder preference
        SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        String folder = sPrefs.getString("backup_directory", "Android/data/opensource.zeocompanion/internals");

        // compose source File instances and ensure it actually exists on-disk
        File backupsDir = null;
        if (folder != null) {
            backupsDir = new File(
                    Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + folder);
        } else {
            backupsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        }
        backupsDir.mkdirs();
        File source = new File(backupsDir + File.separator + theSourceFile);
        File dest = getDatabasePath(CompanionDatabase.DATABASE_NAME);

        // perform the copy
        boolean okay = closeDatabase();
        if (!okay) {
            return "Could not close the current active database; see error.log";
        }
        try {
            FileUtils.copyFile(source, dest);
        } catch (Exception e) {
            String msg = reopenDatabase(); // need to reopen the database first to regain access to SystemAlerts; if there is a problem postToErrorLog will have already been called for the database issues
            String eMsg = "Failed to restore database because of filesystem error: " + e.getMessage();
            postToErrorLog(_CTAG + ".restoreCopyOfDB", e, "Failed to restore database from "
                    + source.getAbsoluteFile() + " to " + dest.getAbsoluteFile()); // automatically posts a Log.e
            if (!msg.isEmpty()) {
                return msg + "AND " + eMsg;
            }
            return eMsg;
        }
        Log.i(_CTAG + ".restoreCopyOfDB",
                "Restored database from " + source.getAbsoluteFile() + " to " + dest.getAbsoluteFile());
        String msg = reopenDatabase();
        if (!msg.isEmpty()) {
            return msg;
        }
        return "";
    }

    ///////////////////////////////////////////////////////////////////
    // Methods related to the error.log and Alerts
    ///////////////////////////////////////////////////////////////////

    // Thread Context: called from MainActivity in main thread
    // return the FILE for the path to the error.log file
    public static File errorLogFile() {
        File internalsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        internalsDir.mkdirs();
        return new File(internalsDir + File.separator + "error.log");
    }

    // Thread Context: called from MainActivity in main thread
    // indicate whether the error.log file exists or not
    public static boolean hasErrorLog() {
        File internalsDir = new File(mBaseExtStorageDir + File.separator + "internals");
        internalsDir.mkdirs();
        File errLogFile = new File(internalsDir + File.separator + "error.log");
        return errLogFile.exists();
    }

    // Thread Context: can be called from utility threads, so cannot perform UI actions like Toast
    // write detailed exception information into an error log that the end-user can email to the developer
    public static void postToErrorLog(String method, Throwable theException) {
        postToErrorLog(method, theException, null, null, false);
    }

    public static void postToErrorLog(String method, Throwable theException, String extra) {
        postToErrorLog(method, theException, extra, null, false);
    }

    public static void postToErrorLog(String method, Throwable theException, String extra, String threadName) {
        postToErrorLog(method, theException, extra, threadName, false);
    }

    public static void postToErrorLog(String method, Throwable theException, String extra, String threadName,
            boolean noAlert) {
        String eMsg = "Exception";
        if (method != null) {
            if (!method.isEmpty()) {
                eMsg = eMsg + " in " + method;
            }
        }
        if (threadName != null) {
            if (!threadName.isEmpty()) {
                eMsg = eMsg + " in Thread " + threadName;
            }
        }
        if (extra != null) {
            if (!extra.isEmpty()) {
                eMsg = eMsg + " (" + extra + ")";
            }
        }
        eMsg = eMsg + ": " + theException.toString();
        Log.e(_CTAG + ".postToErrorLog", eMsg);
        theException.printStackTrace();

        int r = checkExternalStorage();
        if (r != 0) {
            Log.e(_CTAG + ".postToErrorLog", "Cannot write to external storage code " + r);
            return;
        }

        FileWriter wrt = null;
        try {
            // pull the stack trace
            StackTraceElement[] traces = theException.getStackTrace();

            // ensure the directory structure is present and compose the file name
            File internalsDir = new File(mBaseExtStorageDir + File.separator + "internals");
            internalsDir.mkdirs();
            File errLogFile = new File(internalsDir + File.separator + "error.log");

            // create and append to the file
            wrt = new FileWriter(errLogFile, true); // append if file already exists
            wrt.write(new Date().toString() + "\n");
            if (threadName != null) {
                if (!threadName.isEmpty()) {
                    wrt.write(" in Thread " + threadName + " ");
                }
            }
            wrt.write("AppVerName " + BuildConfig.VERSION_NAME + " AppVerCode " + BuildConfig.VERSION_CODE);
            if (mDatabaseHandler != null) {
                wrt.write(" with DBver " + mDatabaseHandler.mVersion);
            }
            wrt.write("\n");
            if (mZeoAppHandler != null) {
                if (mZeoAppHandler.mZeoApp_versionName == null) {
                    wrt.write("Zeo App not installed\n");
                } else {
                    wrt.write("Zeo App version " + mZeoAppHandler.mZeoApp_versionName + " build "
                            + mZeoAppHandler.mZeoApp_versionCode + "\n");
                }
            }
            wrt.write("Android Version " + android.os.Build.VERSION.RELEASE + " API "
                    + android.os.Build.VERSION.SDK_INT + "\n");
            wrt.write("Platform Manf " + Build.MANUFACTURER + " Model " + Build.MODEL + "\n");
            WindowManager windowManager = (WindowManager) mApp.getSystemService(Context.WINDOW_SERVICE);
            Display display = windowManager.getDefaultDisplay();
            Point screenSize = new Point();
            display.getSize(screenSize);
            wrt.write("Platform Screen Orientation X,Y " + screenSize.x + "," + screenSize.y + ", Density="
                    + mScreenDensity + "\n");
            if (method != null) {
                if (!method.isEmpty()) {
                    wrt.write(method + "\n");
                }
            }
            if (extra != null) {
                if (!extra.isEmpty()) {
                    wrt.write(extra + "\n");
                }
            }
            wrt.write(theException.toString() + "\n");
            for (StackTraceElement st : traces) {
                wrt.write(st.toString() + "\n");
            }
            wrt.write("=====\n");
            wrt.write("=====\n");
            wrt.flush();
            wrt.close();

            // force it to be shown and post an alert
            forceShowOnPC(errLogFile);
            if (!noAlert) {
                postAlert("An abort occured; details are in \'internals/error.log\'; contact the Developer");
            } // noAlert is only needed when an Alert itself was being posted to the database and it failed to post

            // must send the toast indirectly in case this is being called from a utility thread
            Message msg = new Message();
            msg.what = ZeoCompanionApplication.MESSAGE_APP_SEND_TOAST;
            msg.obj = "Abort successfully logged to \'internals/error.log\'";
            mAppHandler.sendMessage(msg);
        } catch (Exception e) {
            if (wrt != null) {
                try {
                    wrt.close();
                } catch (Exception ignored) {
                }
            }
            Log.e(_CTAG + ".postToErrLog", "Cannot write to error.log: " + e.toString());
            e.printStackTrace();
        }
    }

    public static void postToErrorLog(String method, String errorMessage, String extra) {
        postToErrorLog(method, errorMessage, extra, false);
    }

    public static void postToErrorLog(String method, String errorMessage, String extra, boolean noAlert) {
        String eMsg = "Severe Error";
        if (method != null) {
            if (!method.isEmpty()) {
                eMsg = eMsg + " in " + method;
            }
        }
        if (extra != null) {
            if (!extra.isEmpty()) {
                eMsg = eMsg + " (" + extra + ")";
            }
        }
        eMsg = eMsg + ": " + errorMessage;
        Log.e(_CTAG + ".postToErrorLog", eMsg);

        int r = checkExternalStorage();
        if (r != 0) {
            Log.e(_CTAG + ".postToErrorLog", "Cannot write to external storage code " + r);
            return;
        }

        FileWriter wrt = null;
        try {
            // ensure the directory structure is present and compose the file name
            File internalsDir = new File(mBaseExtStorageDir + File.separator + "internals");
            internalsDir.mkdirs();
            File errLogFile = new File(internalsDir + File.separator + "error.log");

            // create and append to the file
            wrt = new FileWriter(errLogFile, true); // append if file already exists
            wrt.write(new Date().toString() + "\n");
            wrt.write("Appver " + BuildConfig.VERSION_NAME);
            if (mDatabaseHandler != null) {
                wrt.write(" with DBver " + mDatabaseHandler.mVersion);
            }
            wrt.write("\n");
            if (mZeoAppHandler != null) {
                if (mZeoAppHandler.mZeoApp_versionName == null) {
                    wrt.write("Zeo App not installed\n");
                } else {
                    wrt.write("Zeo App version " + mZeoAppHandler.mZeoApp_versionName + " build "
                            + mZeoAppHandler.mZeoApp_versionCode + "\n");
                }
            }
            wrt.write("Android Version " + android.os.Build.VERSION.RELEASE + " API "
                    + android.os.Build.VERSION.SDK_INT + "\n");
            wrt.write("Platform Manf " + Build.MANUFACTURER + " Model " + Build.MODEL + "\n");
            if (method != null) {
                if (!method.isEmpty()) {
                    wrt.write(method + "\n");
                }
            }
            if (extra != null) {
                if (!extra.isEmpty()) {
                    wrt.write(extra + "\n");
                }
            }
            wrt.write(errorMessage + "\n");
            wrt.write("=====\n");
            wrt.write("=====\n");
            wrt.flush();
            wrt.close();

            // force it to be shown and post an alert
            forceShowOnPC(errLogFile);
            if (!noAlert) {
                postAlert("An abort occured; details are in \'internals/error.log\'; contact the Developer");
            } // noAlert is only needed when an Alert itself was being posted to the database and it failed to post

            // must send the toast indirectly in case this is being called from a utility thread
            Message msg = new Message();
            msg.what = ZeoCompanionApplication.MESSAGE_APP_SEND_TOAST;
            msg.obj = "Abort successfully logged to \'internals/error.log\'";
            mAppHandler.sendMessage(msg);
        } catch (Exception e) {
            if (wrt != null) {
                try {
                    wrt.close();
                } catch (Exception ignored) {
                }
            }
            Log.e(_CTAG + ".postToErrLog", "Cannot write to error.log: " + e.toString());
            e.printStackTrace();
        }
    }

    // Thread Context: may be called from utility threads, so cannot perform UI actions like Toast
    // write an alert message into the alerts.log file;
    // warning the database manager may be closed and therefore NULL
    public static void postAlert(String message) {
        if (ZeoCompanionApplication.mDatabaseHandler != null) {
            CompanionAlertRec aRec = new CompanionAlertRec(System.currentTimeMillis(), message);
            aRec.saveToDB(ZeoCompanionApplication.mDatabaseHandler);

            // inform the Main Activity so it can changes its menus; must be done via messaging
            if (MainActivity.instance != null) {
                if (MainActivity.instance.mHandler != null) {
                    Message msg = new Message();
                    msg.what = ZeoCompanionApplication.MESSAGE_MAIN_UPDATE_MENU;
                    MainActivity.instance.mHandler.sendMessage(msg);
                }
            }
        }
    }

    // are there any alerts present?
    public static int getQtyAlerts() {
        return ZeoCompanionApplication.mDatabaseHandler.getQtyCompanionAlertRecs();
    }

    // read all alerts in the indicate ArrayList
    public static void getAllAlerts(ArrayList<CompanionAlertRec> list) {
        Cursor cursor = ZeoCompanionApplication.mDatabaseHandler.getAllAlertRecs();
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                do {
                    CompanionAlertRec aRec = new CompanionAlertRec(cursor);
                    list.add(aRec);
                } while (cursor.moveToNext());
            }
            cursor.close();
        }
    }

    // delete one line in the alerts.log file
    public static void deleteAlertLine(long id) {
        CompanionAlertRec.removeFromDB(ZeoCompanionApplication.mDatabaseHandler, id);

        // inform the Main Activity so it can changes its menus; must be done via messaging
        if (MainActivity.instance != null) {
            Message msg = new Message();
            msg.what = ZeoCompanionApplication.MESSAGE_MAIN_UPDATE_MENU;
            MainActivity.instance.mHandler.sendMessage(msg);
        }
    }
}