Initial implementation: Broadcasts on app launch.

Look for com.android.launcher3.action.LAUNCH to be sent when
an icon is clicked in Launcher. (Restricted to
com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS
which is a signature permission right now.

This is specifically tracking apps launched via shortcut
icon; any other method of launching apps (notifications,
recents, internal navigation, etc.) is outside of Launcher's
purview and hence not broadcast.

The broadcast currently includes, in the "intent" extra, the
Uri flattening of the specific shortcut clicked.

The file /data/data/<pkg>/files/launches.log contains a
binary log of all such launches, including additional info
like screen# that should probably be in the broadcast too.

This info is summarized in .../stats.log, which encodes a
simple histogram of app launches since basically forever.
This should probably be done over a sliding window, which
will require more processing on startup.

Bug: 10031590
Change-Id: Ifc5921d5dc20701c67678cbfdc89b03cacd62028
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index fa96279..c3ea1ef 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -345,6 +345,8 @@
         int cellY;
     }
 
+    private Stats mStats;
+
     private static boolean isPropertyEnabled(String propertyName) {
         return Log.isLoggable(propertyName, Log.VERBOSE);
     }
@@ -378,6 +380,8 @@
         mDragController = new DragController(this);
         mInflater = getLayoutInflater();
 
+        mStats = new Stats(this);
+
         mAppWidgetManager = AppWidgetManager.getInstance(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
         mAppWidgetHost.startListening();
@@ -2086,7 +2090,8 @@
         Object tag = v.getTag();
         if (tag instanceof ShortcutInfo) {
             // Open shortcut
-            final Intent intent = ((ShortcutInfo) tag).intent;
+            final ShortcutInfo shortcut = (ShortcutInfo) tag;
+            final Intent intent = shortcut.intent;
 
             // Check for special shortcuts
             if (intent.getComponent() != null) {
@@ -2112,6 +2117,8 @@
 
             boolean success = startActivitySafely(v, intent, tag);
 
+            mStats.recordLaunch(intent, shortcut);
+
             if (success && v instanceof BubbleTextView) {
                 mWaitingForResume = (BubbleTextView) v;
                 mWaitingForResume.setStayPressed(true);
diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java
new file mode 100644
index 0000000..ca088f7
--- /dev/null
+++ b/src/com/android/launcher3/Stats.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 The Android Open Source 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import java.io.*;
+import java.util.ArrayList;
+
+public class Stats {
+    private static final boolean DEBUG_BROADCASTS = false;
+    private static final String TAG = "Launcher3/Stats";
+
+    private static final boolean LOCAL_LAUNCH_LOG = true;
+
+    public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH";
+    public static final String PERM_LAUNCH = "com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS";
+    public static final String EXTRA_INTENT = "intent";
+    public static final String EXTRA_CONTAINER = "container";
+    public static final String EXTRA_SCREEN = "screen";
+    public static final String EXTRA_CELLX = "cellX";
+    public static final String EXTRA_CELLY = "cellY";
+
+    private static final String LOG_FILE_NAME = "launches.log";
+    private static final int LOG_VERSION = 1;
+    private static final int LOG_TAG_VERSION = 0x1;
+    private static final int LOG_TAG_LAUNCH = 0x1000;
+
+    private static final String STATS_FILE_NAME = "stats.log";
+    private static final int STATS_VERSION = 1;
+    private static final int INITIAL_STATS_SIZE = 100;
+
+    // TODO: delayed/batched writes
+    private static final boolean FLUSH_IMMEDIATELY = true;
+
+    private final Launcher mLauncher;
+
+    DataOutputStream mLog;
+
+    ArrayList<String> mIntents;
+    ArrayList<Integer> mHistogram;
+
+    public Stats(Launcher launcher) {
+        mLauncher = launcher;
+
+        loadStats();
+
+        if (LOCAL_LAUNCH_LOG) {
+            try {
+                mLog = new DataOutputStream(mLauncher.openFileOutput(LOG_FILE_NAME, Context.MODE_APPEND));
+                mLog.writeInt(LOG_TAG_VERSION);
+                mLog.writeInt(LOG_VERSION);
+            } catch (FileNotFoundException e) {
+                Log.e(TAG, "unable to create stats log: " + e);
+                mLog = null;
+            } catch (IOException e) {
+                Log.e(TAG, "unable to write to stats log: " + e);
+                mLog = null;
+            }
+        }
+
+        if (DEBUG_BROADCASTS) {
+            launcher.registerReceiver(
+                    new BroadcastReceiver() {
+                        @Override
+                        public void onReceive(Context context, Intent intent) {
+                            android.util.Log.v("Stats", "got broadcast: " + intent + " for launched intent: "
+                                    + intent.getStringExtra(EXTRA_INTENT));
+                        }
+                    },
+                    new IntentFilter(ACTION_LAUNCH),
+                    PERM_LAUNCH,
+                    null
+            );
+        }
+    }
+
+    public void incrementLaunch(String intentStr) {
+        int pos = mIntents.indexOf(intentStr);
+        if (pos < 0) {
+            mIntents.add(intentStr);
+            mHistogram.add(1);
+        } else {
+            mHistogram.set(pos, mHistogram.get(pos) + 1);
+        }
+    }
+
+    public void recordLaunch(Intent intent, ShortcutInfo shortcut) {
+        intent = new Intent(intent);
+        intent.setSourceBounds(null);
+
+        final String flat = intent.toUri(0);
+
+        mLauncher.sendBroadcast(
+                new Intent(ACTION_LAUNCH)
+                        .putExtra(EXTRA_INTENT, flat)
+                        .putExtra(EXTRA_CONTAINER, shortcut.container)
+                        .putExtra(EXTRA_SCREEN, shortcut.screenId)
+                        .putExtra(EXTRA_CELLX, shortcut.cellX)
+                        .putExtra(EXTRA_CELLY, shortcut.cellY),
+                PERM_LAUNCH);
+
+        incrementLaunch(flat);
+
+        if (FLUSH_IMMEDIATELY) {
+            saveStats();
+        }
+
+        if (LOCAL_LAUNCH_LOG && mLog != null) {
+            try {
+                mLog.writeInt(LOG_TAG_LAUNCH);
+                mLog.writeLong(System.currentTimeMillis());
+                mLog.writeShort((short) shortcut.container);
+                mLog.writeShort((short) shortcut.screenId);
+                mLog.writeShort((short) shortcut.cellX);
+                mLog.writeShort((short) shortcut.cellY);
+                mLog.writeUTF(flat);
+                if (FLUSH_IMMEDIATELY) {
+                    mLog.flush(); // TODO: delayed writes
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private void saveStats() {
+        DataOutputStream stats = null;
+        try {
+            stats = new DataOutputStream(mLauncher.openFileOutput(STATS_FILE_NAME + ".tmp", Context.MODE_PRIVATE));
+            stats.writeInt(STATS_VERSION);
+            final int N = mHistogram.size();
+            stats.writeInt(N);
+            for (int i=0; i<N; i++) {
+                stats.writeUTF(mIntents.get(i));
+                stats.writeInt(mHistogram.get(i));
+            }
+            stats.close();
+            stats = null;
+            mLauncher.getFileStreamPath(STATS_FILE_NAME + ".tmp")
+                     .renameTo(mLauncher.getFileStreamPath(STATS_FILE_NAME));
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "unable to create stats data: " + e);
+        } catch (IOException e) {
+            Log.e(TAG, "unable to write to stats data: " + e);
+        } finally {
+            if (stats != null) {
+                try {
+                    stats.close();
+                } catch (IOException e) { }
+            }
+        }
+    }
+
+    private void loadStats() {
+        mIntents = new ArrayList<String>(INITIAL_STATS_SIZE);
+        mHistogram = new ArrayList<Integer>(INITIAL_STATS_SIZE);
+        DataInputStream stats = null;
+        try {
+            stats = new DataInputStream(mLauncher.openFileInput(STATS_FILE_NAME));
+            final int version = stats.readInt();
+            if (version == STATS_VERSION) {
+                final int N = stats.readInt();
+                for (int i=0; i<N; i++) {
+                    final String pkg = stats.readUTF();
+                    final int count = stats.readInt();
+                    mIntents.add(pkg);
+                    mHistogram.add(count);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // not a problem
+        } catch (IOException e) {
+            // more of a problem
+
+        } finally {
+            if (stats != null) {
+                try {
+                    stats.close();
+                } catch (IOException e) { }
+            }
+        }
+    }
+}