org.opendatakit.common.android.logic.PropertiesSingleton.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.common.android.logic.PropertiesSingleton.java

Source

/*
 * Copyright (C) 2013-2014 University of Washington
 *
 * 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 org.opendatakit.common.android.logic;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.apache.commons.lang3.CharEncoding;
import org.opendatakit.IntentConsts;
import org.opendatakit.aggregate.odktables.rest.TableConstants;
import org.opendatakit.androidlibrary.R;
import org.opendatakit.common.android.utilities.ODKFileUtils;
import org.opendatakit.common.android.utilities.WebLogger;

import java.io.*;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;

/**
 * Properties are in 3 classes:
 *
 * (1) general (syncable) -- the contents of config/assets/app.properties
 * (2) device -- the contents of data/device.properties
 * (3) secure -- stored in SharedPreferences (ideally only within ODK Services)
 *
 * The tools provide the different sets of these and default values for these settings.
 *
 * If the general (syncable) file contains values for the device and secure settings,
 * these become the default settings for those values.
 *
 * Device settings and secure settings are not overwritten by changes in the
 * general (syncable) settings. You need to Reset the device configuration to
 * re-initialize these.
 */
public class PropertiesSingleton {

    private static final String t = "PropertiesSingleton";

    private static final String GENERAL_PROPERTIES_FILENAME = "app.properties";
    private static final String DEVICE_PROPERTIES_FILENAME = "device.properties";

    private static boolean isMocked = false;

    private final String mAppName;
    private long lastGeneralModified = 0L;
    private long lastDeviceModified = 0L;

    private final TreeMap<String, String> mGeneralDefaults;
    private final TreeMap<String, String> mDeviceDefaults;
    private final TreeMap<String, String> mSecureDefaults;

    private Properties mGeneralProps;
    private Properties mDeviceProps;
    private Context mBaseContext;

    public String getAppName() {
        return mAppName;
    }

    private boolean isSecureProperty(String propertyName) {
        return mSecureDefaults.containsKey(propertyName);
    }

    private boolean isDeviceProperty(String propertyName) {
        return mDeviceDefaults.containsKey(propertyName);
    }

    void setCurrentContext(Context context) {
        try {
            mBaseContext = context;
            // if we are re-using the existing one, pick up any changes by other apps
            // including, e.g., the reset of the configuration by ODK Services.
            if (isModified()) {
                init();
            }
        } catch (Exception e) {
            // TODO: remove the mocking logic? it looks like garbage...
            if (isMocked) {
                mBaseContext = context;
            } else {
                boolean faked = false;
                Context app = context.getApplicationContext();
                Class<?> classObj = app.getClass();
                String appName = classObj.getSimpleName();
                while (!appName.equals("CommonApplication")) {
                    classObj = classObj.getSuperclass();
                    if (classObj == null)
                        break;
                    appName = classObj.getSimpleName();
                }

                if (classObj != null) {
                    try {
                        Class<?>[] argClassList = new Class[] {};
                        Method m = classObj.getDeclaredMethod("isMocked", argClassList);
                        Object[] argList = new Object[] {};
                        Object o = m.invoke(null, argList);
                        if (((Boolean) o).booleanValue()) {
                            mBaseContext = context;
                            isMocked = true;
                            faked = true;
                        }
                    } catch (Exception e1) {
                    }
                }
                if (!faked) {
                    e.printStackTrace();
                    throw new IllegalStateException("ODK Services must be installed!");
                }
            }
            init();
        }
    }

    /**
     * SharedPreferences are ONLY available from within ODK Services.
     *
     * @param context
     * @return
      */
    private static SharedPreferences getSharedPreferences(Context context) {
        try {
            if (!context.getPackageName().equals(IntentConsts.AppProperties.APPLICATION_NAME)) {
                return null;
            }
            return context.getSharedPreferences(context.getPackageName(),
                    Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
        } catch (Exception e) {
            Log.e("PropertiesSingleton", "Unable to access SharedPreferences!");
            return null;
        }
    }

    public boolean containsKey(String propertyName) {
        if (isSecureProperty(propertyName)) {
            // this needs to be stored in a protected area
            SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
            return (sharedPreferences == null) ? false : sharedPreferences.contains(mAppName + "_" + propertyName);
        } else if (isDeviceProperty(propertyName)) {
            return mDeviceProps.containsKey(propertyName);
        } else {
            return mGeneralProps.containsKey(propertyName);
        }
    }

    /**
     * Accesses the given propertyName. This may be stored in SharedPreferences or
     * in the PROPERTIES_FILENAME in the config/assets directory.
     * 
     * @param propertyName
     * @return null or the string value
     */
    public String getProperty(String propertyName) {
        if (isSecureProperty(propertyName)) {
            // this needs to be stored in a protected area
            SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
            return (sharedPreferences == null) ? null
                    : sharedPreferences.getString(mAppName + "_" + propertyName, null);
        } else if (isDeviceProperty(propertyName)) {
            return mDeviceProps.getProperty(propertyName);
        } else {
            return mGeneralProps.getProperty(propertyName);
        }
    }

    /**
     * Accesses the given propertyName. This may be stored in SharedPreferences or
     * in the PROPERTIES_FILENAME in the config/assets directory.
     * 
     * If the value is not specified, null or an empty string, a null value is
     * returned. Boolean.TRUE is returned if the value is "true", otherwise
     * Boolean.FALSE is returned.
     * 
     * @param propertyName
     * @return null or boolean true/false
     */
    public Boolean getBooleanProperty(String propertyName) {
        Boolean booleanSetting = Boolean.TRUE;
        String value = getProperty(propertyName);
        if (value == null || value.length() == 0) {
            return null;
        }

        if (!"true".equalsIgnoreCase(value)) {
            booleanSetting = Boolean.FALSE;
        }

        return booleanSetting;
    }

    public void setBooleanProperty(String propertyName, boolean value) {
        setProperty(propertyName, Boolean.toString(value));
    }

    /**
     * Accesses the given propertyName. This may be stored in SharedPreferences or
     * in the PROPERTIES_FILENAME in the config/assets directory.
     * 
     * If the value is not specified, null or an empty string, or if the value
     * cannot be parsed as an integer, then null is return. Otherwise, the integer
     * value is returned.
     * 
     * @param propertyName
     * @return
     */
    public Integer getIntegerProperty(String propertyName) {
        String value = getProperty(propertyName);
        if (value == null) {
            return null;
        }
        try {
            int v = Integer.parseInt(value);
            return v;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    public void setIntegerProperty(String propertyName, int value) {
        setProperty(propertyName, Integer.toString(value));
    }

    /**
     * Caller is responsible for calling writeProperties() to persist this value
     * to disk.
     * 
     * @param propertyName
     */
    public void removeProperty(String propertyName) {
        if (isSecureProperty(propertyName)) {
            // this needs to be stored in a protected area
            SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
            if (sharedPreferences != null) {
                sharedPreferences.edit().remove(mAppName + "_" + propertyName).commit();
            } else {
                throw new IllegalStateException("Unable to remove SharedPreferences");
            }
        } else if (isDeviceProperty(propertyName)) {
            mDeviceProps.remove(propertyName);
        } else {
            mGeneralProps.remove(propertyName);
        }
    }

    /**
     * Caller is responsible for calling writeProperties() to persist this value
     * to disk.
     * 
     * @param propertyName
     * @param value
     */
    public void setProperty(String propertyName, String value) {
        if (isSecureProperty(propertyName)) {
            // this needs to be stored in a protected area
            SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
            if (sharedPreferences != null) {
                sharedPreferences.edit().putString(mAppName + "_" + propertyName, value).commit();
            } else {
                throw new IllegalStateException("Unable to write SharedPreferences");
            }
        } else {
            if (isModified()) {
                readProperties();
            }
            if (isDeviceProperty(propertyName)) {
                mDeviceProps.setProperty(propertyName, value);
            } else {
                mGeneralProps.setProperty(propertyName, value);
            }
            writeProperties();
        }
    }

    /**
     * Determine whether or not the initialization task for the given toolName
     * should be run.
     *
     * @param toolName
     *          (e.g., survey, tables, scan, etc.)
     */
    public boolean shouldRunInitializationTask(String toolName) {
        // this is stored in the device properties
        if (isModified()) {
            readProperties();
        }

        String value = mDeviceProps.getProperty(toolInitializationPropertyName(toolName));
        if (value == null || value.length() == 0) {
            return Boolean.TRUE;
        }

        return Boolean.FALSE;
    }

    /**
     * Indicate that the initialization task for this given tool has been run.
     *
     * @param toolName
     */
    public void clearRunInitializationTask(String toolName) {
        // this is stored in the device properties
        if (isModified()) {
            readProperties();
        }

        mDeviceProps.setProperty(toolInitializationPropertyName(toolName),
                TableConstants.nanoSecondsFromMillis(System.currentTimeMillis()));
        writeProperties();
    }

    /**
     * Indicate that the initialization task for this given tool should be run
     * (again).
     *
     * @param toolName
     */
    public void setRunInitializationTask(String toolName) {
        // this is stored in the device properties
        if (isModified()) {
            readProperties();
        }

        mDeviceProps.remove(toolInitializationPropertyName(toolName));
        writeProperties();
    }

    public String getActiveUser() {
        final String CREDENTIAL_TYPE_NONE = mBaseContext.getString(R.string.credential_type_none);
        final String CREDENTIAL_TYPE_USERNAME_PASSWORD = mBaseContext
                .getString(R.string.credential_type_username_password);
        final String CREDENTIAL_TYPE_GOOGLE_ACCOUNT = mBaseContext
                .getString(R.string.credential_type_google_account);

        String authType = getProperty(CommonToolProperties.KEY_AUTHENTICATION_TYPE);
        if (authType.equals(CREDENTIAL_TYPE_NONE)) {
            return "anonymous";
        } else if (authType.equals(CREDENTIAL_TYPE_USERNAME_PASSWORD)) {
            String name = getProperty(CommonToolProperties.KEY_USERNAME);
            if (name != null) {
                return "username:" + name;
            } else {
                return "anonymous";
            }
        } else if (authType.equals(CREDENTIAL_TYPE_GOOGLE_ACCOUNT)) {
            String name = getProperty(CommonToolProperties.KEY_ACCOUNT);
            if (name != null) {
                return "mailto:" + name;
            } else {
                return "anonymous";
            }
        } else {
            throw new IllegalStateException("unexpected authentication type!");
        }
    }

    public String getLocale() {
        return Locale.getDefault().toString();
    }

    private static String toolInitializationPropertyName(String toolName) {
        return toolName + ".tool_last_initialization_start_time";
    }

    public static String toolVersionPropertyName(String toolName) {
        return toolName + ".tool_version_code";
    }

    public static String toolFirstRunPropertyName(String toolName) {
        return toolName + ".tool_first_run";
    }

    PropertiesSingleton(Context context, String appName, TreeMap<String, String> plainDefaults,
            TreeMap<String, String> deviceDefaults, TreeMap<String, String> secureDefaults) {
        mAppName = appName;
        mGeneralDefaults = plainDefaults;
        mDeviceDefaults = deviceDefaults;
        mSecureDefaults = secureDefaults;

        // initialize the cache of properties read from the sdcard
        mGeneralProps = new Properties();
        mDeviceProps = new Properties();

        // set our context
        // this will automatically call init();
        setCurrentContext(mBaseContext);
    }

    void init() {
        // (re)set values to defaults

        lastGeneralModified = 0L;
        lastDeviceModified = 0L;

        mGeneralProps.clear();
        mDeviceProps.clear();

        // populate the caches from disk...
        readProperties();

        boolean dirtyProps = false;

        // see if there are missing values in the general props
        // and update them from the mGeneralDefaults map.
        for (TreeMap.Entry<String, String> entry : mGeneralDefaults.entrySet()) {
            if (mGeneralProps.containsKey(entry.getKey()) == false) {
                mGeneralProps.setProperty(entry.getKey(), entry.getValue());
                dirtyProps = true;
            }
        }

        // scan for device properties in the (syncable) app properties file.
        // update the provided mDeviceDefaults with these new default values.
        for (TreeMap.Entry<String, String> entry : mDeviceDefaults.entrySet()) {
            if (mGeneralProps.containsKey(entry.getKey())) {
                entry.setValue(mGeneralProps.getProperty(entry.getKey()));
            }
        }

        // see if there are missing values in the device props
        // and update them from the mGeneralDefaults map.
        for (TreeMap.Entry<String, String> entry : mDeviceDefaults.entrySet()) {
            if (mDeviceProps.containsKey(entry.getKey()) == false) {
                mDeviceProps.setProperty(entry.getKey(), entry.getValue());
                dirtyProps = true;
            }
        }

        // scan for secure properties in the (syncable) app properties file.
        // remove these and do not propagate them into SharedPreferences.
        for (TreeMap.Entry<String, String> entry : mSecureDefaults.entrySet()) {
            if (mGeneralProps.containsKey(entry.getKey())) {
                mGeneralProps.remove(entry.getKey());
                dirtyProps = true;
            }
        }

        // Now, scan through the shared preferences.  These will only be available
        // from within ODK Services. If we try to access them outside of that
        // application, this will be a no-op.
        SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
        if (sharedPreferences != null) {
            for (TreeMap.Entry<String, String> entry : mSecureDefaults.entrySet()) {
                // NOTE: can't use the methods because this object is not yet fully created
                if (!sharedPreferences.contains(mAppName + "_" + entry.getKey())) {
                    sharedPreferences.edit().putString(mAppName + "_" + entry.getKey(), entry.getValue()).commit();
                }
            }
        }

        if (dirtyProps) {
            writeProperties();
        }
    }

    private void verifyDirectories() {
        try {
            ODKFileUtils.verifyExternalStorageAvailability();
            ODKFileUtils.assertDirectoryStructure(mAppName);
        } catch (Exception e) {
            Log.e(t, "External storage not available");
            throw new IllegalArgumentException("External storage not available");
        }
    }

    public boolean isModified() {
        File configFile;

        configFile = new File(ODKFileUtils.getAssetsFolder(mAppName), GENERAL_PROPERTIES_FILENAME);

        if (configFile.exists()) {
            if (lastGeneralModified != configFile.lastModified()) {
                return true;
            }
        } else {
            // doesn't exist -- ergo, it has changed.
            return true;
        }

        configFile = new File(ODKFileUtils.getAssetsFolder(mAppName), DEVICE_PROPERTIES_FILENAME);

        if (configFile.exists()) {
            if (lastDeviceModified != configFile.lastModified()) {
                return true;
            }
        } else {
            // doesn't exist -- ergo, it has changed.
            return true;
        }

        return false;
    }

    public void readProperties() {
        verifyDirectories();

        FileInputStream configFileInputStream = null;
        try {
            File configFile = new File(ODKFileUtils.getAssetsFolder(mAppName), GENERAL_PROPERTIES_FILENAME);

            if (configFile.exists()) {
                configFileInputStream = new FileInputStream(configFile);

                mGeneralProps.loadFromXML(configFileInputStream);
                lastGeneralModified = configFile.lastModified();
            }
        } catch (Exception e) {
            WebLogger.getLogger(mAppName).printStackTrace(e);
        } finally {
            if (configFileInputStream != null) {
                try {
                    configFileInputStream.close();
                } catch (IOException e) {
                    // ignore
                    WebLogger.getLogger(mAppName).printStackTrace(e);
                }
            }
        }

        configFileInputStream = null;
        try {
            File configFile = new File(ODKFileUtils.getDataFolder(mAppName), DEVICE_PROPERTIES_FILENAME);

            if (configFile.exists()) {
                configFileInputStream = new FileInputStream(configFile);

                mDeviceProps.loadFromXML(configFileInputStream);
                lastDeviceModified = configFile.lastModified();
            }
        } catch (Exception e) {
            WebLogger.getLogger(mAppName).printStackTrace(e);
        } finally {
            if (configFileInputStream != null) {
                try {
                    configFileInputStream.close();
                } catch (IOException e) {
                    // ignore
                    WebLogger.getLogger(mAppName).printStackTrace(e);
                }
            }
        }
    }

    public void writeProperties() {
        verifyDirectories();

        try {
            File tempConfigFile = new File(ODKFileUtils.getAssetsFolder(mAppName),
                    GENERAL_PROPERTIES_FILENAME + ".temp");
            FileOutputStream configFileOutputStream = new FileOutputStream(tempConfigFile, false);

            mGeneralProps.storeToXML(configFileOutputStream, null, CharEncoding.UTF_8);
            configFileOutputStream.close();

            File configFile = new File(ODKFileUtils.getAssetsFolder(mAppName), GENERAL_PROPERTIES_FILENAME);

            boolean fileSuccess = tempConfigFile.renameTo(configFile);

            if (!fileSuccess) {
                WebLogger.getLogger(mAppName).i(t, "Temporary General Config File Rename Failed!");
            } else {
                lastGeneralModified = configFile.lastModified();
            }

        } catch (Exception e) {
            WebLogger.getLogger(mAppName).printStackTrace(e);
        }

        try {
            File tempConfigFile = new File(ODKFileUtils.getDataFolder(mAppName),
                    DEVICE_PROPERTIES_FILENAME + ".temp");
            FileOutputStream configFileOutputStream = new FileOutputStream(tempConfigFile, false);

            mDeviceProps.storeToXML(configFileOutputStream, null, CharEncoding.UTF_8);
            configFileOutputStream.close();

            File configFile = new File(ODKFileUtils.getDataFolder(mAppName), DEVICE_PROPERTIES_FILENAME);

            boolean fileSuccess = tempConfigFile.renameTo(configFile);

            if (!fileSuccess) {
                WebLogger.getLogger(mAppName).i(t, "Temporary Device Config File Rename Failed!");
            } else {
                lastDeviceModified = configFile.lastModified();
            }

        } catch (Exception e) {
            WebLogger.getLogger(mAppName).printStackTrace(e);
        }
    }

    public void clearSettings() {
        try {
            File f;
            f = new File(ODKFileUtils.getDataFolder(mAppName), DEVICE_PROPERTIES_FILENAME);
            if (f.exists()) {
                f.delete();
            }

            // and now go through the shared preferences and delete any that pertain to this appName
            SharedPreferences sharedPreferences = getSharedPreferences(mBaseContext);
            if (sharedPreferences != null) {
                Map<String, ?> allPreferences = sharedPreferences.getAll();
                for (String key : allPreferences.keySet()) {
                    if (key.startsWith(mAppName + "_")) {
                        sharedPreferences.edit().remove(key).commit();
                    }
                }
            } else {
                throw new IllegalStateException("Clearing settings should only be done within ODK Services");
            }
        } finally {
            init();
        }
    }
}