package com.malcom.library.android.module.stats; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.CountDownTimer; import android.util.Log; import com.malcom.library.android.module.core.MCMCoreAdapter; import com.malcom.library.android.module.stats.Subbeacon.SubbeaconType; import com.malcom.library.android.module.stats.services.PendingBeaconsDeliveryService; import com.malcom.library.android.utils.LocationUtils; import com.malcom.library.android.utils.ToolBox; import com.malcom.library.android.utils.encoding.DigestUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.Thread.UncaughtExceptionHandler; import java.util.*; /** * Stats module. * * @author Malcom Ventures, S.L. * @since 2012 * */ public class MCMStats { private static MCMStats mBeacon; private static boolean uncaughtExceptionHandlerInitialized = false; private static final UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); public static final String TAG = "MCMStats"; public static final String CACHED_BEACON_FILE_PREFIX = "beacon_"; private static Context mContext; private static Properties properties; private static double mStartTime; private static double mEndTime; private static Hashtable<String, Subbeacon> mSubbeaconsDictionary; private static ArrayList<Subbeacon> mSubbeaconsArray; private static List<String> tags; private static boolean mUseLocation; private static boolean appCrashed; private static CountDownTimer waitTimer; private MCMStats(Context context, Properties properties, boolean useLocation, List<String> tags) { MCMStats.mContext = context; MCMStats.properties = properties; MCMStats.mSubbeaconsArray = new ArrayList<Subbeacon>(); MCMStats.mSubbeaconsDictionary = new Hashtable<String, Subbeacon>(); MCMStats.tags = tags; setUseLocation(useLocation); } // Patch to fix a bug: https://github.com/MyMalcom/malcom-lib-android/issues/30 // TODO: This class should be refactored anyway public static void initContext(Context context) { mContext = context; } // GETTERS & SETTERS private void setUseLocation(boolean useLocation) { MCMStats.mUseLocation = useLocation; } // BEACONS --------------------------------------------------------------------------------- /** * Initialize and starts the stats service. * * @param context * @param properties * @param uselocation */ public synchronized static void initAndStartBeacon(final Context context, Properties properties, boolean uselocation) { if(mBeacon!=null){ Log.i(TAG, "Beacon was already started."); }else{ Log.i(TAG, "Starting beacon..."); mBeacon = new MCMStats(context, properties, uselocation, tags); mBeacon.startBeacon(); } if(!uncaughtExceptionHandlerInitialized){ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { onCrash(); //This way much better than System.exit(). //android.os.Process.killProcess(android.os.Process.myPid()); //call original handler defaultUncaughtExceptionHandler.uncaughtException(t, e); } }); } } /** * Stops the beacon, collecting data and sending the information to * Malcom server. */ public synchronized static void stopBeacon() { Log.i(TAG, "Stopping beacon..."); mEndTime = BeaconUtils.timeIntervalSince1970(new Date()); String beaconData = getJSON(); try { if(beaconData!=null){ cacheBeacon(beaconData); } } catch (Exception e) { Log.i(TAG, "Exception sending message to the Malcom beacon queue = " + e.getMessage()); Log.e(TAG, "Detailed error trace: ",e); cacheBeacon(beaconData); } finally { MCMStats.mBeacon = null; Intent svcPendingBeacons = new Intent(mContext,PendingBeaconsDeliveryService.class); mContext.startService(svcPendingBeacons); } } /** * Get the beacon service instance. * * @return * @throws MCMStats.BeaconException */ public static synchronized MCMStats getSharedInstance() throws MCMStats.BeaconException { System.out.println("mBeacon: " + mBeacon); if (mBeacon != null) { return mBeacon; } else { throw new BeaconException("Did you call initAndStartBeacon() before?"); } } // BEACONS - AUXILIAR FUNCTIONS private synchronized void startBeacon() { Date date = new Date(); mStartTime = BeaconUtils.timeIntervalSince1970(date); } /* * When a crash occurs, we set the "crash" flag before beacons is sent. * */ private static void onCrash(){ Log.i(TAG, "Stopping beacon after application crash..."); appCrashed = true; stopBeacon(); Log.i(TAG, "Stopping beacon after application crash done."); } /* * Saves the beacon in the internal memory of the device in the private * application folder. */ private static synchronized void cacheBeacon(String beaconData){ //Save the beacon for later send try { String name = CACHED_BEACON_FILE_PREFIX +DigestUtils.md5Hex(beaconData.getBytes()); ToolBox.storage_storeDataInInternalStorage(mContext, name, beaconData.getBytes()); } catch (Exception e) { Log.e(TAG,"Error saving the beacon for later delivery ("+e.getMessage()+")",e); } } private static String getJSON() { String res = null; JSONObject beaconJson = new JSONObject(); try { JSONObject beaconContentJson = new JSONObject(); beaconContentJson.put("application_code", MCMCoreAdapter.getInstance().coreGetProperty(MCMCoreAdapter.PROPERTIES_MALCOM_APPID)); beaconContentJson.put("lib_version", MCMCoreAdapter.getInstance().SDKVersion()); beaconContentJson.put("udid", ToolBox.device_getId(mContext)); beaconContentJson.put("device_model", BeaconUtils.getDeviceModel()); beaconContentJson.put("device_os", BeaconUtils.getDeviceOs()); beaconContentJson.put("app_version", BeaconUtils.getApplicationVersion(mContext)); beaconContentJson.put("language", BeaconUtils.getDeviceIsoLanguage()); beaconContentJson.put("device_platform", BeaconUtils.getDevicePlatform()); beaconContentJson.put("time_zone", BeaconUtils.getDeviceTimeZone()); beaconContentJson.put("country", BeaconUtils.getDeviceIsoCountry()); beaconContentJson.put("tags", (getTagsAsJsonArray())); beaconContentJson.put("city", mUseLocation? LocationUtils.getDeviceCityLocation(mContext):""); beaconContentJson.put("started_on", mStartTime); beaconContentJson.put("stopped_on", mEndTime); beaconContentJson.put("location", LocationUtils.getLocationJson(mContext)); beaconContentJson.put("subbeacons", getSubbeaconsJsonArray()); beaconContentJson.put("user_metadata", getUserMetadata()); if(appCrashed){ beaconContentJson.put("crash", true); } beaconJson.put("beacon", beaconContentJson); Log.i(TAG, "JSON = \n" + beaconJson.toString()); res = beaconJson.toString(); } catch (Exception e) { Log.i(TAG, "Exception generating JSON beacon = " + e.getMessage()); } return res; } private static JSONArray getTagsAsJsonArray() { List<String> listTags = getTags(); Log.d(TAG, "Tags: "+ listTags.toString()); return new JSONArray(listTags); } // SUB-BEACONS -------------------------------------------------------------------------------- /** * Starts the sub-beacon (an event) with the specified name. If track-session * is enabled, time will be saved with the event. * * @param beaconName SubBeacon name. * @param trackSession Tells if track the session */ public void startSubBeaconWithName(String beaconName, boolean trackSession) { Subbeacon subbeacon; try { Log.i(TAG, "Starting subbeacon..."); subbeacon = new Subbeacon(beaconName); if (trackSession) { subbeacon.setStartedOn(new Date().getTime()); mSubbeaconsDictionary.put(beaconName, subbeacon); } mSubbeaconsArray.add(subbeacon); } catch (JSONException e) { Log.e(TAG, "Error starting sub-beacon: "+e.getMessage(),e); } } /** * Stops the specified sub-beacon. * * @param beaconName SubBeacon name. */ public void endSubBeaconWithName(String beaconName) { Subbeacon subbeacon = mSubbeaconsDictionary.get(beaconName); if (subbeacon != null) { subbeacon.setStoppedOn(new Date().getTime()); } } /** * Starts the sub-beacon (an event) with the specified name. If track-session * is enabled, time will be saved with the event. * * @param beaconName SubBeacon name. * @param type SubBeacon type, it could be: CUSTOM, SPECIAL, ERROR... * @param trackSession Tells if track the session */ public void startSubBeaconWithName(String beaconName, SubbeaconType type, Hashtable<String, Object> params, boolean trackSession) { Subbeacon subbeacon; try { Log.i(TAG, "Starting subbeacon..."); subbeacon = new Subbeacon(beaconName, type, params); subbeacon.setStartedOn(new Date().getTime()); if (trackSession) { //We add the subbeacon to dictionary in order to search it later and stablish the end time mSubbeaconsDictionary.put(beaconName, subbeacon); }else{ //If not track session means that is a unique event without time subbeacon.setStoppedOn(new Date().getTime()); } mSubbeaconsArray.add(subbeacon); } catch (JSONException e) { Log.e(TAG, "Error starting sub-beacon: "+e.getMessage(),e); } } /** * Stops the specified sub-beacon. * * @param beaconName SubBeacon name. */ public void endSubBeaconWithName(String beaconName, Hashtable<String, Object> params) { Subbeacon subbeacon = mSubbeaconsDictionary.get(beaconName); //Check if the new params are in the start one Enumeration<String> enumeration = params.keys(); //Update the param array while (enumeration.hasMoreElements()) { String key = enumeration.nextElement(); subbeacon.getParams().put(key, params.get(enumeration.nextElement())); } if (subbeacon != null) { subbeacon.setStoppedOn(new Date().getTime()); } } // SUB-BEACONS Auxiliar functions private static JSONArray getSubbeaconsJsonArray() { ArrayList<Subbeacon> subbeaconsJSON = new ArrayList<Subbeacon>(); for (Subbeacon sub : mSubbeaconsArray) { //We automatically close any unclosed sub-beacons. if(sub.getStoppedOn()==0){ Subbeacon subbeacon = mSubbeaconsDictionary.get(sub.getName()); if (subbeacon != null) { Log.w(TAG, "You should close the subbeacon with name: "+subbeacon.getName()); // subbeacon.setStoppedOn(new Date().getTime()); subbeacon.setStoppedOn(subbeacon.getStartedOn()); } } subbeaconsJSON.add(sub); } return new JSONArray(subbeaconsJSON); } // TAGS public static List<String> getTags() { SharedPreferences preferences = mContext.getSharedPreferences("tags", Context.MODE_PRIVATE); List<String> tags = new ArrayList<String>((Collection<? extends String>) preferences.getAll().values()); return tags; } public static void addTag(String tag) { SharedPreferences preferences = mContext.getSharedPreferences("tags", Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString(tag,tag); editor.commit(); } public static void removeTag(String tag) { SharedPreferences preferences = mContext.getSharedPreferences("tags", Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.remove(tag); editor.commit(); } public void setUserMetadata(String userMetadata) { SharedPreferences preferences = mContext.getSharedPreferences("mcm_user_metadata", Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString("user_metadata", userMetadata); editor.commit(); } public static String getUserMetadata() { SharedPreferences preferences = mContext.getSharedPreferences("mcm_user_metadata", Context.MODE_PRIVATE); return (String) preferences.getAll().get("user_metadata"); } // UTILITY CLASSES ********************************************************* // BeaconException public static class BeaconException extends Exception { private static final long serialVersionUID = 2115918307306918270L; public BeaconException(String detailMessage) { super(detailMessage); } } /* * This class is used to send beacon to malcom * * We run this process in a separate thread to android > 4.0 * * @author Malcom Ventures S.L * @since 2012 * */ private static class SendBeaconToMalcom extends Thread implements Runnable{ private String beaconToSend; public SendBeaconToMalcom(String beacon) { this.beaconToSend = beacon; } @Override public void run() { try { StatsUtils.sendBeaconToMalcom(this.beaconToSend); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }