summaryrefslogtreecommitdiff
path: root/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java')
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java532
1 files changed, 532 insertions, 0 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java
new file mode 100644
index 0000000000..a62fdc98c5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/telemetry/MapboxEventManager.java
@@ -0,0 +1,532 @@
+package com.mapbox.mapboxsdk.telemetry;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+import com.mapbox.mapboxsdk.constants.MapboxConstants;
+import com.mapbox.mapboxsdk.location.LocationService;
+import com.mapbox.mapboxsdk.utils.ApiAccess;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+import java.util.Vector;
+import okhttp3.CertificatePinner;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class MapboxEventManager {
+
+ private static final String TAG = "MapboxEventManager";
+
+ private static MapboxEventManager mapboxEventManager = null;
+
+ private boolean telemetryEnabled;
+
+ private final Vector<Hashtable<String, Object>> events = new Vector<>();
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+ private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
+
+ private Context context = null;
+ private String accessToken = null;
+ private String eventsURL = MapboxEvent.MAPBOX_EVENTS_BASE_URL;
+
+ private String userAgent = MapboxEvent.MGLMapboxEventsUserAgent;
+
+ private Intent batteryStatus = null;
+
+ private DisplayMetrics displayMetrics = null;
+
+ private String mapboxVendorId = null;
+
+ private String mapboxSessionId = null;
+ private long mapboxSessionIdLastSet = 0;
+ private static long hourInMillis = 1000 * 60 * 60;
+ private static long flushDelayInitialInMillis = 1000 * 10; // 10 Seconds
+ private static long flushDelayInMillis = 1000 * 60 * 2; // 2 Minutes
+ private static final int SESSION_ID_ROTATION_HOURS = 24;
+
+ private static MessageDigest messageDigest = null;
+
+ private Timer timer = null;
+
+ private MapboxEventManager(@NonNull Context context) {
+ super();
+ this.accessToken = ApiAccess.getToken(context);
+ this.context = context;
+
+ // Setup Message Digest
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "Error getting Encryption Algorithm: " + e);
+ }
+
+ SharedPreferences prefs = context.getSharedPreferences(MapboxConstants.MAPBOX_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE);
+
+ // Determine if Telemetry Should Be Enabled
+ setTelemetryEnabled(prefs.getBoolean(MapboxConstants.MAPBOX_SHARED_PREFERENCE_KEY_TELEMETRY_ENABLED, true));
+
+ // Load / Create Vendor Id
+ if (prefs.contains(MapboxConstants.MAPBOX_SHARED_PREFERENCE_KEY_VENDORID)) {
+ mapboxVendorId = prefs.getString(MapboxConstants.MAPBOX_SHARED_PREFERENCE_KEY_VENDORID, "Default Value");
+ Log.d(TAG, "Found Vendor Id = " + mapboxVendorId);
+ } else {
+ String vendorId = UUID.randomUUID().toString();
+ vendorId = encodeString(vendorId);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(MapboxConstants.MAPBOX_SHARED_PREFERENCE_KEY_VENDORID, vendorId);
+ editor.apply();
+ editor.commit();
+ Log.d(TAG, "Set New Vendor Id = " + vendorId);
+ }
+
+ // Create Initial Session Id
+ rotateSessionId();
+
+ // Get DisplayMetrics Setup
+ displayMetrics = new DisplayMetrics();
+ ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(displayMetrics);
+
+ // Check for Staging Server Information
+ try {
+ ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ String stagingURL = appInfo.metaData.getString(MapboxConstants.KEY_META_DATA_STAGING_SERVER);
+ String stagingAccessToken = appInfo.metaData.getString(MapboxConstants.KEY_META_DATA_STAGING_ACCESS_TOKEN);
+ String appName = context.getPackageManager().getApplicationLabel(appInfo).toString();
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ String versionName = packageInfo.versionName;
+ int versionCode = packageInfo.versionCode;
+
+ if (!TextUtils.isEmpty(stagingURL)) {
+ eventsURL = stagingURL;
+ }
+
+ if (!TextUtils.isEmpty(stagingAccessToken)) {
+ this.accessToken = stagingAccessToken;
+ }
+
+ // Build User Agent
+ if (!TextUtils.isEmpty(appName) && !TextUtils.isEmpty(versionName)) {
+ userAgent = appName + "/" + versionName + "/" + versionCode + " " + userAgent;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error Trying to load Staging Credentials: " + e.toString());
+ }
+
+ // Register for battery updates
+ IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ batteryStatus = context.registerReceiver(null, iFilter);
+ }
+
+ /**
+ * Primary Access method using Singleton pattern
+ * @param context Application Context
+ * @return MapboxEventManager
+ */
+ public static MapboxEventManager getMapboxEventManager(@NonNull Context context) {
+ if (mapboxEventManager == null) {
+ mapboxEventManager = new MapboxEventManager(context.getApplicationContext());
+ }
+ return mapboxEventManager;
+ }
+
+ public boolean isTelemetryEnabled() {
+ return telemetryEnabled;
+ }
+
+ /**
+ * Enables / Disables Telemetry
+ * @param telemetryEnabled True to start telemetry, false to stop it
+ */
+ public void setTelemetryEnabled(boolean telemetryEnabled) {
+ if (this.telemetryEnabled == telemetryEnabled) {
+ Log.i(TAG, "no need to start / stop telemetry as it's already in that state.");
+ return;
+ }
+
+ if (telemetryEnabled) {
+ Log.i(TAG, "Starting Telemetry Up!");
+ // Start It Up
+ context.startService(new Intent(context, TelemetryService.class));
+
+ // Make sure Ambient Mode is started at a minimum
+ if (LocationService.getInstance(context).isGPSEnabled()) {
+ LocationService.getInstance(context).toggleGPS(false);
+ }
+
+ // Manage Timer Flush
+ timer = new Timer();
+ timer.schedule(new FlushEventsTimerTask(), flushDelayInitialInMillis, flushDelayInMillis);
+ } else {
+ Log.i(TAG, "Shutting Telemetry Down");
+ // Shut It Down
+ events.removeAllElements();
+ context.stopService(new Intent(context, TelemetryService.class));
+
+ if (timer != null) {
+ timer.cancel();
+ timer = null;
+ }
+ }
+
+ // Persist
+ this.telemetryEnabled = telemetryEnabled;
+ SharedPreferences prefs = context.getSharedPreferences(MapboxConstants.MAPBOX_SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(MapboxConstants.MAPBOX_SHARED_PREFERENCE_KEY_TELEMETRY_ENABLED, telemetryEnabled);
+ editor.apply();
+ editor.commit();
+ }
+
+ /**
+ * Adds a Location Event to the system for processing
+ * @param location Location event
+ */
+ public void addLocationEvent(Location location) {
+ // Add Location even to queue
+ Hashtable<String, Object> event = new Hashtable<>();
+ event.put(MapboxEvent.KEY_LATITUDE, location.getLatitude());
+ event.put(MapboxEvent.KEY_LONGITUDE, location.getLongitude());
+ event.put(MapboxEvent.KEY_SPEED, location.getSpeed());
+ event.put(MapboxEvent.KEY_COURSE, location.getBearing());
+ event.put(MapboxEvent.KEY_ALTITUDE, location.getAltitude());
+ event.put(MapboxEvent.KEY_HORIZONTAL_ACCURACY, location.getAccuracy());
+ event.put(MapboxEvent.ATTRIBUTE_CREATED, dateFormat.format(new Date()));
+ event.put(MapboxEvent.ATTRIBUTE_EVENT, MapboxEvent.TYPE_LOCATION);
+
+ events.add(event);
+
+ rotateSessionId();
+ }
+
+ /**
+ * Push Interactive Events to the system for processing
+ * @param eventWithAttributes Event with attributes
+ */
+ public void pushEvent(Hashtable<String, Object> eventWithAttributes) {
+
+ if (eventWithAttributes == null) {
+ return;
+ }
+
+ String eventType = (String)eventWithAttributes.get(MapboxEvent.ATTRIBUTE_EVENT);
+ if (!TextUtils.isEmpty(eventType) && eventType.equalsIgnoreCase(MapboxEvent.TYPE_MAP_LOAD)) {
+ pushTurnstileEvent();
+ }
+
+ events.add(eventWithAttributes);
+ }
+
+ /**
+ * Pushes turnstile event for internal billing purposes
+ */
+ private void pushTurnstileEvent() {
+
+ Hashtable<String, Object> event = new Hashtable<>();
+ event.put(MapboxEvent.ATTRIBUTE_EVENT, MapboxEvent.TYPE_TURNSTILE);
+ event.put(MapboxEvent.ATTRIBUTE_CREATED, dateFormat.format(new Date()));
+/*
+ // Already set by processing
+ event.put(MapboxEvent.ATTRIBUTE_APP_BUNDLE_ID, context.getPackageName());
+ event.put(MapboxEvent.ATTRIBUTE_VERSION, MapboxEvent.VERSION_NUMBER);
+ event.put(MapboxEvent.ATTRIBUTE_VENDOR_ID, mapboxVendorId);
+*/
+
+ events.add(event);
+
+ // Send to Server Immediately
+ new FlushTheEventsTask().execute();
+ Log.d(TAG, "turnstile event pushed.");
+ }
+
+ /**
+ * SHA-1 Encoding for strings
+ * @param string String to encode
+ * @return String encoded if no error, original string if error
+ */
+ private String encodeString(String string) {
+ try {
+ if (messageDigest != null) {
+ messageDigest.reset();
+ messageDigest.update(string.getBytes("UTF-8"));
+ byte[] bytes = messageDigest.digest();
+
+ // Get the Hex version of the digest
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append( String.format("%02X", b) );
+ }
+ String hex = sb.toString();
+ Log.d(TAG, "original = " + string + "; hex = " + hex);
+
+ return hex;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Error encoding string, will return in original form." + e);
+ }
+ return string;
+ }
+
+ /**
+ * Changes Session Id based on time boundary
+ */
+ private void rotateSessionId() {
+ long now = System.currentTimeMillis();
+ if (now - mapboxSessionIdLastSet > (SESSION_ID_ROTATION_HOURS * hourInMillis)) {
+ mapboxSessionId = UUID.randomUUID().toString();
+ mapboxSessionIdLastSet = System.currentTimeMillis();
+ }
+ }
+
+ private String getOrientation() {
+ switch (context.getResources().getConfiguration().orientation) {
+ case Configuration.ORIENTATION_LANDSCAPE:
+ return "Landscape";
+ case Configuration.ORIENTATION_PORTRAIT:
+ return "Portrait";
+ default:
+ return "Undefined";
+ }
+ }
+
+ private int getBatteryLevel() {
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+
+ return Math.round((level / (float)scale) * 100);
+ }
+
+ private String getApplicationState() {
+
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
+ if (appProcesses == null) {
+ return "Unknown";
+ }
+ final String packageName = context.getPackageName();
+ for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
+ if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) {
+ return "Foreground";
+ }
+ }
+ return "Background";
+ }
+
+ private float getAccesibilityFontScaleSize() {
+ // Values
+ // Small = 0.85
+ // Normal = 1.0
+ // Large = 1.15
+ // Huge = 1.3
+
+ return context.getResources().getConfiguration().fontScale;
+ }
+
+ private String getCellularCarrier() {
+ TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
+ String carrierName = manager.getNetworkOperatorName();
+ if (TextUtils.isEmpty(carrierName)) {
+ carrierName = "None";
+ }
+ return carrierName;
+ }
+
+ private String getCellularNetworkType () {
+ TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
+ switch (manager.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ return "1xRTT";
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ return "CDMA";
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ return "EDGE";
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ return "EHRPD";
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ return "EVDO_0";
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ return "EVDO_A";
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ return "EVDO_B";
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ return "GPRS";
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ return "HSDPA";
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ return "HSPA";
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return "HSPAP";
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ return "HSUPA";
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return "IDEN";
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return "LTE";
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ return "UMTS";
+ case TelephonyManager.NETWORK_TYPE_UNKNOWN:
+ return "Unknown";
+ default:
+ return "Default Unknown";
+ }
+ }
+
+
+ public String getConnectedToWifi() {
+
+ String status = "No";
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ if (wifiMgr.isWifiEnabled()) {
+ try {
+ WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
+ if( wifiInfo.getNetworkId() != -1 ){
+ status = "Yes";
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Error getting Wifi Connection Status: " + e);
+ status = "Unknown";
+ }
+ }
+
+ return status;
+ }
+
+ /**
+ * Task responsible for converting stored events and sending them to the server
+ */
+ private class FlushTheEventsTask extends AsyncTask<Void, Void, Void> {
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+
+ if (events.size() < 1) {
+ Log.i(TAG, "No events in the queue to send so returning.");
+ return null;
+ }
+
+ // Check for NetworkConnectivity
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ if (networkInfo == null || !networkInfo.isConnected()) {
+ Log.w(TAG, "Not connected to network, so returning without attempting to send events");
+ return null;
+ }
+
+ try {
+ // Send data
+ // =========
+ JSONArray jsonArray = new JSONArray();
+
+ for (Hashtable<String, Object> evt : events) {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(MapboxEvent.KEY_LATITUDE, evt.get(MapboxEvent.KEY_LATITUDE));
+ jsonObject.put(MapboxEvent.KEY_LONGITUDE, evt.get(MapboxEvent.KEY_LONGITUDE));
+ jsonObject.put(MapboxEvent.KEY_SPEED, evt.get(MapboxEvent.KEY_SPEED));
+ jsonObject.put(MapboxEvent.KEY_COURSE, evt.get(MapboxEvent.KEY_COURSE));
+ jsonObject.put(MapboxEvent.KEY_ALTITUDE, evt.get(MapboxEvent.KEY_ALTITUDE));
+ jsonObject.put(MapboxEvent.KEY_HORIZONTAL_ACCURACY, evt.get(MapboxEvent.KEY_HORIZONTAL_ACCURACY));
+ jsonObject.put(MapboxEvent.KEY_ZOOM, evt.get(MapboxEvent.KEY_ZOOM));
+
+ // Basic Event Meta Data
+ jsonObject.put(MapboxEvent.ATTRIBUTE_EVENT, evt.get(MapboxEvent.ATTRIBUTE_EVENT));
+ jsonObject.put(MapboxEvent.ATTRIBUTE_CREATED, evt.get(MapboxEvent.ATTRIBUTE_CREATED));
+ jsonObject.put(MapboxEvent.ATTRIBUTE_SESSION_ID, encodeString(mapboxSessionId));
+ jsonObject.put(MapboxEvent.ATTRIBUTE_VERSION, MapboxEvent.VERSION_NUMBER);
+ jsonObject.put(MapboxEvent.ATTRIBUTE_VENDOR_ID, mapboxVendorId);
+ jsonObject.put(MapboxEvent.ATTRIBUTE_APP_BUNDLE_ID, context.getPackageName());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_MODEL, Build.MODEL);
+ jsonObject.put(MapboxEvent.ATTRIBUTE_OPERATING_SYSTEM, Build.VERSION.RELEASE);
+ jsonObject.put(MapboxEvent.ATTRIBUTE_ORIENTATION, getOrientation());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_BATTERY_LEVEL, getBatteryLevel());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_APPLICATION_STATE, getApplicationState());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_RESOLUTION, displayMetrics.density);
+ jsonObject.put(MapboxEvent.ATTRIBUTE_ACCESSIBILITY_FONT_SCALE, getAccesibilityFontScaleSize());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_CARRIER, getCellularCarrier());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_CELLULAR_NETWORK_TYPE, getCellularNetworkType());
+ jsonObject.put(MapboxEvent.ATTRIBUTE_WIFI, getConnectedToWifi());
+
+ jsonArray.put(jsonObject);
+ }
+
+ // Based on http://square.github.io/okhttp/3.x/okhttp/okhttp3/CertificatePinner.html
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("cloudfront-staging.tilestream.net", "sha1/KcdiTca54HxWTV8VuAd67x8I=")
+ .add("cloudfront-staging.tilestream.net", "sha1//KDE76PP0DQBDcTnMFBv+efp4eg=")
+ .add("api.mapbox.com", "sha1/Uv71ooi32pyba+oLD7egnXm7/GQ=")
+ .add("api.mapbox.com", "sha1/hOP0d37/ZTSGgCSseE3DIZ1uSg0=")
+ .build();
+
+ OkHttpClient client = new OkHttpClient.Builder().certificatePinner(certificatePinner).build();
+ RequestBody body = RequestBody.create(JSON, jsonArray.toString());
+
+ String url = eventsURL + "/events/v1?access_token=" + accessToken;
+ Log.d(TAG, "url = " + url);
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header("User-Agent", userAgent)
+ .post(body)
+ .build();
+ Response response = client.newCall(request).execute();
+ Log.d(TAG, "Response Code from Mapbox Events Server: " + response.code() + " for " + events.size() + " events sent in.");
+
+ // Reset Events
+ // ============
+ events.removeAllElements();
+ } catch (Exception e) {
+ Log.e(TAG, "FlushTheEventsTask borked: " + e);
+ }
+
+ return null;
+ }
+
+ }
+
+
+ /**
+ * TimerTask responsible for sending event data to server
+ */
+ private class FlushEventsTimerTask extends TimerTask {
+ /**
+ * The task to run should be specified in the implementation of the {@code run()}
+ * method.
+ */
+ @Override
+ public void run() {
+ new FlushTheEventsTask().execute();
+ }
+ }
+}