Merge "Adding overview mode for reordering, widget adding and wallpaper switching" into jb-ub-gel-agar
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b9f75cf..f885a6c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,6 +50,11 @@
         android:label="@string/permlab_write_settings"
         android:description="@string/permdesc_write_settings"/>
 
+    <permission
+        android:name="com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS"
+        android:protectionLevel="signature"
+        />
+
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.SET_WALLPAPER" />
     <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
@@ -60,6 +65,7 @@
     <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />
     <uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" />
     <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS" />
 
     <application
         android:name="com.android.launcher3.LauncherApplication"
diff --git a/print_db.py b/print_db.py
index f0fc45e..6caa7bb 100755
--- a/print_db.py
+++ b/print_db.py
@@ -140,6 +140,10 @@
   hotseatIdMap = []
   HOTSEAT_SIZE = 0
   for d in data:
+    if d["spanX"] is None:
+      d["spanX"] = 1
+    if d["spanY"] is None:
+      d["spanY"] = 1
     if d["container"] == CONTAINER_DESKTOP:
       if d["screen"] not in screensIdMap:
         screensIdMap.append(d["screen"])
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index 92f126c..fa713a4 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -27,6 +27,7 @@
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Looper;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -167,8 +168,6 @@
         public float mOuterRingSize;
         public float mInnerRingSize;
         public FolderIcon mFolderIcon = null;
-        public Drawable mOuterRingDrawable = null;
-        public Drawable mInnerRingDrawable = null;
         public static Drawable sSharedOuterRingDrawable = null;
         public static Drawable sSharedInnerRingDrawable = null;
         public static int sPreviewSize = -1;
@@ -180,12 +179,14 @@
         public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) {
             mFolderIcon = folderIcon;
             Resources res = launcher.getResources();
-            mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo);
-            mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo);
 
             // We need to reload the static values when configuration changes in case they are
             // different in another configuration
             if (sStaticValuesDirty) {
+                if (Looper.myLooper() != Looper.getMainLooper()) {
+                    throw new RuntimeException("FolderRingAnimator loading drawables on non-UI thread "
+                            + Thread.currentThread());
+                }
                 sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size);
                 sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding);
                 sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 09881b6..6cebe42 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -346,6 +346,8 @@
         int cellY;
     }
 
+    private Stats mStats;
+
     private static boolean isPropertyEnabled(String propertyName) {
         return Log.isLoggable(propertyName, Log.VERBOSE);
     }
@@ -379,6 +381,8 @@
         mDragController = new DragController(this);
         mInflater = getLayoutInflater();
 
+        mStats = new Stats(this);
+
         mAppWidgetManager = AppWidgetManager.getInstance(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
         mAppWidgetHost.startListening();
@@ -2128,7 +2132,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) {
@@ -2154,6 +2159,8 @@
 
             boolean success = startActivitySafely(v, intent, tag);
 
+            mStats.recordLaunch(intent, shortcut);
+
             if (success && v instanceof BubbleTextView) {
                 mWaitingForResume = (BubbleTextView) v;
                 mWaitingForResume.setStayPressed(true);
@@ -3554,6 +3561,7 @@
     public void bindAddScreens(ArrayList<Long> orderedScreenIds) {
         int count = orderedScreenIds.size();
         for (int i = 0; i < count; i++) {
+            Log.d(TAG, "10249126 - bindAddScreens(" + orderedScreenIds.get(i) + ")");
             mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(orderedScreenIds.get(i), false);
         }
     }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index cec446f..9ec6c35 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -311,6 +311,7 @@
                                     sBgWorkspaceScreens.size());
                             while (numPagesToAdd > 0) {
                                 long screenId = lp.generateNewScreenId();
+                                Log.d(TAG, "10249126 - addAndBindAddedApps(" + screenId + ")");
                                 // Update the model
                                 sBgWorkspaceScreens.add(screenId);
                                 updateWorkspaceScreenOrder(context, sBgWorkspaceScreens);
@@ -1008,6 +1009,7 @@
                     long screenId = screens.get(i);
                     v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
                     v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+                    Log.d(TAG, "10249126 - updateWorkspaceScreenOrder(" + screenId + ", " + i + ")");
                     values[i] = v;
                 }
                 cr.bulkInsert(uri, values);
@@ -1432,6 +1434,7 @@
 
             // Ensure that all the applications that are in the system are represented on the home
             // screen.
+            Log.d(TAG, "10249126 - verifyApplications(" + isUpgrade + ")");
             if (!isUpgrade) {
                 verifyApplications();
             }
@@ -1495,6 +1498,7 @@
             synchronized (sBgLock) {
                 for (ApplicationInfo app : mBgAllAppsList.data) {
                     tmpInfos = getItemInfoForComponentName(app.componentName);
+                    Log.d(TAG, "10249126 - \t" + app.componentName.getPackageName() + ", " + tmpInfos.isEmpty());
                     if (tmpInfos.isEmpty()) {
                         // We are missing an application icon, so add this to the workspace
                         added.add(app);
@@ -1585,6 +1589,7 @@
                 sBgItemsIdMap.clear();
                 sBgDbIconCache.clear();
                 sBgWorkspaceScreens.clear();
+                Log.d(TAG, "10249126 - loadWorkspace()");
 
                 final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
                 final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
@@ -1647,16 +1652,18 @@
                                 try {
                                     intent = Intent.parseUri(intentDescription, 0);
                                     ComponentName cn = intent.getComponent();
-                                    if (!isValidPackage(manager, cn)) {
+                                    if (!isValidPackageComponent(manager, cn)) {
                                         if (!mAppsCanBeOnRemoveableStorage) {
-                                            // Log the invalid package, and remove it from the database
-                                            Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
+                                            // Log the invalid package, and remove it from the db
+                                            Uri uri = LauncherSettings.Favorites.getContentUri(id,
+                                                    false);
                                             contentResolver.delete(uri, null, null);
-                                            Log.e(TAG, "Invalid package removed in loadWorkspace: " + cn);
+                                            Log.e(TAG, "Invalid package removed: " + cn);
                                         } else {
-                                            // If apps can be on external storage, then we just leave
-                                            // them for the user to remove (maybe add treatment to it)
-                                            Log.e(TAG, "Invalid package found in loadWorkspace: " + cn);
+                                            // If apps can be on external storage, then we just
+                                            // leave them for the user to remove (maybe add
+                                            // visual treatment to it)
+                                            Log.e(TAG, "Invalid package found: " + cn);
                                         }
                                         continue;
                                     }
@@ -1715,6 +1722,8 @@
                                     // now that we've loaded everthing re-save it with the
                                     // icon in case it disappears somehow.
                                     queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex);
+                                } else {
+                                    throw new RuntimeException("Unexpected null ShortcutInfo");
                                 }
                                 break;
 
@@ -1826,6 +1835,7 @@
                         long screenId = item.screenId;
                         if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
                                 !sBgWorkspaceScreens.contains(screenId)) {
+                            Log.d(TAG, "10249126 - loadWorkspace-loadedOldDb(" + screenId + ")");
                             sBgWorkspaceScreens.add(screenId);
                             if (screenId > maxScreenId) {
                                 maxScreenId = screenId;
@@ -1859,9 +1869,11 @@
                                 long screenId = sc.getLong(idIndex);
                                 int rank = sc.getInt(rankIndex);
 
+                                Log.d(TAG, "10249126 - loadWorkspace-!loadedOldDb(" + screenId + ", " + rank + ")");
+
                                 orderedScreens.put(rank, screenId);
                             } catch (Exception e) {
-                                Log.w(TAG, "Desktop items loading interrupted:", e);
+                                Log.w(TAG, "Desktop items loading interrupted - invalid screens: ", e);
                             }
                         }
                     } finally {
@@ -2039,7 +2051,7 @@
 
         private void bindWorkspaceScreens(final Callbacks oldCallbacks,
                 final ArrayList<Long> orderedScreens) {
-
+            Log.d(TAG, "10249126 - bindWorkspaceScreens()");
             final Runnable r = new Runnable() {
                 @Override
                 public void run() {
@@ -2535,7 +2547,7 @@
         return widgetsAndShortcuts;
     }
 
-    private boolean isValidPackage(PackageManager pm, ComponentName cn) {
+    private boolean isValidPackageComponent(PackageManager pm, ComponentName cn) {
         if (cn == null) {
             return false;
         }
@@ -2563,9 +2575,18 @@
     public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, Context context,
             Cursor c, int iconIndex, int titleIndex, HashMap<Object, CharSequence> labelCache) {
         ComponentName componentName = intent.getComponent();
-        if (!isValidPackage(manager, componentName)) {
+        final ShortcutInfo info = new ShortcutInfo();
+        if (!isValidPackageComponent(manager, componentName)) {
             Log.d(TAG, "Invalid package found in getShortcutInfo: " + componentName);
             return null;
+        } else {
+            try {
+                PackageInfo pi = manager.getPackageInfo(componentName.getPackageName(), 0);
+                info.initFlagsAndFirstInstallTime(pi);
+            } catch (NameNotFoundException e) {
+                Log.d(TAG, "getPackInfo failed for package " +
+                        componentName.getPackageName());
+            }
         }
 
         // TODO: See if the PackageManager knows about this case.  If it doesn't
@@ -2579,7 +2600,6 @@
         // Attempt to use queryIntentActivities to get the ResolveInfo (with IntentFilter info) and
         // if that fails, or is ambiguious, fallback to the standard way of getting the resolve info
         // via resolveActivity().
-        final ShortcutInfo info = new ShortcutInfo();
         Bitmap icon = null;
         ResolveInfo resolveInfo = null;
         ComponentName oldComponent = intent.getComponent();
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) { }
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 4e95f09..97625f5 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -520,6 +520,7 @@
     }
 
     public long insertNewWorkspaceScreen(long screenId, int insertIndex, boolean updateDb) {
+        Log.d(TAG, "10249126 - insertNewWorkspaceScreen(" + screenId + ", " + insertIndex + ", " + updateDb + ")");
         CellLayout newScreen = (CellLayout)
                 mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null);
 
@@ -536,6 +537,7 @@
     }
 
     public void createCustomContentPage() {
+        Log.d(TAG, "10249126 - createCustomContentPage()");
         CellLayout customScreen = (CellLayout)
                 mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null);
 
@@ -570,6 +572,7 @@
     }
 
     public long commitExtraEmptyScreen() {
+        Log.d(TAG, "10249126 - commitExtraEmptyScreen()");
         CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID);
         mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
         mScreenOrder.remove(EXTRA_EMPTY_SCREEN_ID);
@@ -591,11 +594,13 @@
     }
 
     public CellLayout getScreenWithId(long screenId) {
+        Log.d(TAG, "10249126 - getScreenWithId(" + screenId + ")");
         CellLayout layout = mWorkspaceScreens.get(screenId);
         return layout;
     }
 
     public long getIdForScreen(CellLayout layout) {
+        Log.d(TAG, "10249126 - getIdForScreen()");
         Iterator<Long> iter = mWorkspaceScreens.keySet().iterator();
         while (iter.hasNext()) {
             long id = iter.next();
@@ -607,6 +612,7 @@
     }
 
     public int getPageIndexForScreenId(long screenId) {
+        Log.d(TAG, "10249126 - getPageIndexForScreenId(" + screenId + ")");
         return indexOfChild(mWorkspaceScreens.get(screenId));
     }
 
@@ -624,6 +630,8 @@
             return;
         }
 
+        Log.d(TAG, "10249126 - stripEmptyScreens()");
+
         int currentPage = getNextPage();
         ArrayList<Long> removeScreens = new ArrayList<Long>();
         for (Long id: mWorkspaceScreens.keySet()) {
@@ -635,6 +643,7 @@
 
         int pageShift = 0;
         for (Long id: removeScreens) {
+            Log.d(TAG, "10249126 - \tremove(" + id + ")");
             CellLayout cl = mWorkspaceScreens.get(id);
             mWorkspaceScreens.remove(id);
             mScreenOrder.remove(id);
@@ -693,6 +702,8 @@
         if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
             if (getScreenWithId(screenId) == null) {
                 Log.e(TAG, "Skipping child, screenId " + screenId + " not found");
+                // DEBUGGING - Print out the stack trace to see where we are adding from
+                new Throwable().printStackTrace();
                 return;
             }
         }
@@ -1413,6 +1424,7 @@
     }
 
     protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
         mWindowToken = null;
     }