public class LQClientManager {
    // Constants
    public static final String PROPERTY_REG_ID = "registration_id";
    private static final String PROPERTY_APP_VERSION = "appVersion";
    private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

    // Member variables
    private GoogleCloudMessaging mGCM;
    private String mRegid;
    private String mSenderId;
    private Activity mActivity;

    public static abstract class RegistrationCompletedHandler {
        public abstract void onSuccess(String registrationId, boolean isNewRegistration);
        public void onFailure(String ex) {
            LQLog.error(ex);
        }
    }

    public LQClientManager(Activity activity, String senderid) {
        this.mActivity = activity;
        this.mSenderId = senderid;
        this.mGCM = GoogleCloudMessaging.getInstance(activity);
    }

    // Register if needed or fetch from shared prefs
    public void registerIfNeeded(final RegistrationCompletedHandler handler) {
        if (checkPlayServices()) {
            mRegid = getRegistrationId(getContext());

            if (mRegid.isEmpty()) {
                registerInBackground(handler);
            } else {
                LQLog.info("Got from cache: " + mRegid);
                handler.onSuccess(mRegid, false);
            }
        } else { // no play services
            LQLog.infoVerbose("No valid Google Play Services APK found.");
        }
    }

    // Registers the application with GCM servers asynchronously and stores the
    // registration ID and app versionCode in the application's shared prefs
    private void registerInBackground(final RegistrationCompletedHandler handler) {
        new AsyncTask<Void, Void, String>() {
            @Override
            protected String doInBackground(Void... params) {
                try {
                    if (mGCM == null) {
                        mGCM = GoogleCloudMessaging.getInstance(getContext());
                    }
                    InstanceID instanceID = InstanceID.getInstance(getContext());
                    mRegid = instanceID.getToken(mSenderId, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
                    LQLog.info("Registration ID: " + mRegid);

                    // Persist the regID - no need to register again.
                    Liquid.getInstance().setGCMregistrationID(mRegid);
                    storeRegistrationId(getContext(), mRegid);

                } catch (IOException ex) {
                    // If there is an error, don't just keep trying to register.
                    // Require the user to click a button again, or perform
                    // exponential back-off.
                    handler.onFailure(ex.getMessage());
                }
                return mRegid;
            }

            @Override
            protected void onPostExecute(String regId) {
                if (regId != null) {
                    handler.onSuccess(regId, true);
                }
            }
        }.execute(null, null, null);
    }

    // Gets the registration ID from shared prefs
    private String getRegistrationId(Context context) {
        final SharedPreferences prefs = getGCMPreferences(context);
        String registrationId = prefs.getString(PROPERTY_REG_ID, "");
        if (registrationId.isEmpty()) {
            LQLog.infoVerbose("Registration not found.");
            return "";
        }

        // Check if app was updated; if so, it must clear the registration ID
        // since the existing regID is not guaranteed to work with the new
        // app version.
        int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
        int currentVersion = getAppVersion(context);
        if (registeredVersion != currentVersion) {
            LQLog.infoVerbose("App version changed.");
            return "";
        }
        return registrationId;
    }

    // Stores registration Id and app versionCode in the application's
    // shared prefs
    private void storeRegistrationId(Context context, String regId) {
        final SharedPreferences prefs = getGCMPreferences(context);
        int appVersion = getAppVersion(context);
        LQLog.infoVerbose("Saving regId on app version " + appVersion);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(PROPERTY_REG_ID, regId);
        editor.putInt(PROPERTY_APP_VERSION, appVersion);
        editor.commit();
    }

    // Returns application's version code
    private static int getAppVersion(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager()
                    .getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (NameNotFoundException e) {
            // should never happen
            throw new RuntimeException("Could not get package name: " + e);
        }
    }

    private SharedPreferences getGCMPreferences(Context context) {
        return getContext().getSharedPreferences(context.getPackageName(),
                Context.MODE_PRIVATE);
    }

    // Checks if the device has Google Play Services installed. If not display
    // a dialog to download the APK or enable it in system settings
    private boolean checkPlayServices() {
        int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getContext());
        if (resultCode != ConnectionResult.SUCCESS) {
            if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
                GooglePlayServicesUtil.getErrorDialog(resultCode, getActivity(),
                        PLAY_SERVICES_RESOLUTION_REQUEST).show();
            } else {
                LQLog.error("This device is not supported.");
            }
            return false;
        }
        return true;
    }

    private Context getContext() {
        return mActivity.getApplicationContext();
    }

    private Activity getActivity() {
        return mActivity;
    }
} public class LQDataPoint {


    private LQUser mUser;
    private LQDevice mDevice;
    private LQEvent mEvent;
    private ArrayList<LQValue> mValues;
    private Date mTimestamp;

    public LQDataPoint(LQUser user, LQDevice device, LQEvent event, ArrayList<LQValue> values, Date date) {
        mUser = user;
        mDevice = device;
        mEvent = event;
        mValues = values;
        mTimestamp = date;
    }

    public LQDataPoint(LQUser user, LQDevice device, LQEvent event, ArrayList<LQValue> values) {
        this(user,device,event,values, new Date());
    }

    // JSON
    public JSONObject toJSON() {
        JSONObject json = new JSONObject();
        try {
            JSONObject userJSON = mUser.toJSON();
            if(userJSON != null){
                json.put("user", userJSON);
            }

            JSONObject deviceJSON = mDevice.toJSON();
            if(deviceJSON != null){
                json.put("device", deviceJSON);
            }

            JSONObject eventJSON = mEvent.toJSON();
            if(eventJSON != null){
                json.put("event", eventJSON);
            }

            JSONArray valuesJsonArray = new JSONArray();
            for(LQValue value : mValues){
                JSONObject valueJSON = value.toJSON();
                if(valueJSON != null){
                    valuesJsonArray.put(valueJSON);
                }
            }

            if(valuesJsonArray.length() > 0) {
                json.put("values", valuesJsonArray);
            }

            json.put("timestamp",LiquidTools.dateToString(mTimestamp));
            return json;
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return new JSONObject();
    }

} public class LQEvent extends LQModel {

    private static final long serialVersionUID = 4817275328778708775L;

    public static final String UNNAMED_EVENT = "unnamedEvent";

    private String mName;
    private HashMap<String,Object> mAttributes;
    private Date mDate;

    // Initialization
    public LQEvent(String name, HashMap<String,Object> attributes, Date date){
        setName(name);

        if(attributes == null) {
            mAttributes = new HashMap<String,Object>();
        } else {
            mAttributes = attributes;
        }
        mDate = date;
    }

    public LQEvent(String name, HashMap<String,Object> attributes) {
        this(name,attributes, new Date());
    }

    public void setName(String name) {
        if ((name == null) || (name.length() == 0)) {
            mName = UNNAMED_EVENT;
        } else {
            mName = name;
        }
    }

    public String getName() {
        return mName;
    }

    // JSON
    public JSONObject toJSON() {
        JSONObject json = new JSONObject();
        try {
            if(mAttributes != null) {
                for(String key : mAttributes.keySet()){
                    if(mAttributes.get(key) instanceof Date) {
                        json.put(key, LiquidTools.dateToString((Date) mAttributes.get(key)));
                    } else {
                        json.put(key, mAttributes.get(key));
                    }
                }
            }
            json.put("name", mName);
            json.put("date",LiquidTools.dateToString(mDate));
            return json;
        } catch (JSONException e) {
            LQLog.error("LQEvent toJSON: " + e.getMessage());
        }

        return null;
    }

    public static boolean hasValidName(String name, boolean raiseException) {
        boolean valid = name == null || name.length() == 0 || (name.charAt(0) != '_' && !name.contains("$") && !name.contains(".") && !name.contains("\0"));
        if(!valid) {
            LiquidTools.exceptionOrLog(raiseException, "Event name cannot start with \'_\' and cannot contain invalid chars: (. $ \\0)");
        }
        return valid;
    }
} public class LQInAppMessage {

    private String mId;
    private String mFormulaId;
    private String mTitleColor;
    private String mTitle;
    private String mBgColor;
    private String mMessage;
    private String mMessageColor;
    private String mDismissEventName;
    private ArrayMap<String, Object> mDismissAttributes;
    private String mType;
    private String mLayout;
    private ArrayList<Cta> mCtas = new ArrayList<>();

    public LQInAppMessage(JSONObject inapp){
        try {
            mLayout = inapp.optString("layout");
            if ("modal".equals(mLayout))
                try {
                    mTitle = inapp.getString("title");
                    mTitleColor = inapp.getString("title_color");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            mBgColor = inapp.getString("bg_color");
            mMessage = inapp.getString("message");
            mMessageColor = inapp.getString("message_color");
            mDismissEventName = inapp.getString("dismiss_event_name");
            mType = inapp.getString("type");

            JSONObject event_attributes = inapp.getJSONObject("event_attributes");
            mFormulaId = event_attributes.getString("formula_id");
            mId = event_attributes.getString("id");
            mDismissAttributes = new ArrayMap<>();
            mDismissAttributes.put("formula_id", mFormulaId);
            mDismissAttributes.put("id", mId);
            JSONArray ctas = inapp.getJSONArray("ctas");
            int ctas_length = ctas.length();

            for (int j = 0; j < ctas_length; j++) {
                mCtas.add(new Cta(ctas.getJSONObject(j)));
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    public String getLayout(){ return mLayout; }

    public String getFormulaId(){ return mFormulaId; }

    public String getId(){ return mId; }

    public String getTitle() { return mTitle; }

    public String getTitleColor(){ return mTitleColor; };

    public String getMessage(){ return mMessage; }

    public String getMessageColor(){ return mMessageColor; }

    public String getBgColor(){ return mBgColor; }

    public String getType(){ return mType; }

    public String getDismissEventName() { return mDismissEventName; }

    public ArrayMap<String, Object> getDismissAttributes(){ return mDismissAttributes; }

    public ArrayList<Cta> getCtas() {
        return mCtas;
    }

    public static class Cta {

        private String mDeepLink;
        private String mCtaId;
        private String mCtaFormulaId;
        private String mButtonColor;
        private String mButtonText;
        private String mButtonTextColor;
        private String mCtasEventName;
        private ArrayMap<String , Object> mCtasAttributes;

        public Cta(JSONObject cta) {
            try {
                mButtonColor = cta.getString("bg_color");
                mCtasEventName = cta.getString("event_name");

                JSONObject cta_attributes = cta.getJSONObject("cta_attributes");

                mCtaFormulaId = cta_attributes.getString("formula_id");
                mCtaId = cta_attributes.getString("id");
                mCtasAttributes = new ArrayMap<>();
                mCtasAttributes.put("formula_id", mCtaFormulaId);
                mCtasAttributes.put("cta_id", mCtaId);

                mButtonText = cta.getString("title");
                mButtonTextColor = cta.getString("title_color");
                mDeepLink = cta.getString("android_url");
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        public String getDeepLink() { return mDeepLink; }

        public String getButtonText(){ return mButtonText; }

        public String getButtonTextColor(){ return mButtonTextColor; }

        public String getButtonColor(){ return mButtonColor; }

        public String getCtasEventName(){ return mCtasEventName; }

        public ArrayMap<String, Object> getCtasAttributes(){ return mCtasAttributes; }
    }
} public class LQLiquidPackage implements Serializable{

    private static final long serialVersionUID = 2252438865270376L;
    private static final String LIQUID_PACKAGE_FILENAME = "LiquidPackage";

    private ArrayList<LQValue> mValues = new ArrayList<LQValue>();

    public LQLiquidPackage() {

    }


    public LQLiquidPackage(JSONObject jsonObject) {
        try {
            JSONArray valuesJsonArray = jsonObject.getJSONArray("values");
            for(int index = 0; index < valuesJsonArray.length(); index ++){
                JSONObject valueJson = valuesJsonArray.getJSONObject(index);
                LQValue v = new LQValue(valueJson);
                mValues.add(v);
            }
        } catch (JSONException e) {
            LQLog.error("Parsing LQLiquidPackage: " + e.getMessage());
        }

    }

    public boolean isEmpty() {
        return (mValues.size() == 0);
    }

    public ArrayList<LQValue> getValues() {
        return mValues;
    }

    public boolean invalidateTargetFromVariableKey(String variableKey) {
        boolean invalidated = false;
        for(LQValue value : mValues) {
            if(value.getVariable().getName().equals(variableKey)) {
                if(value.getTargetId() == null) {
                    invalidated = invalidateValue(variableKey);
                } else {
                    invalidated = invalidateValuesOfTarget(value.getTargetId());
                }
            }
        }
        return invalidated;
    }

    private boolean invalidateValuesOfTarget(String targetId) {
        int valueItems = mValues.size();
        ArrayList<LQValue> tempvar =  new ArrayList<LQValue>();

        for(LQValue value : mValues) {
            if(!targetId.equals(value.getTargetId())) {
                tempvar.add(value);
                --valueItems;
            }
        }
        mValues = tempvar;
        return valueItems > 0;
    }

    private boolean invalidateValue(String variableKey) {
        int valueItems = mValues.size();
        ArrayList<LQValue> tempvar =  new ArrayList<LQValue>();

        for(LQValue value : mValues) {
            if(!value.getVariable().getName().equals(variableKey)) {
                tempvar.add(value);
                --valueItems;
            }
        }
        mValues = tempvar;
        return valueItems > 0;
    }

    public void saveToDisk(Context context) {
        LQLog.data("Saving to local storage");
        try {
            FileOutputStream fileOutputStream = context.openFileOutput(
                    LIQUID_PACKAGE_FILENAME + ".vars",
                    Context.MODE_PRIVATE);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(
                    fileOutputStream);
            objectOutputStream.writeObject(this);
            objectOutputStream.flush();
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            LQLog.infoVerbose("Could not save liquid package to file");
        }
    }

    public static LQLiquidPackage loadFromDisk(Context context){
        LQLog.data("Loading from local storage");
        try {
            FileInputStream fileInputStream = context
                    .openFileInput(LIQUID_PACKAGE_FILENAME
                            + ".vars");
            ObjectInputStream objectInputStream = new ObjectInputStream(
                    fileInputStream);
            Object result = objectInputStream.readObject();
            objectInputStream.close();
            return (LQLiquidPackage) result;
        } catch (IOException e) {
            LQLog.infoVerbose("Could not load liquid package from file");
        } catch (ClassNotFoundException e) {
            LQLog.infoVerbose("Could not load liquid package from file");
        }
        return new LQLiquidPackage();
    }

} public abstract class LQModel implements Serializable {

    private static final long serialVersionUID = 1L;
    private static final String PREF_FILE_NAME = "LQPrefs";

    /**
     * Generate a random unique id
     * @return
     */
    public static String newIdentifier() {
        String uid = UUID.randomUUID().toString().toUpperCase(Locale.ENGLISH);
        String epoch = LiquidTools.tenCharEpoch(Calendar.getInstance().getTimeInMillis());
        return uid + "-" + epoch;
    }

    /*
     * *******************
     * Attributes Assertion
     * *******************
     */

    /**
     * Check if the key have invalid chars: { $ . \0 } are the invalid attributes
     * @param key key that will be checked
     * @param raiseException if true will raise IllegalArgumentException, otherwise will Log
     * @return true if the key is valid, false otherwise
     */
    public static boolean validKey(String key, boolean raiseException) {
        boolean isValid = (!key.contains("$") && !key.contains(".") && !key.contains("\0"));
        if(!isValid) {
            LiquidTools.exceptionOrLog(raiseException, "Key: (" + key + ") contains invalid chars: (. $ \\0)");
        }
        return isValid;
    }

    /**
     * Check if the attribute type is valid: {null, String, Number, Boolean, Date} are the valid attributes
     * @param attribute attribute that will be checked
     * @param raiseException if true will raise IllegalArgumentException, otherwise will Log
     * @return true if the attribute is valid, false otherwise
     */
    public static boolean validValue(Object attribute, boolean raiseException) {
        boolean isValid = ((attribute == null) || (attribute instanceof String) ||
                (attribute instanceof Number) || (attribute instanceof Boolean) ||
                (attribute instanceof Date));
        if(!isValid) {
            LiquidTools.exceptionOrLog(raiseException, "Value (" + attribute + ") with unsupported type. Supported: (String, Number, Boolean, Date)");
        }
        return isValid;
    }

    public static HashMap<String, Object> sanitizeAttributes(Map<String, Object> attributes, boolean raiseException) {
        if (attributes == null) {
            return null;
        }
        HashMap<String, Object> attrs = new HashMap<String, Object>();
        for (String key : attributes.keySet()) {
            if (LQModel.validKey(key, raiseException) && LQModel.validValue(attributes.get(key), raiseException)) {
                attrs.put(key, attributes.get(key));
            }
        }
        return attrs;
    }

    public void save(Context context, String path) {
        String serializedObject = toJSON().toString();

        SharedPreferences preferences = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor prefsEditor = preferences.edit();

        String[] split = path.split("\\.");

        prefsEditor.putString(split[1], serializedObject);

        LQLog.infoVerbose("Saving " + split[1] + " to shared prefs");
        prefsEditor.apply();
    }

    public static JSONObject retriveFromFile(Context context, String path) {
        String retrivedData;

        SharedPreferences preferences = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE);

        String[] split = path.split("\\.");

        retrivedData = preferences.getString(split[1], "");

        try {
            return new JSONObject(retrivedData);
        } catch (JSONException e) {
            LQLog.infoVerbose("Couldn't retrieve " + split[1] + " from file, probably new user");
        }
        return new JSONObject();
    }

    protected static HashMap<String, Object> attributesFromJSON(JSONObject object, String[] excludedKeys) {
        HashMap<String, Object> hashMap = new HashMap<>();

        if(object != JSONObject.NULL)
            hashMap = toHashMap(object, excludedKeys);
        return hashMap;
    }


    private static HashMap<String, Object> toHashMap(JSONObject object, String[] excludedKeys) {
        HashMap<String, Object> hashmap = new HashMap<>();

        Iterator<String> keysItr = object.keys();

        while(keysItr.hasNext()) {
            String key = keysItr.next();
            if (!Arrays.asList(excludedKeys).contains(key)) {
                Object value = null;
                try {
                    value = object.get(key);
                } catch (JSONException e) {
                    e.printStackTrace();
                }

                if (value instanceof JSONArray) {
                    value = toList((JSONArray) value, excludedKeys);
                } else if (value instanceof JSONObject) {
                    value = toHashMap((JSONObject) value, excludedKeys);
                }
                hashmap.put(key, value);
            }
        }
        return hashmap;
    }

    public static List<Object> toList(JSONArray array, String[] excludedKeys) {
        List<Object> list = new ArrayList<>();

        for(int i = 0; i < array.length(); i++) {
            Object value = null;
            try {
                value = array.get(i);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            if(value instanceof JSONArray) {
                value = toList((JSONArray) value, excludedKeys);
            }

            else if(value instanceof JSONObject) {
                value = toHashMap((JSONObject) value, excludedKeys);
            }
            list.add(value);
        }
        return list;
    }


    public abstract JSONObject toJSON();
} public class LQNetworkRequest extends LQModel {

    public static final int HALF_HOUR = 30 * 60 * 1000;

    private static final long serialVersionUID = 7456534930025458866L;

    private static final String LOCAL = Locale.getDefault().toString();
    private static final String DEVICE = Build.MANUFACTURER + " " + Build.MODEL;

    private static final String USER_AGENT = "Liquid/" + Liquid.LIQUID_VERSION + " (Android; Android " + Build.VERSION.RELEASE + "; " + LOCAL + "; " + DEVICE + ")";
    private String mUrl;
    private String mHttpMethod;
    private String mJson;
    private int mNumberOfTries;
    private Date mLastTry;
    private SimpleDateFormat mDateFormat;

    public LQNetworkRequest(String url, String httpMethod, String json) {
        mUrl = url;
        mHttpMethod = httpMethod;
        mJson = json;
        mNumberOfTries = 0;
        mLastTry = null;
        mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
    }

    public String getUrl() {
        return mUrl;
    }
    public String getHttpMethod() {
        return mHttpMethod;
    }
    public String getJSON() {
        return mJson;
    }
    public int getNumberOfTries() {
        return mNumberOfTries;
    }

    public void incrementNumberOfTries() {
        mNumberOfTries++;
    }

    public void setLastTry(Date lastTry) {
        mLastTry = lastTry;
    }

    public Date getLastTry() {
        return mLastTry;
    }

    public boolean willFlushAndSet(Date now) {
        boolean willflush = canFlush(now);
        if(!willflush) {
            mLastTry = now;
        }
        return willflush;
    }

    public boolean canFlush(Date now) {
        return mLastTry == null ||  now.getTime() - mLastTry.getTime() >= HALF_HOUR;
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof LQNetworkRequest) &&
                ((LQNetworkRequest) o).getHttpMethod().equals(this.getHttpMethod()) &&
                ((LQNetworkRequest) o).getUrl().equals(this.getUrl()) &&
                ((LQNetworkRequest) o).getJSON().equals(this.getJSON());
    }

    public LQNetworkResponse sendRequest(String token) {
        String response = null;
        String date = mDateFormat.format(Calendar.getInstance().getTime());
        int responseCode = -1;
        InputStream err = null;
        BufferedReader boin = null;
        DataOutputStream outputStream = null;
        HttpURLConnection connection = null;
        try {
            URL url = new URL(this.getUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod(this.getHttpMethod());
            connection.setRequestProperty("Date", date);
            connection.setRequestProperty("Authorization", "Token " + token);
            connection.setRequestProperty("User-Agent", USER_AGENT);
            connection.setRequestProperty("Accept", "application/vnd.lqd.v1+json");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Accept-Encoding", "gzip");
            connection.setDoInput(true);
            if (this.getJSON() != null) {
                connection.setDoOutput(true);
                outputStream = new DataOutputStream(connection.getOutputStream());
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
                writer.write(this.getJSON());
                writer.close();
                outputStream.close();
            }
            responseCode = connection.getResponseCode();
            err = connection.getErrorStream();
            GZIPInputStream gzip = new GZIPInputStream(connection.getInputStream());
            boin = new BufferedReader(new InputStreamReader(gzip, "UTF-8"));
            response = boin.readLine();
        } catch (IOException e) {
            LQLog.http("Failed due to " + e + " responseCode " + responseCode);
            LQLog.http("Error " + inputStreamToString(err));
        } finally {
            if(connection != null)
                connection.disconnect();
            try {
                if(outputStream != null)
                    outputStream.close();
            } catch (IOException e) {}
            try {
                if(err != null)
                    err.close();
            } catch (IOException e) {}
            try {
                if (boin != null)
                    boin.close();
            } catch (IOException e) {}
        }
        if ((response != null) || ((responseCode >= 200) && (responseCode < 300))) {
            LQLog.http("HTTP Success " + response);
            return new LQNetworkResponse(responseCode, response);
        }
        return new LQNetworkResponse(responseCode);
    }

    private static String inputStreamToString(final InputStream stream) {
        if(stream == null) {
            return "";
        }
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    new GZIPInputStream(stream)));
            StringBuilder sb = new StringBuilder();
            String line = null;
            while ((line = br.readLine()) != null) {
                sb.append(line + "\n");
            }
            br.close();
            return sb.toString();
        } catch (IOException e) {
            return "";
        }
    }

    public static LQModel fromJSON(JSONObject jsonObject) {
        LQNetworkRequest lqNetworkRequest = null;
        try {
            String mUrl = jsonObject.getString("mUrl");
            String mHttpMethod = jsonObject.getString("mHttpMethod");
            String mJson = jsonObject.getString("mJson");
            lqNetworkRequest = new LQNetworkRequest(mUrl, mHttpMethod, mJson);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return lqNetworkRequest;
    }

    @Override
    public JSONObject toJSON() {
        JSONObject json = new JSONObject();
        try {
            json.put("mUrl", mUrl);
            json.put("mHttpMethod", mHttpMethod);
            json.put("mJson", mJson);
            json.put("mNumberOfTries", mNumberOfTries);
            json.put("mLastTry", mLastTry);
            return json;
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return null;
    }

} public class LQUser extends LQModel {

    private static final long serialVersionUID = 1582937331182018907L;
    private static final String[] RESERVED_KEYS  = {"unique_id", "identified"};

    private String mIdentifier;
    private boolean mIdentified;
    private HashMap<String, Object> mAttributes = new HashMap<String, Object>();

    public LQUser(String identifier) {
        this(identifier, new HashMap<String,Object>());
    }

    public LQUser(String identifier, boolean identified) {
        this(identifier, new HashMap<String,Object>(), identified);
    }

    public LQUser(String identifier, HashMap<String, Object> attributes) {
        this(identifier, attributes, true);
    }

    public LQUser(String identifier, HashMap<String, Object> attributes, boolean identified) {
        mIdentifier = identifier;
        mAttributes = attributes;
        this.setIdentified(identified);
        attributesCheck();

    }

    // Attributes
    public String getIdentifier() {
        return mIdentifier;
    }

    public String setIdentifierForPrefs(String identifier){
        mIdentifier = identifier;
        return mIdentifier;
    }

    public boolean isIdentified() {
        return mIdentified;
    }

    public void setIdentified(boolean mAutoIdentified) {
        this.mIdentified = mAutoIdentified;
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof LQUser) && ( this.toJSON().toString().equals(((LQUser) o).toJSON().toString()));
    }

    public void clearCustomAttributes() {
        setAttributes(new HashMap<String, Object>());
    }

    public HashMap<String, Object> getAttributes() {
        return new HashMap<String, Object>(mAttributes);
    }

    public void setAttributes(HashMap<String, Object> attributes) {
        mAttributes = attributes;
        attributesCheck();
    }

    public Object setAttribute(String key, Object attribute) {
        return mAttributes.put(key, attribute);
    }

    public Object attributeForKey(String key) {
        return mAttributes.get(key);
    }

    // JSON
    public JSONObject toJSON() {
        JSONObject json = new JSONObject();
        try {
            if(mAttributes != null) {
                for(String key : mAttributes.keySet()) {
                    if(mAttributes.get(key) instanceof Date) {
                        json.put(key, LiquidTools.dateToString((Date) mAttributes.get(key)));
                    } else {
                        json.put(key, mAttributes.get(key));
                    }
                }
            }
            json.put("unique_id", mIdentifier);
            json.put("identified", mIdentified);
            return json;
        } catch (JSONException e) {
            LQLog.error("LQUser toJSON: " + e.getMessage());
        }
        return null;
    }



    public static LQModel fromJSON(JSONObject jsonObject) {
        try {
            String unique_id = jsonObject.getString("unique_id");
            boolean identified = jsonObject.getBoolean("identified");
            HashMap<String, Object> attrs = attributesFromJSON(jsonObject, RESERVED_KEYS);
            return new LQUser(unique_id, attrs, identified);
        } catch (JSONException e) {
            LQLog.infoVerbose("New user, identifying now...");
            return new LQUser(LQModel.newIdentifier(), false);
        }
    }

    protected void attributesCheck() {
        if(mAttributes == null) {
            mAttributes = new HashMap<String, Object>();
        }
    }

    @Override
    public void save(Context context, String path) {
        super.save(context, path + ".user");
    }

    public static LQUser load(Context context, String path) {
        return (LQUser) fromJSON(retriveFromFile(context, path));
    }

} public interface InappMessage {

    void show();

    void show(int seconds);
} public class Modal implements InappMessage {

    private LQInAppMessage mModalModel;
    private PopupWindow mPopupWindow;
    private ViewGroup container;
    private View mRoot;
    private TextView mViewMessage;
    private Context mContext;

    public Modal(Context context, View root, LQInAppMessage modalModel) {
        mRoot = root;
        mModalModel = modalModel;
        mContext = context;

        setUpModal();
        setUpButtons();
    }

    public void show() {
        show(0);
    }

    public void show(final int milliseconds){
        mRoot.postDelayed(new Runnable() {
            @Override
            public void run() {
                mPopupWindow.showAtLocation(mRoot, Gravity.CENTER, 0, 0);
            }
        }, milliseconds);
    }

    private void setUpModal() {
        LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        container = (ViewGroup) layoutInflater.inflate(R.layout.activity_modal, null);

        TextView mViewTitle = (TextView) container.findViewById(R.id.inAppMessage);
        mViewMessage = (TextView) container.findViewById(R.id.textView);

        Typeface HeavyLato = Typeface.createFromAsset(mContext.getAssets(), "fonts/Lato-Heavy.ttf");
        Typeface RegularLato = Typeface.createFromAsset(mContext.getAssets(), "fonts/Lato-Regular.ttf");

        // Set font
        mViewTitle.setTypeface(HeavyLato);
        mViewMessage.setTypeface(RegularLato);

        // Set the background Color
        ((GradientDrawable) container.findViewById(R.id.modal_linear_bg_color).getBackground()).
                setColor(Color.parseColor(mModalModel.getBgColor()));

        // Set the text
        mViewTitle.setText(mModalModel.getTitle());
        mViewMessage.setText(mModalModel.getMessage());

        // Set Text Color
        mViewTitle.setTextColor(Color.parseColor(mModalModel.getTitleColor()));
        mViewMessage.setTextColor(Color.parseColor(mModalModel.getMessageColor()));

        mPopupWindow = new PopupWindow(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, true);
        mPopupWindow.setAnimationStyle(R.style.FadeInOutAnimation);
        mPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                Liquid.getInstance().showInAppMessages();
            }
        });
    }

    private void setUpButtons() {
        final Button xClose = (Button) container.findViewById(R.id.xClose);
        xClose.setTransformationMethod(null);

        LinearLayout ctasContainer = (LinearLayout) container.findViewById(R.id.modal_buttons_container);
        LinearLayout.LayoutParams layout_params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        layout_params.setMargins(px(), px(), px(), px());
        ctasContainer.setLayoutParams(layout_params);

        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);

        // Buttons parameters
        params.weight = 1.0f;
        params.width = 0;
        params.height = Math.round(mViewMessage.getTextSize() * 2.66F) ;
        params.setMargins(Math.round(mViewMessage.getTextSize() * 0.66F), Math.round(mViewMessage.getTextSize() * 0.66F), Math.round(mViewMessage.getTextSize() * 0.66F), Math.round(mViewMessage.getTextSize() * 0.66F));

        Typeface boldLato = Typeface.createFromAsset(mContext.getAssets(), "fonts/Lato-Bold.ttf");

        for(final LQInAppMessage.Cta cta : mModalModel.getCtas()) {
            final Button ctaBtn = new Button(mContext);
            ctaBtn.setBackgroundResource(R.drawable.buttonshape);
            ctaBtn.setLayoutParams(params);

            // Set button font
            ctaBtn.setTypeface(boldLato);
            ctaBtn.setTransformationMethod(null);

            // Character Limit
            if(cta.getButtonText().length() > 14)
                ctaBtn.setText(cta.getButtonText().substring(0, 14));
            else
                ctaBtn.setText(cta.getButtonText());

            // Button text size
            ctaBtn.setTextSize(TypedValue.COMPLEX_UNIT_PX, getButtonTextSize());

            // Button background color
            ctaBtn.getBackground().setColorFilter(Color.parseColor(cta.getButtonColor()), PorterDuff.Mode.SRC_IN);

            // Button text color
            ctaBtn.setTextColor(Color.parseColor(cta.getButtonTextColor()));

            if (Build.VERSION.SDK_INT > 15) {
                ctaBtn.setOnTouchListener(new View.OnTouchListener() {
                    Rect mRect;

                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (event.getAction() == MotionEvent.ACTION_DOWN) {
                            mRect = new Rect(ctaBtn.getLeft(), ctaBtn.getTop(), ctaBtn.getRight(), ctaBtn.getBottom());
                            ViewHelper.setAlpha(ctaBtn, 0.7F);
                        }
                        if (event.getAction() == MotionEvent.ACTION_UP) {
                            ViewHelper.setAlpha(ctaBtn, 1);
                        }
                        if (event.getAction() == MotionEvent.ACTION_MOVE &&
                                !mRect.contains(ctaBtn.getLeft() + (int) event.getX(), ctaBtn.getTop() + (int) event.getY())) {
                                ViewHelper.setAlpha(ctaBtn, 1);
                        }
                        return false;
                    }
                });
            }

            ctaBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent mIntent = new Intent(Intent.ACTION_VIEW);
                    if (cta.getDeepLink() != null) {
                        try {
                            mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            mIntent.setData(Uri.parse(cta.getDeepLink()));
                            Liquid.getInstance().trackCta(cta);
                            mContext.startActivity(mIntent);
                        } catch (Exception e) {
                            LQLog.infoVerbose("Canceled or not properly assigned to deeplink or URL");
                        }
                    }
                    mPopupWindow.dismiss();
                }
            });
            ctasContainer.addView(ctaBtn);
        }

        // Change x button color
        xClose.setTextColor(Color.parseColor(mModalModel.getTitleColor()));

        xClose.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Liquid.getInstance().trackDismiss(mModalModel);
                mPopupWindow.dismiss();
            }
        });
    }

    private int px() {
        return Math.round(getRealDimensions() * 0.013F);
    }

    private float getButtonTextSize() {
        return mViewMessage.getTextSize();
    }

    private float getRealDimensions(){
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);

        Display display = wm.getDefaultDisplay();
        int realWidth;
        int realHeight;

        if (Build.VERSION.SDK_INT >= 17){
            // New way to get real metrics
            DisplayMetrics realMetrics = new DisplayMetrics();
            display.getRealMetrics(realMetrics);
            realWidth = realMetrics.widthPixels;
            realHeight = realMetrics.heightPixels;

        } else if (Build.VERSION.SDK_INT >= 14) {
            // Reflection for this weird in-between time
            try {
                Method mGetRawH = Display.class.getMethod("getRawHeight");
                Method mGetRawW = Display.class.getMethod("getRawWidth");
                realWidth = (Integer) mGetRawW.invoke(display);
                realHeight = (Integer) mGetRawH.invoke(display);
            } catch (Exception e) {
                realWidth = display.getWidth();
                realHeight = display.getHeight();
                Log.e("Display Info", "Couldn't use reflection to get the real display metrics.");
            }

        } else {
            // This should be close, as lower API devices should not have window navigation bars
            realWidth = display.getWidth();
            realHeight = display.getHeight();
        }

        if (realHeight < realWidth) {
            realWidth = realHeight;
        }

        if (realWidth == 480)
            realWidth = 700;
        return realWidth;
    }

} public class SlideUp implements OnTouchListener, InappMessage {

    private final SlideUp mInstance;
    private final LQInAppMessage mSlideModel;
    private boolean mNewSDK;
    private Context mContext;
    private View mRoot;
    private ViewGroup container;
    private int height;
    private PopupWindow mPopupWindow;
    private float mDy;
    private int mCurrentX;
    private int mCurrentY;


    public SlideUp(Context context, View root, LQInAppMessage slideModel) {
        mInstance = this;
        mRoot = root;
        mContext = context;
        mSlideModel = slideModel;

        if (Build.VERSION.SDK_INT < 16)
            mNewSDK = false;
        else
            mNewSDK = true;

        setUpSlideUp();
        setUpButton();
    }


    private void setUpSlideUp() {
        LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        container = (ViewGroup) layoutInflater.inflate(R.layout.activity_slide_up, null);
        TextView mViewMessage = (TextView) container.findViewById(R.id.slideUpText);

        Typeface RegularLato = Typeface.createFromAsset(mContext.getAssets(), "fonts/Lato-Regular.ttf");

        // Set the font
        mViewMessage.setTypeface(RegularLato);

        // Set the message
        mViewMessage.setText(mSlideModel.getMessage());

        // Change Background Color
        container.findViewById(R.id.lowest_layout).setBackgroundColor(Color.parseColor(mSlideModel.getBgColor()));

        // Change Text Color
        ((TextView) container.findViewById(R.id.slideUpText)).setTextColor(Color.parseColor(mSlideModel.getMessageColor()));

        // Get View's height depending on device
        final ViewTreeObserver observer = container.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                height = container.getHeight();
            }
        });

        mPopupWindow = new PopupWindow(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);

        mPopupWindow.setFocusable(false);
        mPopupWindow.setOutsideTouchable(true);
        mPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                Liquid.getInstance().showInAppMessages();
            }
        });
    }

    public void setUpButton() {
        final  ImageView mArrowButton = (ImageView) container.findViewById(R.id.slideUpArrowButton);

        for(final LQInAppMessage.Cta cta : mSlideModel.getCtas()) {

            Drawable myArrow = ContextCompat.getDrawable(mContext, R.drawable.arrow);

            // Change arrow button color
            myArrow.setColorFilter(Color.parseColor(cta.getButtonColor()), PorterDuff.Mode.SRC_IN);
            ((ImageView) container.findViewById(R.id.slideUpArrowButton)).setImageDrawable(myArrow);

            mArrowButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent mIntent = new Intent(Intent.ACTION_VIEW);
                    if (cta.getDeepLink() != null) {
                        try {
                            mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            mIntent.setData(Uri.parse(cta.getDeepLink()));
                            Liquid.getInstance().trackCta(cta);
                            mContext.startActivity(mIntent);
                        } catch (Exception e) {
                            LQLog.infoVerbose("Canceled or not properly assigned to deeplink or URL");
                        }
                    }
                    mPopupWindow.dismiss();
                }
            });
        }
    }

    public void show() {
        show(0);
    }

    public void show(int milliseconds){

        mRoot.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mNewSDK)
                    animateNew(0, 0);

                mPopupWindow.setAnimationStyle(R.style.SlideUpAnimation);

                container.setOnTouchListener(mInstance);

                mPopupWindow.showAtLocation(mRoot, Gravity.BOTTOM, 0, 0);
            }
        }, milliseconds);
    }


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mDy = mCurrentY - event.getRawY();
        }
        if (isSlidingDown(event))
            animateDown(event);
        if (isCancelingSlideDown(event)) {
            if (mCurrentY * 1.5F >= height / 2)
                dismiss();
            else
                resetPosition();
        }
        return false;
    }

    private boolean isSlidingDown(MotionEvent event) {
        return event.getAction() == MotionEvent.ACTION_MOVE;
    }

    private boolean isCancelingSlideDown(MotionEvent event) {
        return event.getAction() == MotionEvent.ACTION_UP;
    }

    private void animateDown(MotionEvent event) {
        mCurrentY = (int) (event.getRawY() + mDy);
        if (mCurrentY > 0) {
            setTransparency(1 - (float) mCurrentY * 1 / height); //root equation: 0.5F + (float) mCurrentY * 1 / (2 * height)

            if (mNewSDK)
                animateNew(mCurrentY, 0);
            else {
                animateOld(mCurrentX, -mCurrentY, -1, -1, false);
            }
        }
    }

    private void resetPosition() {
        if (mNewSDK) {
            animateNew(0, 0);
        } else {
            animateOld(0, 0, -1, -1, true);
        }
        mCurrentX = 0;
        mCurrentY = 0;
        setTransparency(1);
    }

    private void setTransparency(float slide) {
        ViewHelper.setAlpha(container, slide);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void animateNew(float yCoordinate, int duration) {
        container.animate().y(yCoordinate * 1.5F).setDuration(duration).start();
    }

    private void animateOld(int xCoordinate, int yCoordinate, int width, int height, boolean moveOutside) {
        int yCoordinateUpdated = (int) (yCoordinate * 1.5F);
        mPopupWindow.update(xCoordinate, yCoordinateUpdated, width, height);
        mPopupWindow.setClippingEnabled(moveOutside);
    }

    private void dismiss() {
        Liquid.getInstance().trackDismiss(mSlideModel);
        mPopupWindow.dismiss();
    }
}
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.lqd.sdk.gcm.LQClientManager;
import io.lqd.sdk.model.LQDataPoint;
import io.lqd.sdk.model.LQDevice;
import io.lqd.sdk.model.LQEvent;
import io.lqd.sdk.model.LQInAppMessage;
import io.lqd.sdk.model.LQLiquidPackage;
import io.lqd.sdk.model.LQModel;
import io.lqd.sdk.model.LQNetworkRequest;
import io.lqd.sdk.model.LQUser;
import io.lqd.sdk.model.LQValue;
import io.lqd.sdk.model.LQVariable;
import io.lqd.sdk.visual.InappMessage;
import io.lqd.sdk.visual.Modal;
import io.lqd.sdk.visual.SlideUp;
/**
 * Copyright 2014-present Liquid Data Intelligence S.A.
 *
 * 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 io.lqd.sdk;





public class Liquid {

    protected static final String TAG_LIQUID = "LIQUID";

    public static final String LIQUID_VERSION = "2.1.1";
    private static final int LIQUID_DEFAULT_SESSION_TIMEOUT = 30;

    private int mSessionTimeout;
    private String mApiToken;
    private LQUser mCurrentUser;
    private LQUser mPreviousUser;
    private LQDevice mDevice;
    protected ExecutorService mQueue;
    private boolean mAutoLoadValues;
    private Context mContext;
    private static Liquid mInstance;
    private LQLiquidPackage mLoadedLiquidPackage;