diff options
Diffstat (limited to 'src')
4 files changed, 413 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/conversation/ConversationActivity.java b/src/com/android/messaging/ui/conversation/ConversationActivity.java index 66310ea..20f857d 100644 --- a/src/com/android/messaging/ui/conversation/ConversationActivity.java +++ b/src/com/android/messaging/ui/conversation/ConversationActivity.java @@ -16,14 +16,20 @@ package com.android.messaging.ui.conversation; +import android.app.AlarmManager; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.text.TextUtils; +import android.util.Log; import android.view.MenuItem; import com.android.messaging.R; @@ -42,9 +48,13 @@ import com.android.messaging.util.LogUtil; import com.android.messaging.util.OsUtil; import com.android.messaging.util.UiUtils; +import com.cyanogenmod.messaging.util.MetricsJob; + public class ConversationActivity extends BugleActionBarActivity implements ContactPickerFragmentHost, ConversationFragmentHost, ConversationActivityUiStateHost { + public static final String TAG = "ConversationActivity"; + public static final int FINISH_RESULT_CODE = 1; private static final String SAVED_INSTANCE_STATE_UI_STATE_KEY = "uistate"; @@ -117,6 +127,39 @@ public class ConversationActivity extends BugleActionBarActivity UIIntents.get().launchFullScreenVideoViewer(this, Uri.parse(extraToDisplay)); } } + + // Schedule a Job for Metrics Service + JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); + + if (jobScheduler != null) { + boolean jobExists = false; + for (JobInfo ji : jobScheduler.getAllPendingJobs()) { + if (ji.getId() != MetricsJob.METRICS_JOB_ID) { + // Job exists + jobExists = true; + break; + } + } + if (!jobExists) { + // We need a job to send our aggregated events to our metrics service every 24 hours. + // As long as this service has been used, we know we'll need this data. + ComponentName jobComponent = new ComponentName(this, MetricsJob.class); + + JobInfo job = new JobInfo.Builder(MetricsJob.METRICS_JOB_ID, jobComponent) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setPersisted(true) + .setPeriodic(AlarmManager.INTERVAL_DAY) + .setBackoffCriteria(AlarmManager.INTERVAL_FIFTEEN_MINUTES, + JobInfo.BACKOFF_POLICY_EXPONENTIAL) + .build(); + jobScheduler.schedule(job); + } else { + Log.v(TAG, "Messaging Metrics job " + MetricsJob.METRICS_JOB_ID + " already exists"); + } + } else { + Log.e(TAG, "Running on a device without JobScheduler." + + " Messaging Metrics will fail to collect."); + } } @Override diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageView.java b/src/com/android/messaging/ui/conversation/ConversationMessageView.java index 94a8fe0..bf82983 100644 --- a/src/com/android/messaging/ui/conversation/ConversationMessageView.java +++ b/src/com/android/messaging/ui/conversation/ConversationMessageView.java @@ -17,6 +17,7 @@ package com.android.messaging.ui.conversation; import android.content.Context; +import android.content.ComponentName; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; @@ -83,6 +84,7 @@ import com.cyanogen.lookup.phonenumber.response.LookupResponse; import com.cyanogenmod.messaging.lookup.LookupProviderManager.LookupProviderListener; import com.cyanogenmod.messaging.ui.AttributionContactIconView; import com.cyanogenmod.messaging.util.GoogleStaticMapsUtil; +import com.cyanogenmod.messaging.util.MetricsHelper; import com.cyanogenmod.messaging.util.RidesharingUtil; import com.cyanogenmod.messaging.util.RoundedCornerTransformation; import com.google.common.base.Predicate; @@ -752,6 +754,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick builder.addDropoffLocation(decodedAddress); Intent intent = builder.build(); mContext.startActivity(intent); + + MetricsHelper.increaseCountOfEventMetricAfterValidate(mContext.getApplicationContext(), new ComponentName(mContext.getApplicationContext(), ConversationActivity.class), + MetricsHelper.MetricEvent.UBER_RIDE_REQUESTED); } } }); @@ -765,6 +770,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick mButtonDivider.measure(dividerWidthMeasureSpec, unspecifiedMeasureSpec); mMessageMapsView.setVisibility(View.VISIBLE); + + MetricsHelper.increaseCountOfEventMetricAfterValidate(mContext.getApplicationContext(), new ComponentName(mContext.getApplicationContext(), ConversationActivity.class), + MetricsHelper.MetricEvent.RIDESHARING_MAP_SHOWN); } private class ImageLoadedCallback implements com.squareup.picasso.Callback { diff --git a/src/com/cyanogenmod/messaging/util/MetricsHelper.java b/src/com/cyanogenmod/messaging/util/MetricsHelper.java new file mode 100644 index 0000000..aff2e32 --- /dev/null +++ b/src/com/cyanogenmod/messaging/util/MetricsHelper.java @@ -0,0 +1,223 @@ +/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*====* + * Copyright (c) 2016 Cyanogen Inc. + * All Rights Reserved. + * Cyanogen Confidential and Proprietary. + * =========================================================================*/ + +package com.cyanogenmod.messaging.util; + +import android.content.SharedPreferences; +import android.util.Log; + +//import com.android.internal.annotations.VisibleForTesting; +import com.cyanogen.ambient.analytics.Event; +import android.content.ComponentName; +import android.content.Context; + +import java.util.HashMap; +import java.util.Map; + +public final class MetricsHelper { + + private static final String TAG = "MetricsHelper"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final String METRICS_SHARED_PREFERENCES = "messaging_metrics"; + public static final String METRICS_VALIDATION = "messaging_metrics_validation"; + public static final String DELIMIT = ":"; + private static final String CATEGORY_BASE = "messaging.metrics."; + + // Positions in our shared preference keys + private static final int POS_COMPONENT_NAME = 0; + private static final int POS_CATEGORY_VALUE = 1; + private static final int POS_EVENT_VALUE = 2; + private static final int POS_PARAM_VALUE = 3; + + public static final String CATEGORY_MESSAGING_RIDESHARING_INTEGRATION = "category_messaging_ridesharing_integration"; + public static final String EVENT_RIDESHARING_MAP_SHOWN = "event_ridesharing_map_shown"; + public static final String EVENT_UBER_RIDE_REQUESTED = "event_uber_ride_requested"; + public static final String PARAM_COUNT = "param_count"; + public static final String PARAM_PROVIDER = "provider"; + + public static enum MetricEvent { + RIDESHARING_MAP_SHOWN, + UBER_RIDE_REQUESTED; + } + + //@VisibleForTesting + /* package */ static void storeEvent(Context context, ComponentName cn, + HashMap<String, String> data, String category, String event) { + + SharedPreferences sp = context.getSharedPreferences( + METRICS_SHARED_PREFERENCES, Context.MODE_PRIVATE); + + SharedPreferences.Editor editor = sp.edit(); + + for (String param : data.keySet()) { + StringBuilder sb = new StringBuilder(); + sb.append(cn.flattenToShortString()); // Add ComponentName String + sb.append(DELIMIT); + sb.append(category); // Add our category value + sb.append(DELIMIT); + sb.append(event); // Add our event value + sb.append(DELIMIT); + sb.append(param); // add our param value + editor.putString(sb.toString(), data.get(param)); + } + editor.apply(); + } + + /** + * Get the sharedpreferences events and output a hashmap for the event's values. + * + * @param context the current context + * @param componentName ComponentName who created the event + * + * @return HashMap of our params and their values. + */ + /* package*/ static HashMap<String, String> getStoredEventParams(Context context, + ComponentName componentName, String category, String event) { + + SharedPreferences sp = context.getSharedPreferences( + METRICS_SHARED_PREFERENCES, Context.MODE_PRIVATE); + + StringBuilder sb = new StringBuilder(); + sb.append(componentName.flattenToShortString()); // Add ComponentName String + sb.append(DELIMIT); + sb.append(category); // Add our category value + sb.append(DELIMIT); + sb.append(event); // Add our event value + sb.append(DELIMIT); + + HashMap<String, String> eventMap = new HashMap<>(); + Map<String, ?> map = sp.getAll(); + + for(Map.Entry<String,?> entry : map.entrySet()) { + if (entry.getKey().startsWith(sb.toString())) { + String[] keyParts = entry.getKey().split(DELIMIT); + String key = keyParts[POS_PARAM_VALUE]; + eventMap.put(key, String.valueOf(entry.getValue())); + } + } + return eventMap; + } + + /** + * Helper method to increase the count of event metric if the last action was not + * the same as the current action. + * + * @param context + */ + public static void increaseCountOfEventMetricAfterValidate(Context context, ComponentName componentName, + MetricEvent metricEvent) { + + StringBuilder sb = new StringBuilder(); + sb.append(componentName.flattenToShortString()); // Add ComponentName String + sb.append(DELIMIT); + sb.append(CATEGORY_MESSAGING_RIDESHARING_INTEGRATION); // Add our category value + + String validationKey = sb.toString(); + String event; + switch (metricEvent) { + case UBER_RIDE_REQUESTED: + event = EVENT_UBER_RIDE_REQUESTED; + break; + case RIDESHARING_MAP_SHOWN: + default: + event = EVENT_RIDESHARING_MAP_SHOWN; + break; + } + + if (checkLastEvent(context, validationKey, event)) { + HashMap<String, String> metricsData + = getStoredEventParams(context, componentName, CATEGORY_MESSAGING_RIDESHARING_INTEGRATION, + event); + + int count = 1; + if (metricsData.containsKey(PARAM_COUNT)) { + count += Integer.valueOf(metricsData.get(PARAM_COUNT)); + } + + metricsData.put(PARAM_COUNT, String.valueOf(count)); + storeEvent(context, componentName, metricsData, CATEGORY_MESSAGING_RIDESHARING_INTEGRATION, event); + } + } + + /* package */ static boolean checkLastEvent(Context context, String validationKey, String event) { + SharedPreferences preferences = context.getSharedPreferences(METRICS_VALIDATION, + Context.MODE_PRIVATE); + + SharedPreferences.Editor editor = preferences.edit(); + String lastEvent = preferences.getString(validationKey, null); + + if (lastEvent != null && lastEvent.equals(event)) { + return false; + } else { + editor.putString(validationKey, event); + } + + editor.apply(); + return true; + } + + + /** + * Prepares all our metrics for sending. + */ + public static HashMap<String, Event.Builder> getEventsToSend(Context c) { + SharedPreferences sp = c.getSharedPreferences(METRICS_SHARED_PREFERENCES, + Context.MODE_PRIVATE); + + Map<String, ?> map = sp.getAll(); + + HashMap<String, Event.Builder> unBuiltEvents = new HashMap<>(); + + for(Map.Entry<String,?> entry : map.entrySet()){ + String[] keyParts = entry.getKey().split(DELIMIT); + + if (keyParts.length == POS_PARAM_VALUE + 1) { + String componentString = keyParts[POS_COMPONENT_NAME]; + String eventCategory = keyParts[POS_CATEGORY_VALUE]; + String parameter = keyParts[POS_PARAM_VALUE]; + String eventAction = keyParts[POS_EVENT_VALUE]; + + StringBuilder sb = new StringBuilder(); + sb.append(componentString); // Add ComponentName String + sb.append(DELIMIT); + sb.append(eventCategory); // Add our category value + sb.append(DELIMIT); + sb.append(eventAction); // Add our event value + String eventKey = sb.toString(); + + Event.Builder eventBuilder; + if (unBuiltEvents.containsKey(eventKey)) { + eventBuilder = unBuiltEvents.get(eventKey); + } else { + eventBuilder = new Event.Builder(CATEGORY_BASE + eventCategory, eventAction); + eventBuilder.addField(PARAM_PROVIDER, componentString); + } + + eventBuilder.addField(parameter, String.valueOf(entry.getValue())); + unBuiltEvents.put(eventKey, eventBuilder); + } + } + return unBuiltEvents; + } + + public static void clearEventData(Context c, String key) { + SharedPreferences sp = c.getSharedPreferences(METRICS_SHARED_PREFERENCES, + Context.MODE_PRIVATE); + + Map<String, ?> map = sp.getAll(); + SharedPreferences.Editor editor = sp.edit(); + + for(Map.Entry<String,?> entry : map.entrySet()){ + String storedKey = entry.getKey(); + if (storedKey.startsWith(key)) { + editor.remove(storedKey); + } + } + editor.apply(); + } + +} diff --git a/src/com/cyanogenmod/messaging/util/MetricsJob.java b/src/com/cyanogenmod/messaging/util/MetricsJob.java new file mode 100644 index 0000000..8b623a7 --- /dev/null +++ b/src/com/cyanogenmod/messaging/util/MetricsJob.java @@ -0,0 +1,139 @@ +/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*====* + * Copyright (c) 2016 Cyanogen Inc. + * All Rights Reserved. + * Cyanogen Confidential and Proprietary. + * =========================================================================*/ + +package com.cyanogenmod.messaging.util; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.os.AsyncTask; +import android.util.Log; + +import com.cyanogen.ambient.common.ConnectionResult; +import com.cyanogen.ambient.common.ConnectionResult; +import com.cyanogen.ambient.common.api.AmbientApiClient; +import com.cyanogen.ambient.analytics.AnalyticsServices; +import com.cyanogen.ambient.analytics.Event; +import com.cyanogen.ambient.common.api.Result; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +/** + * MetricsJob is an aggregation and shipping service that is fired + * once every 24 hours to pass Metrics to ModCore's analytics service. + */ +public final class MetricsJob extends JobService { + + private static final String TAG = "MetricsJob"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final long TIMEOUT_MILLIS = 1000; + + public static final int METRICS_JOB_ID = 1441; + + private MetricsTask mUploadTask; + + private AmbientApiClient ambientApiClient; + + public MetricsJob() { + super(); + } + + @Override + public boolean onStartJob(JobParameters params) { + if (DEBUG) Log.v(TAG, "sending events"); + + // AmbientClient + ambientApiClient = new AmbientApiClient.Builder(this) + .addApi(AnalyticsServices.API) + .addOnConnectionFailedListener(new AmbientApiClient.OnConnectionFailedListener() { + @Override + public void onConnectionFailed(ConnectionResult result) { + Log.e(TAG, "Failed to connect to Ambient. reason: " + result.getErrorCode()); + } + }).build(); + + // Send stored Specific events + mUploadTask = new MetricsTask(params); + mUploadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void) null); + + // Running on another thread, return true. + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + + if (ambientApiClient != null && (ambientApiClient.isConnected() || ambientApiClient.isConnecting())) { + ambientApiClient.disconnect(); + } + + // Cancel our async task + mUploadTask.cancel(true); + + // report that we should try again soon. + return true; + } + + + class MetricsTask extends AsyncTask<Void, Void, Boolean> { + + JobParameters mMetricsJobParams; + + public MetricsTask(JobParameters params) { + this.mMetricsJobParams = params; + } + + @Override + protected Boolean doInBackground(Void... params) { + + HashMap<String, Event.Builder> eventsToSend + = MetricsHelper.getEventsToSend(MetricsJob.this); + + for (String key : eventsToSend.keySet()) { + + Event.Builder eventBuilder = eventsToSend.get(key); + + if (DEBUG) Log.v(TAG, "sending:" + eventBuilder.toString()); + + if (isCancelled()) { + return false; + } + + Result r = AnalyticsServices.AnalyticsApi.sendEvent( + ambientApiClient, + eventBuilder.build()) + .await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + + // if any of our results were not successful, something is wrong. + // Stop this job for now. + if (!r.getStatus().isSuccess()) { + return false; + } + + // We sent all the data we had for this event to the database. So clear it from our + // SharedPreferences. + MetricsHelper.clearEventData(MetricsJob.this, key); + } + return true; + } + + @Override + protected void onCancelled() { + if (DEBUG) Log.w(TAG, "Messaging Metrics Job Cancelled"); + // do nothing + } + + @Override + protected void onPostExecute(Boolean success) { + if (DEBUG) Log.v(TAG, "was success: " + success); + + // attempt to reschedule if analytics service is unavailable for our events + jobFinished(mMetricsJobParams, !success /* reschedule */); + } + } + +} |