diff options
author | Martin Brabham <optedoblivion@cyngn.com> | 2015-05-07 10:53:31 -0700 |
---|---|---|
committer | Martin Brabham <optedoblivion@cyngn.com> | 2015-05-28 14:35:18 -0700 |
commit | fff40f64168cacb55901ed4f0fe79add5efbc3dd (patch) | |
tree | 10df89b3b96229eb42b27908790ef35a7d152aa9 | |
parent | 347c6ec103f0f7781012a8ddac282bfbd319437d (diff) | |
download | android_packages_apps_Trebuchet-fff40f64168cacb55901ed4f0fe79add5efbc3dd.tar.gz android_packages_apps_Trebuchet-fff40f64168cacb55901ed4f0fe79add5efbc3dd.tar.bz2 android_packages_apps_Trebuchet-fff40f64168cacb55901ed4f0fe79add5efbc3dd.zip |
Trebuchet Statistics:
- Add plumbing for sending events.
- Add aggregation service that runs on some interval
- Hook send events into respective UX interactions
Change-Id: I77ba0ab75daf845621d800e0adf3e658096926af
16 files changed, 1419 insertions, 5 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ae2883a35..5a5c2540d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -75,6 +75,7 @@ <uses-permission android:name="com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS" /> <uses-permission android:name="com.android.launcher3.permission.RECEIVE_FIRST_LOAD_BROADCAST" /> <uses-permission android:name="cyanogenmod.permission.PROTECTED_APP" /> + <uses-permission android:name="com.cyngn.stats.SEND_ANALYTICS" /> <application android:name="com.android.launcher3.LauncherApplication" @@ -163,6 +164,14 @@ > </service> + <service + android:name="com.android.launcher3.stats.internal.service.AggregationIntentService" + android:exported="false"> + <intent-filter> + <action android:name="com.cyanogenmod.trebuchet.AGGREGATE_AND_TRACK" /> + </intent-filter> + </service> + <receiver android:name="com.android.launcher3.WallpaperChangedReceiver"> <intent-filter> diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index f6705a437..84f8b46c8 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -72,6 +72,7 @@ import android.os.Handler; import android.os.Message; import android.os.StrictMode; import android.os.SystemClock; +import android.preference.PreferenceManager; import android.speech.RecognizerIntent; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -123,6 +124,8 @@ import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.PagedView.TransitionEffect; import com.android.launcher3.settings.SettingsProvider; +import com.android.launcher3.stats.LauncherStats; +import com.android.launcher3.stats.internal.service.AggregationIntentService; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -172,6 +175,8 @@ public class Launcher extends Activity private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; public static final int REQUEST_TRANSITION_EFFECTS = 14; + public static final String LONGPRESS_CHANGE = "wallpaper_changed_by_longpress"; + private static final float OVERSHOOT_TENSION = 1.4f; static final int REQUEST_PICK_ICON = 13; @@ -1137,7 +1142,7 @@ public class Launcher extends Activity mBindOnResumeCallbacks.clear(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent processing callbacks in onResume: " + - (System.currentTimeMillis() - startTimeCallbacks)); + (System.currentTimeMillis() - startTimeCallbacks)); } } if (mOnResumeCallbacks.size() > 0) { @@ -1188,7 +1193,7 @@ public class Launcher extends Activity //Close out Fragments TransitionEffectsFragment tef = - (TransitionEffectsFragment)getFragmentManager().findFragmentByTag( + (TransitionEffectsFragment) getFragmentManager().findFragmentByTag( TransitionEffectsFragment.TRANSITION_EFFECTS_FRAGMENT); if (tef != null) { tef.setEffect(); @@ -1479,6 +1484,8 @@ public class Launcher extends Activity Intent settings; settings = new Intent(android.provider.Settings.ACTION_SETTINGS); startActivity(settings); + LauncherApplication.getLauncherStats().sendSettingsOpenedEvent( + LauncherStats.ORIGIN_TREB_LONGPRESS); if (mWorkspace.isInOverviewMode()) { mWorkspace.exitOverviewMode(false); } @@ -2177,6 +2184,9 @@ public class Launcher extends Activity public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { removeWidgetToAutoAdvance(launcherInfo.hostView); launcherInfo.hostView = null; + AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(launcherInfo.appWidgetId); + String packageName = info.providerInfo.packageName; + LauncherApplication.getLauncherStats().sendWidgetRemoveEvent(packageName); } void showOutOfSpaceMessage(boolean isHotseatLayout) { @@ -2606,6 +2616,8 @@ public class Launcher extends Activity completeAddAppWidget(appWidgetId, info.container, info.screenId, boundWidget, appWidgetInfo); mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false); + String packageName = appWidgetInfo.providerInfo.packageName; + LauncherApplication.getLauncherStats().sendWidgetAddEvent(packageName); } } @@ -2895,6 +2907,13 @@ public class Launcher extends Activity onClickAllAppsButton(v); } else if (tag instanceof AppInfo) { startAppShortcutOrInfoActivity(v); + LauncherApplication.getLauncherStats().sendAppLaunchEvent( + LauncherStats.ORIGIN_APPDRAWER, ((AppInfo)tag).componentName.getPackageName()); + String packageName = ((AppInfo)tag).getIntent().getComponent().getPackageName(); + if (LauncherStats.SETTINGS_PACKAGE_NAME.equals(packageName)) { + LauncherApplication.getLauncherStats() + .sendSettingsOpenedEvent(LauncherStats.ORIGIN_APPDRAWER); + } } else if (tag instanceof LauncherAppWidgetInfo) { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v); @@ -3059,6 +3078,13 @@ public class Launcher extends Activity // Start activities startAppShortcutOrInfoActivity(v); + String packageName = intent.getComponent().getPackageName(); + LauncherApplication.getLauncherStats().sendAppLaunchEvent(LauncherStats.ORIGIN_HOMESCREEN, + packageName); + if (LauncherStats.SETTINGS_PACKAGE_NAME.equals(packageName)) { + LauncherApplication.getLauncherStats().sendSettingsOpenedEvent( + LauncherStats.ORIGIN_HOMESCREEN); + } } private void startAppShortcutOrInfoActivity(View v) { @@ -3170,6 +3196,7 @@ public class Launcher extends Activity if (LOGD) Log.d(TAG, "onClickWallpaperPicker"); final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); pickWallpaper.setComponent(getWallpaperPickerComponent()); + mSharedPrefs.edit().putBoolean(LONGPRESS_CHANGE, true).apply(); startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER); } @@ -4818,6 +4845,10 @@ public class Launcher extends Activity mWorkspace.createCustomContentContainer(); populateCustomContentContainer(); } + + LauncherModel.saveWidgetCount(this); + LauncherModel.savePageCount(this); + } @Override diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java index 520beb639..56432e769 100644 --- a/src/com/android/launcher3/LauncherApplication.java +++ b/src/com/android/launcher3/LauncherApplication.java @@ -17,15 +17,30 @@ package com.android.launcher3; import android.app.Application; +import com.android.launcher3.stats.LauncherStats; +import com.android.launcher3.stats.internal.service.AggregationIntentService; public class LauncherApplication extends Application { public static boolean LAUNCHER_SHOW_UNREAD_NUMBER; public static boolean LAUNCHER_SHORTCUT_ENABLED; public static boolean SHOW_CTAPP_FEATURE; + public static String PACKAGE_NAME = ""; + + private static LauncherStats sLauncherStats = null; + + /** + * Get the reference handle for LauncherStats commands + * + * @return {@link LauncherStats} + */ + public static LauncherStats getLauncherStats() { + return sLauncherStats; + } @Override public void onCreate() { super.onCreate(); + PACKAGE_NAME = getPackageName(); LAUNCHER_SHOW_UNREAD_NUMBER = getResources().getBoolean( R.bool.config_launcher_show_unread_number); LAUNCHER_SHORTCUT_ENABLED = getResources().getBoolean( @@ -33,6 +48,8 @@ public class LauncherApplication extends Application { SHOW_CTAPP_FEATURE = getResources().getBoolean(R.bool.config_launcher_page); LauncherAppState.setApplicationContext(this); LauncherAppState.getInstance(); + sLauncherStats = LauncherStats.createInstance(this); + AggregationIntentService.scheduleService(this); } @Override @@ -40,4 +57,5 @@ public class LauncherApplication extends Application { super.onTerminate(); LauncherAppState.getInstance().onTerminate(); } + } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index a8bcb2c66..aca236155 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -62,6 +62,7 @@ import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.settings.SettingsProvider; +import com.android.launcher3.stats.internal.service.AggregationIntentService; import java.lang.ref.WeakReference; import java.net.URISyntaxException; @@ -1066,10 +1067,22 @@ public class LauncherModel extends BroadcastReceiver } /** + * Saves the total widget count to a shared preference + * + * @param context {@link Context} + */ + /* package */ static void saveWidgetCount(Context context) { + int widgetCount = LauncherModel.sBgAppWidgets.size(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + prefs.edit().putInt(AggregationIntentService.PREF_KEY_WIDGET_COUNT, widgetCount).apply(); + } + + /** * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ - static void addItemToDatabase(Context context, final ItemInfo item, final long container, + static void addItemToDatabase(final Context context, final ItemInfo item, final long container, final long screenId, final int cellX, final int cellY, final boolean notify) { item.container = container; item.cellX = cellX; @@ -1121,6 +1134,7 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.add((LauncherAppWidgetInfo) item); + saveWidgetCount(context); break; } } @@ -1173,7 +1187,7 @@ public class LauncherModel extends BroadcastReceiver * @param context * @param item */ - static void deleteItemsFromDatabase(Context context, final ArrayList<ItemInfo> items) { + static void deleteItemsFromDatabase(final Context context, final ArrayList<ItemInfo> items) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { @@ -1204,6 +1218,7 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.remove((LauncherAppWidgetInfo) item); + saveWidgetCount(context); break; } sBgItemsIdMap.remove(item.id); @@ -1216,10 +1231,22 @@ public class LauncherModel extends BroadcastReceiver } /** + * Saves the count of workspace pages + * + * @param context {@link Context} + */ + /* package */ static void savePageCount(Context context) { + int pageCount = LauncherModel.sBgWorkspaceScreens.size(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + prefs.edit().putInt(AggregationIntentService.PREF_KEY_PAGE_COUNT, pageCount).apply(); + } + + /** * Update the order of the workspace screens in the database. The array list contains * a list of screen ids in the order that they should appear. */ - void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) { + void updateWorkspaceScreenOrder(final Context context, final ArrayList<Long> screens) { // Log to disk Launcher.addDumpLog(TAG, "11683562 - updateWorkspaceScreenOrder()", true); Launcher.addDumpLog(TAG, "11683562 - screens: " + TextUtils.join(", ", screens), true); @@ -1261,6 +1288,7 @@ public class LauncherModel extends BroadcastReceiver synchronized (sBgLock) { sBgWorkspaceScreens.clear(); sBgWorkspaceScreens.addAll(screensCopy); + savePageCount(context); } } }; diff --git a/src/com/android/launcher3/WallpaperChangedReceiver.java b/src/com/android/launcher3/WallpaperChangedReceiver.java index 2d5612f12..0a6a7efa5 100644 --- a/src/com/android/launcher3/WallpaperChangedReceiver.java +++ b/src/com/android/launcher3/WallpaperChangedReceiver.java @@ -20,10 +20,25 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import com.android.launcher3.stats.LauncherStats; + public class WallpaperChangedReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent data) { LauncherAppState.setApplicationContext(context.getApplicationContext()); LauncherAppState appState = LauncherAppState.getInstance(); appState.onWallpaperChanged(); + SharedPreferences prefs = context.getSharedPreferences(LauncherAppState + .getSharedPreferencesKey(), Context.MODE_PRIVATE); + boolean fromSelf = prefs.getBoolean(Launcher.LONGPRESS_CHANGE, false); + if (fromSelf) { + prefs.edit().putBoolean(Launcher.LONGPRESS_CHANGE, false).apply(); + LauncherApplication.getLauncherStats().sendWallpaperChangedEvent( + LauncherStats.ORIGIN_TREB_LONGPRESS); + } else { + LauncherApplication.getLauncherStats().sendWallpaperChangedEvent( + LauncherStats.ORIGIN_CHOOSER); + } + } } diff --git a/src/com/android/launcher3/stats/LauncherStats.java b/src/com/android/launcher3/stats/LauncherStats.java new file mode 100644 index 000000000..e1dd1bbc6 --- /dev/null +++ b/src/com/android/launcher3/stats/LauncherStats.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import com.android.launcher3.LauncherApplication; +import com.android.launcher3.stats.internal.db.DatabaseHelper; +import com.android.launcher3.stats.internal.model.TrackingEvent; + +/** + * <pre> + * Utility class made specifically for Launcher related events + * </pre> + */ +public class LauncherStats { + + // Constants + private static final String TAG = LauncherStats.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final int MSG_STORE_EVENT = 1000; + public static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + public static final String ORIGIN_HOMESCREEN = "homescreen"; + public static final String ORIGIN_APPDRAWER = "appdrawer"; + public static final String ORIGIN_TREB_LONGPRESS = "trebuchet_longpress"; + public static final String ORIGIN_CHOOSER = "theme_chooser"; + + private static void log(String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be null or empty!"); + } + if (DEBUG) { + Log.d(TAG, msg); + } + } + + private static void loge(String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be null or empty!"); + } + Log.e(TAG, msg); + } + + /** + * <pre> + * This is a thread responsible for writing events to a database + * </pre> + * + * @see {@link HandlerThread} + */ + private static class WriteHandlerThread extends HandlerThread { + public WriteHandlerThread() { + super(WriteHandlerThread.class.getSimpleName()); + } + } + + /** + * <pre> + * Handler for issuing db writes + * </pre> + * + * @see {@link Handler} + */ + private static class WriteHandler extends Handler { + + public WriteHandler() { + super(sHandlerThread.getLooper()); + } + + @Override + public void handleMessage(Message msg) { + log("Handling message: " + msg.what); + switch (msg.what) { + case MSG_STORE_EVENT: + handleStoreEvent((TrackingEvent) msg.obj); + break; + default: + super.handleMessage(msg); + } + } + } + + // Instance + private static LauncherStats sInstance = null; + + // Members + private static WriteHandlerThread sHandlerThread = new WriteHandlerThread(); + private static WriteHandler sWriteHandler; + private static DatabaseHelper sDatabaseHelper; + private LauncherApplication mApplication; + + /** + * Send a message to the handler to store event data + * + * @param trackingEvent {@link TrackingEvent} + */ + private void sendStoreEventMessage(TrackingEvent trackingEvent) { + log("Sending tracking event to handler: " + trackingEvent); + Message msg = new Message(); + msg.what = MSG_STORE_EVENT; + msg.obj = trackingEvent; + sWriteHandler.sendMessage(msg); + } + + /** + * Handle the storing work + * + * @param trackingEvent {@link TrackingEvent} + */ + private static void handleStoreEvent(TrackingEvent trackingEvent) { + log("Handling store event: " + trackingEvent); + if (trackingEvent != null) { + sDatabaseHelper.writeEvent(trackingEvent); + } else { + loge("Tracking event was null!"); + } + } + + /** + * Constructor + * + * @param application {@link LauncherApplication} not null! + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + private LauncherStats(LauncherApplication application) throws IllegalArgumentException { + if (application == null) { + throw new IllegalArgumentException("'application' cannot be null!"); + } + mApplication = application; + sDatabaseHelper = new DatabaseHelper(application); + sHandlerThread.start(); + sWriteHandler = new WriteHandler(); + } + + /** + * Creates a singleton instance of the stats utility + * + * @param application {@link LauncherApplication} not null! + * @return {@link LauncherStats} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static LauncherStats createInstance(LauncherApplication application) + throws IllegalArgumentException { + if (sInstance == null) { + sInstance = new LauncherStats(application); + } + return sInstance; + } + + /** + * Interface for posting a new widget add event + * + * @param pkg {@link String} package name of widget + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendWidgetAddEvent(String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WIDGET_ADD); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for posting a new widget removal event + * + * @param pkg {@link String} package name of widget + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendWidgetRemoveEvent(String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WIDGET_REMOVE); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for posting an app launch event + * + * @param origin {@link String} origin of application launch + * @param pkg {@link String} package of app launched + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void sendAppLaunchEvent(String origin, String pkg) throws IllegalArgumentException { + if (TextUtils.isEmpty(origin)) { + throw new IllegalArgumentException("'origin' cannot be null!"); + } + if (TextUtils.isEmpty(pkg)) { + throw new IllegalArgumentException("'pkg' cannot be null!"); + } + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.APP_LAUNCH); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + trackingEvent.setMetaData(TrackingEvent.KEY_PACKAGE, pkg); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for sending a "settings opened" event + * + * @param origin {@link String} origin of the event + */ + public void sendSettingsOpenedEvent(String origin) { + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.SETTINGS_OPEN); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + sendStoreEventMessage(trackingEvent); + } + + /** + * Interface for sending a "wallpaper changed" event + * + * @param origin {@link String} origin of the event + */ + public void sendWallpaperChangedEvent(String origin) { + TrackingEvent trackingEvent = new TrackingEvent(TrackingEvent.Category.WALLPAPER_CHANGE); + trackingEvent.setMetaData(TrackingEvent.KEY_ORIGIN, origin); + sendStoreEventMessage(trackingEvent); + } + +} diff --git a/src/com/android/launcher3/stats/external/StatsUtil.java b/src/com/android/launcher3/stats/external/StatsUtil.java new file mode 100644 index 000000000..c258cf5b2 --- /dev/null +++ b/src/com/android/launcher3/stats/external/StatsUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.external; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import com.android.launcher3.stats.util.Logger; + +/** + * StatsUtil + * <pre> + * Utility for interfacing with CyanogenStats + * </pre> + */ +public class StatsUtil { + + // Tag and logging + private static final String TAG = StatsUtil.class.getSimpleName(); + + // Constants + private static final String KEY_TRACKING_ID = "tracking_id"; + private static final String ANALYTIC_INTENT = "com.cyngn.stats.action.SEND_ANALYTIC_EVENT"; + private static final String STATS_PACKAGE = "com.cyngn.stats"; + + /** + * Checks if stats collection is enabled + * + * @param context {@link android.content.Context} + * @return {@link java.lang.Boolean} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static boolean isStatsCollectionEnabled(Context context) + throws IllegalArgumentException { + return isStatsPackageInstalledAndSystemApp(context); + } + + /** + * Checks if the stats package is installed + * + * @param context {@link android.content.Context} + * @return {@link Boolean {@link Boolean {@link Boolean {@link Boolean}}}} + */ + private static boolean isStatsPackageInstalledAndSystemApp(Context context) + throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(STATS_PACKAGE, 0); + boolean isSystemApp = (pi.applicationInfo.flags & + (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0; + return pi.applicationInfo.enabled && isSystemApp; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, e.getMessage(), e); + return false; + } + } + + /** + * Send an event to CyangenStats + * + * @param context {@link Context} not null + * @param trackingBundle {@link Bundle} + * @throws IllegalArgumentException + */ + public static void sendEvent(Context context, Bundle trackingBundle) + throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + if (trackingBundle == null) { + throw new IllegalArgumentException("'trackingBundle' cannot be null!"); + } + if (!isStatsCollectionEnabled(context)) { + Logger.logd(TAG, "Stats collection: DISABLED!"); + return; + } + Logger.logd(TAG, "Stats collection: ENABLED!"); + + Intent newIntent = new Intent("com.cyngn.stats.action.SEND_ANALYTIC_EVENT"); + + if (!trackingBundle.containsKey(KEY_TRACKING_ID)) { + Logger.logd(TAG, "No tracking id in bundle"); + return; + } else { + if (trackingBundle.containsKey(TrackingBundle.KEY_EVENT_CATEGORY) + && trackingBundle.containsKey(TrackingBundle.KEY_EVENT_ACTION)) { + Logger.logd(TAG, trackingBundle.toString()); + newIntent.putExtras(trackingBundle); + context.sendBroadcast(newIntent); + } else { + Logger.logd(TAG, "Not a valid tracking bundle"); + } + } + } + +} diff --git a/src/com/android/launcher3/stats/external/TrackingBundle.java b/src/com/android/launcher3/stats/external/TrackingBundle.java new file mode 100644 index 000000000..6ca5d971e --- /dev/null +++ b/src/com/android/launcher3/stats/external/TrackingBundle.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.external; + +import android.os.Bundle; +import android.text.TextUtils; + +/** + * <pre> + * Extension of a {@link Bundle} to provider streamline interfaces for + * the specific task of sending events + * </pre> + * + * @see {@link Bundle} + */ +public class TrackingBundle { + + // Constants + public static final String KEY_TRACKING_ID = "tracking_id"; + public static final String KEY_EVENT_CATEGORY = "category"; + public static final String KEY_EVENT_ACTION = "action"; + public static final String KEY_METADATA_VALUE = "value"; + public static final String KEY_METADATA_ORIGIN = "origin"; + public static final String KEY_METADATA_PACKAGE = "package"; + + + /** + * Constructor + * + * @param trackingId {@link String} + * @param category {@link String} + * @param action {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static Bundle createTrackingBundle(String trackingId, String category, String action) + throws IllegalArgumentException { + if (TextUtils.isEmpty(trackingId)) { + throw new IllegalArgumentException("'trackingId' cannot be null or empty!"); + } + if (TextUtils.isEmpty(category)) { + throw new IllegalArgumentException("'category' cannot be null or empty!"); + } + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException("'action' cannot be null or empty!"); + } + Bundle bundle = new Bundle(); + bundle.putString(KEY_EVENT_CATEGORY, category); + bundle.putString(KEY_EVENT_ACTION, action); + bundle.putString(KEY_TRACKING_ID, trackingId); + return bundle; + } + +}
\ No newline at end of file diff --git a/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java b/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java new file mode 100644 index 000000000..7ffd509ff --- /dev/null +++ b/src/com/android/launcher3/stats/internal/db/DatabaseHelper.java @@ -0,0 +1,159 @@ +package com.android.launcher3.stats.internal.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import com.android.launcher3.stats.internal.model.TrackingEvent; +import com.android.launcher3.stats.util.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * <pre> + * Helper for accessing the database + * </pre> + * + * @see {@link SQLiteOpenHelper} + */ +public class DatabaseHelper extends SQLiteOpenHelper { + + // Constants + private static final String TAG = DatabaseHelper.class.getSimpleName(); + private static final String DATABASE_NAME = "events"; + private static final int DATABASE_VERSION = 1; + + // Instance + private static DatabaseHelper sInstance = null; + + /** + * Constructor + * + * @param context {@link Context} + * @return {@link DatabaseHelper} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static DatabaseHelper createInstance(Context context) throws IllegalArgumentException { + if (sInstance == null) { + sInstance = new DatabaseHelper(context); + } + return sInstance; + } + + /** + * Constructor + * + * @param context {@link Context} + */ + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + /** + * Write an event to the database + * + * @param trackingEvent {@link TrackingEvent} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void writeEvent(TrackingEvent trackingEvent) + throws IllegalArgumentException { + if (trackingEvent == null) { + throw new IllegalArgumentException("'trackingEvent' cannot be null!"); + } + Logger.logd(TAG, "Event written to database: " + trackingEvent); + SQLiteDatabase db = getWritableDatabase(); + ContentValues contentValues = trackingEvent.toContentValues(); + db.insert(TrackingEventContract.EVENT_TABLE_NAME, null, contentValues); + db.close(); + } + + /** + * Get a list of tracking events + * + * @param instanceId {@link Integer} + * @return {@link List} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public List<TrackingEvent> getTrackingEventsByCategory(int instanceId, + TrackingEvent.Category category) throws IllegalArgumentException { + if (category == null) { + throw new IllegalArgumentException("'category' cannot be null!"); + } + + List<TrackingEvent> eventList = new ArrayList<TrackingEvent>(); + + // Get a writable database + SQLiteDatabase db = getWritableDatabase(); + + // Update unclaimed items for this instance + ContentValues contentValues = new ContentValues(); + contentValues.put(TrackingEventContract.EVENT_COLUMN_INSTANCE, instanceId); + String whereClause = TrackingEventContract.EVENT_COLUMN_INSTANCE + " IS NULL AND " + + TrackingEventContract.EVENT_COLUMN_CATEGORY + " = ? "; + String[] whereArgs = new String[] { + category.name(), + }; + int cnt = db.update(TrackingEventContract.EVENT_TABLE_NAME, contentValues, whereClause, + whereArgs); + + // Short circuit empty update + if (cnt < 1) { + return eventList; + } + + // Select all tagged items + String selection = TrackingEventContract.EVENT_COLUMN_CATEGORY + " = ? AND " + + TrackingEventContract.EVENT_COLUMN_INSTANCE + " = ? "; + String[] selectionArgs = new String[]{ + category.name(), + String.valueOf(instanceId), + }; + Cursor c = db.query(TrackingEventContract.EVENT_TABLE_NAME, null, selection, selectionArgs, + null, null, null); + + // Build return list + while (c != null && c.getCount() > 0 && c.moveToNext()) { + eventList.add(new TrackingEvent(c)); + } + + db.close(); + + return eventList; + } + + /** + * Deletes events related to the instance + * + * @param instanceId {@link Integer} + * @return {@link Integer} + */ + public int deleteEventsByInstanceId(int instanceId) { + SQLiteDatabase db = getWritableDatabase(); + String whereClause = TrackingEventContract.EVENT_COLUMN_INSTANCE + " = ?"; + String[] whereArgs = new String[]{ + String.valueOf(instanceId), + }; + int cnt = db.delete(TrackingEventContract.EVENT_TABLE_NAME, whereClause, whereArgs); + db.close(); + return cnt; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(TrackingEventContract.CREATE_EVENT_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + // [NOTE][MSB]: This will lose data, need to make sure this is handled if/when database + // schema changes + + // db.execSQL("DROP TABLE IF EXISTS " + TrackingEventContract.EVENT_TABLE_NAME); + // onCreate(db); + + } + +} diff --git a/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java b/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java new file mode 100644 index 000000000..481a43193 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/db/TrackingEventContract.java @@ -0,0 +1,31 @@ +package com.android.launcher3.stats.internal.db; + +import android.provider.BaseColumns; + +/** + * <pre> + * Table contract definition + * </pre> + * + * @see {@link BaseColumns} + */ +public class TrackingEventContract implements BaseColumns { + + // Constants + public static final String EVENT_TABLE_NAME = "event"; + + // Columns + public static final String EVENT_COLUMN_CATEGORY = "category"; + public static final String EVENT_COLUMN_METADATA = "metadata"; + public static final String EVENT_COLUMN_INSTANCE = "instance"; + + // SQL + public static final String CREATE_EVENT_TABLE = "CREATE TABLE " + EVENT_TABLE_NAME + + " ( " + + " `" + _ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, " + + " `" + EVENT_COLUMN_CATEGORY + "` TEXT, " + + " `" + EVENT_COLUMN_METADATA + "` TEXT, " + + " `" + EVENT_COLUMN_INSTANCE + "` INTEGER " + + ");"; + +} diff --git a/src/com/android/launcher3/stats/internal/model/CountAction.java b/src/com/android/launcher3/stats/internal/model/CountAction.java new file mode 100644 index 000000000..d509d4d26 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/CountAction.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.internal.model; + +import android.os.Bundle; +import android.text.TextUtils; +import com.android.launcher3.stats.external.TrackingBundle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <pre> + * Handles the specific for sending a tracking event + * </pre> + * + * @see {@link ITrackingAction} + */ +public class CountAction implements ITrackingAction { + + public static final String TRACKING_ACTION = "count"; + + @Override + public String toString() { + return TRACKING_ACTION; + } + + @Override + public List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList) { + + Map<String, List<TrackingEvent>> eventPackageMap = + new HashMap<String, List<TrackingEvent>>(); + + for (TrackingEvent event : eventList) { + String pkg = event.getMetaData(TrackingEvent.KEY_PACKAGE); + pkg = (TextUtils.isEmpty(pkg)) ? trackingId : pkg; + if (!eventPackageMap.containsKey(pkg)) { + eventPackageMap.put(pkg, new ArrayList<TrackingEvent>()); + } + eventPackageMap.get(pkg).add(event); + } + + List<Bundle> bundleList = new ArrayList<Bundle>(); + for (Map.Entry<String, List<TrackingEvent>> entry : eventPackageMap.entrySet()) { + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, category.name(), + TRACKING_ACTION); + bundle.putInt(TrackingBundle.KEY_METADATA_VALUE, entry.getValue().size()); + String pkg = entry.getKey(); + if (!pkg.equals(trackingId)) { + bundle.putString(TrackingBundle.KEY_METADATA_PACKAGE, pkg); + } + bundleList.add(bundle); + } + return bundleList; + } +} diff --git a/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java b/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java new file mode 100644 index 000000000..fc04ca088 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/CountOriginByPackageAction.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.internal.model; + +import android.os.Bundle; +import android.text.TextUtils; +import com.android.launcher3.stats.external.TrackingBundle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <pre> + * This is an action to send a count of events with common origins + * </pre> + */ +public class CountOriginByPackageAction implements ITrackingAction { + + public static final String TRACKING_ACTION = "count_by_origin"; + + @Override + public String toString() { + return TRACKING_ACTION; + } + + @Override + public List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList) { + // Make an origin mapper + Map<String, Map<String, List<TrackingEvent>>> originEventMap = + new HashMap<String, Map<String, List<TrackingEvent>>>(); + + // Parse the event list and categorize by origin + for (TrackingEvent event : eventList) { + // We are parsing for things with origin, if no origin is set, discard it! + if (TextUtils.isEmpty(event.getMetaData(TrackingEvent.KEY_ORIGIN))) { + continue; + } + String originKey = event.getMetaData(TrackingEvent.KEY_ORIGIN); + if (!originEventMap.containsKey(originKey)) { + HashMap<String, List<TrackingEvent>> newMap = + new HashMap<String, List<TrackingEvent>>(); + originEventMap.put(originKey, newMap); + } + String packageName = event.getMetaData(TrackingEvent.KEY_PACKAGE); + // Set a default so our iteration picks it up and just discard package metadata + packageName = (TextUtils.isEmpty(packageName)) ? trackingId : packageName; + if (!originEventMap.get(originKey).containsKey(packageName)) { + originEventMap.get(originKey).put(packageName, new ArrayList<TrackingEvent>()); + } + originEventMap.get(originKey).get(packageName).add(event); + } + + // Start building result tracking bundles + List<Bundle> bundleList = new ArrayList<Bundle>(); + for (Map.Entry<String, Map<String, List<TrackingEvent>>> entry : + originEventMap.entrySet()) { + String origin = entry.getKey(); + for (Map.Entry<String, List<TrackingEvent>> entry2 : entry.getValue().entrySet()) { + String pkg = entry2.getKey(); + List<TrackingEvent> events = entry2.getValue(); + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, category.name(), + TRACKING_ACTION); + bundle.putString(TrackingBundle.KEY_METADATA_ORIGIN, origin); + bundle.putInt(TrackingBundle.KEY_METADATA_VALUE, events.size()); + if (!trackingId.equals(pkg)) { + bundle.putString(TrackingBundle.KEY_METADATA_PACKAGE, pkg); + } + bundleList.add(bundle); + } + } + return bundleList; + } + +} diff --git a/src/com/android/launcher3/stats/internal/model/ITrackingAction.java b/src/com/android/launcher3/stats/internal/model/ITrackingAction.java new file mode 100644 index 000000000..b577ed2d0 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/ITrackingAction.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.internal.model; + +import android.os.Bundle; + +import java.util.List; + +/** + * <pre> + * This is an action we want to perfrom from a report. + * + * e.g. + * 1. I want to get the COUNT of widgets added + * 2. I want to get the origin of app launches + * </pre> + */ +public interface ITrackingAction { + + /** + * Creates a new bundle used to tracking events + * + * @param trackingId {@link String} + * @param category {@link com.android.launcher3.stats.internal.model.TrackingEvent.Category} + * @param eventList {@link List} + * @return {@link List} + */ + List<Bundle> createTrackingBundles(String trackingId, TrackingEvent.Category category, + List<TrackingEvent> eventList); + +} diff --git a/src/com/android/launcher3/stats/internal/model/TrackingEvent.java b/src/com/android/launcher3/stats/internal/model/TrackingEvent.java new file mode 100644 index 000000000..91a9017be --- /dev/null +++ b/src/com/android/launcher3/stats/internal/model/TrackingEvent.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.internal.model; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import com.android.launcher3.stats.external.TrackingBundle; +import com.android.launcher3.stats.internal.db.TrackingEventContract; +import com.android.launcher3.stats.util.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * <pre> + * Model of an event to track + * </pre> + */ +public class TrackingEvent { + + // Constants + private static final String TAG = TrackingEvent.class.getSimpleName(); + + // Members + private Category mCategory; + private final Map<String, String> mMetaData = new HashMap<String, String>(); + + public enum Category { + APP_LAUNCH, + WIDGET_ADD, + WIDGET_REMOVE, + SETTINGS_OPEN, + WALLPAPER_CHANGE, + HOMESCREEN_PAGE, + WIDGET, + } + + public static final String KEY_ORIGIN = TrackingBundle.KEY_METADATA_ORIGIN; + public static final String KEY_VALUE = TrackingBundle.KEY_METADATA_VALUE; + public static final String KEY_PACKAGE = TrackingBundle.KEY_METADATA_PACKAGE; + + /** + * Constructor + * + * @param category {@link TrackingEvent.Category} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public TrackingEvent(Category category) throws IllegalArgumentException { + if (category == null) { + throw new IllegalArgumentException("'category' cannot be null or empty!"); + } + mCategory = category; + } + + /** + * Constructor + * + * @param cursor {@link Cursor} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public TrackingEvent(Cursor cursor) throws IllegalArgumentException { + if (cursor == null) { + throw new IllegalArgumentException("'cursor' cannot be null!"); + } + mCategory = Category.valueOf(cursor.getString(cursor.getColumnIndex( + TrackingEventContract.EVENT_COLUMN_CATEGORY))); + String metadata = cursor.getString(cursor.getColumnIndex( + TrackingEventContract.EVENT_COLUMN_METADATA)); + if (!TextUtils.isEmpty(metadata)) { + String[] parts = metadata.split(","); + for (String part : parts) { + try { + String key = part.split("=")[0]; + String val = part.split("=")[1]; + mMetaData.put(key, val); + } catch (IndexOutOfBoundsException e) { + Log.w(TAG, e.getMessage(), e); + } + } + } + } + + /** + * Get the category + * + * @return {@link TrackingEvent.Category} + */ + public Category getCategory() { + return mCategory; + } + + /** + * Get the set of meta data keys + * + * @return {@link Set} + */ + public Set<String> getMetaDataKeySet() { + return mMetaData.keySet(); + } + + /** + * Set some meta data + * + * @param key {@link String} + * @param value {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public void setMetaData(String key, String value) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (TextUtils.isEmpty(value)) { + throw new IllegalArgumentException("'value' cannot be null or empty!"); + } + mMetaData.put(key, value); + } + + /** + * Get some meta data value + * + * @param key {@link String} + * @return {@link String} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public String getMetaData(String key) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (mMetaData.containsKey(key)) { + return mMetaData.get(key); + } + return null; + } + + /** + * Remove some meta data + * + * @param key {@link String} + * @return {@link String} or null + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public String removeMetaData(String key) throws IllegalArgumentException { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("'key' cannot be null or empty!"); + } + if (mMetaData.containsKey(key)) { + return mMetaData.remove(key); + } + return null; + } + + /** + * Converts this object into content values for use with sqlite + * + * @return {@link ContentValues} + */ + public ContentValues toContentValues() { + ContentValues contentValues = new ContentValues(); + contentValues.put(TrackingEventContract.EVENT_COLUMN_CATEGORY, mCategory.name()); + StringBuilder sb = new StringBuilder(); + for (String key : mMetaData.keySet()) { + sb.append(key).append("=").append(mMetaData.get(key)).append(","); + } + if (sb.length() > 0) { + String metadata = sb.toString(); + metadata = metadata.substring(0, metadata.length() - 1); + Logger.logd(TAG, "MetaData: " + metadata); + contentValues.put(TrackingEventContract.EVENT_COLUMN_METADATA, metadata); + } + return contentValues; + } + + /** + * Convert this object into a tracking bundle + * + * @param trackingId {@link String} + * @param action {@link ITrackingAction} + * @return {@link Bundle} + */ + public Bundle toTrackingBundle(String trackingId, ITrackingAction action) { + Bundle bundle = TrackingBundle.createTrackingBundle(trackingId, mCategory.name(), + action.toString()); + return bundle; + } + +} diff --git a/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java b/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java new file mode 100644 index 000000000..5b3fd7493 --- /dev/null +++ b/src/com/android/launcher3/stats/internal/service/AggregationIntentService.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.internal.service; + +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherApplication; +import com.android.launcher3.stats.external.StatsUtil; +import com.android.launcher3.stats.external.TrackingBundle; +import com.android.launcher3.stats.internal.db.DatabaseHelper; +import com.android.launcher3.stats.internal.model.CountAction; +import com.android.launcher3.stats.internal.model.CountOriginByPackageAction; +import com.android.launcher3.stats.internal.model.ITrackingAction; +import com.android.launcher3.stats.internal.model.TrackingEvent; +import com.android.launcher3.stats.util.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * <pre> + * Service that starts on a timer and handles aggregating events and sending them to + * CyanogenStats + * </pre> + * + * @see {@link IntentService} + */ +public class AggregationIntentService extends IntentService { + + // Constants + private static final String TAG = AggregationIntentService.class.getSimpleName(); + private static final String TRACKING_ID = "com.cyanogenmod.trebuchet"; + public static final String ACTION_AGGREGATE_AND_TRACK = + "com.cyanogenmod.trebuchet.AGGREGATE_AND_TRACK"; + private static final List<ITrackingAction> TRACKED_ACTIONS = new ArrayList<ITrackingAction>() { + { + add(new CountAction()); + add(new CountOriginByPackageAction()); + } + }; + private static final int INVALID_COUNT = -1; + private static final String KEY_LAST_TIME_RAN = "last_time_stats_ran"; + public static final String PREF_KEY_PAGE_COUNT = "page_count"; + public static final String PREF_KEY_WIDGET_COUNT = "widget_count"; + + // Members + private DatabaseHelper mDatabaseHelper = null; + private int mInstanceId = -1; + private SharedPreferences mPrefs = null; + + /** + * Creates an IntentService. Invoked by your subclass's constructor. + */ + public AggregationIntentService() { + super(AggregationIntentService.class.getSimpleName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (!isTrebuchetDefaultLauncher()) { + // Cancel repeating schedule + unscheduleService(); + // don't return b/c we still want to upload whatever metrics are left. + } + String action = intent.getAction(); + if (ACTION_AGGREGATE_AND_TRACK.equals(action)) { + mPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + mPrefs.edit().putLong(KEY_LAST_TIME_RAN, System.currentTimeMillis()).apply(); + mInstanceId = (int) System.currentTimeMillis(); + mDatabaseHelper = DatabaseHelper.createInstance(this); + performAggregation(); + deleteTrackingEventsForInstance(); + handleNonEventMetrics(); + } + } + + private void performAggregation() { + + // Iterate available categories + for (TrackingEvent.Category category : TrackingEvent.Category.values()) { + + // Fetch the events from the database based on the category + List<TrackingEvent> eventList = + mDatabaseHelper.getTrackingEventsByCategory(mInstanceId, category); + + Logger.logd(TAG, "Event list size: " + eventList.size()); + // Short circuit if no events for the category + if (eventList.size() < 1) { + continue; + } + + // Now crunch the data into actionable events for the server + for (ITrackingAction action : TRACKED_ACTIONS) { + try { + for (Bundle bundle : action.createTrackingBundles(TRACKING_ID, category, + eventList)) { + performTrackingCall(bundle); + } + } catch (NullPointerException e) { + Log.e(TAG, "NPE fetching bundle list!", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Illegal argument!", e); + } + } + + } + } + + private void deleteTrackingEventsForInstance() { + mDatabaseHelper.deleteEventsByInstanceId(mInstanceId); + } + + /** + * These are metrics that are not event based and need a snapshot every INTERVAL + */ + private void handleNonEventMetrics() { + sendPageCountStats(); + sendWidgetCountStats(); + + } + + private void sendPageCountStats() { + int pageCount = mPrefs.getInt(PREF_KEY_PAGE_COUNT, INVALID_COUNT); + if (pageCount == INVALID_COUNT) { + return; + } + Bundle bundle = TrackingBundle + .createTrackingBundle(TRACKING_ID, TrackingEvent.Category.HOMESCREEN_PAGE.name(), + "count"); + bundle.putInt(TrackingEvent.KEY_VALUE, pageCount); + StatsUtil.sendEvent(this, bundle); + } + + private void sendWidgetCountStats() { + int widgetCount = mPrefs.getInt(PREF_KEY_WIDGET_COUNT, INVALID_COUNT); + if (widgetCount == INVALID_COUNT) { + return; + } + Bundle bundle = TrackingBundle + .createTrackingBundle(TRACKING_ID, TrackingEvent.Category.WIDGET.name(), "count"); + bundle.putInt(TrackingEvent.KEY_VALUE, widgetCount); + StatsUtil.sendEvent(this, bundle); + } + + private void performTrackingCall(Bundle bundle) throws IllegalArgumentException { + StatsUtil.sendEvent(this, bundle); + } + + private void unscheduleService() { + Intent intent = new Intent(this, AggregationIntentService.class); + intent.setAction(ACTION_AGGREGATE_AND_TRACK); + PendingIntent pi = PendingIntent.getService(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pi); + } + + private boolean isTrebuchetDefaultLauncher() { + final IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); + filter.addCategory(Intent.CATEGORY_HOME); + + List<IntentFilter> filters = new ArrayList<IntentFilter>(); + filters.add(filter); + + final String myPackageName = getPackageName(); + List<ComponentName> activities = new ArrayList<ComponentName>(); + final PackageManager packageManager = getPackageManager(); + + // You can use name of your package here as third argument + packageManager.getPreferredActivities(filters, activities, null); + + for (ComponentName activity : activities) { + if (myPackageName.equals(activity.getPackageName())) { + Logger.logd(TAG, "Trebuchet IS default launcher!"); + return true; + } + } + Logger.logd(TAG, "Trebuchet IS NOT default launcher!"); + return false; + } + + private static final long ALARM_INTERVAL = 86400000; // 1 day + + /** + * Schedule an alarm service, will cancel existing + * + * @param context {@link Context} + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static void scheduleService(Context context) throws IllegalArgumentException { + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + long lastTimeRan = prefs.getLong(KEY_LAST_TIME_RAN, 0); + Intent intent = new Intent(context, AggregationIntentService.class); + intent.setAction(ACTION_AGGREGATE_AND_TRACK); + PendingIntent pi = PendingIntent.getService(context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pi); + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, lastTimeRan + ALARM_INTERVAL, + ALARM_INTERVAL, pi); + } + +} diff --git a/src/com/android/launcher3/stats/util/Logger.java b/src/com/android/launcher3/stats/util/Logger.java new file mode 100644 index 000000000..8d73f54ca --- /dev/null +++ b/src/com/android/launcher3/stats/util/Logger.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015. The CyanogenMod Project + * + * 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 com.android.launcher3.stats.util; + +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +/** + * <pre> + * Metrics debug logging + * </pre> + */ +public class Logger { + + private static final String TAG = "TrebuchetStats"; + + /** + * Log a debug message + * + * @param tag {@link String} + * @param msg {@link String } + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + public static void logd(String tag, String msg) throws IllegalArgumentException { + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("'tag' cannot be empty!"); + } + if (TextUtils.isEmpty(msg)) { + throw new IllegalArgumentException("'msg' cannot be empty!"); + } + if (isDebugging()) { + Log.d(TAG, tag + " [ " + msg + " ]"); + } + } + + private static boolean isDebugging() { + return Log.isLoggable(TAG, Log.DEBUG); + } + +} |