merge in nakasi-factoryrom-release history after reset to jb-dev
diff --git a/proguard.flags b/proguard.flags
index 79378e6..39784b1 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -9,10 +9,12 @@
   public void *(android.view.MenuItem);
 }
 
-# Any class or method annotated with NeededForTesting.
+# Any class or method annotated with NeededForTesting or NeededForReflection.
 -keep @com.android.contacts.test.NeededForTesting class *
+-keep @com.android.contacts.test.NeededForReflection class *
 -keepclassmembers class * {
 @com.android.contacts.test.NeededForTesting *;
+@com.android.contacts.test.NeededForReflection *;
 }
 
 -verbose
diff --git a/res/drawable-hdpi/spinner_default_holo_dark.9.png b/res/drawable-hdpi/spinner_default_holo_dark.9.png
new file mode 100644
index 0000000..34a88df
--- /dev/null
+++ b/res/drawable-hdpi/spinner_default_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/spinner_default_holo_dark.9.png b/res/drawable-mdpi/spinner_default_holo_dark.9.png
new file mode 100644
index 0000000..48af192
--- /dev/null
+++ b/res/drawable-mdpi/spinner_default_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/spinner_default_holo_dark.9.png b/res/drawable-xhdpi/spinner_default_holo_dark.9.png
new file mode 100644
index 0000000..e94ce80
--- /dev/null
+++ b/res/drawable-xhdpi/spinner_default_holo_dark.9.png
Binary files differ
diff --git a/res/drawable/ab_dropdown_navigation_item_background.xml b/res/drawable/ab_dropdown_navigation_item_background.xml
new file mode 100644
index 0000000..c144996
--- /dev/null
+++ b/res/drawable/ab_dropdown_navigation_item_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true"
+        android:drawable="@drawable/ab_dropdown_navigation_item_background_pressed"/>
+    <item android:drawable="@drawable/spinner_default_holo_dark" />
+</selector>
diff --git a/res/drawable/ab_dropdown_navigation_item_background_pressed.xml b/res/drawable/ab_dropdown_navigation_item_background_pressed.xml
new file mode 100644
index 0000000..d075a05
--- /dev/null
+++ b/res/drawable/ab_dropdown_navigation_item_background_pressed.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<layer-list
+    xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:drawable="@drawable/action_bar_item_pressed_holo_light" />
+    <item android:drawable="@drawable/spinner_default_holo_dark" />
+</layer-list>
diff --git a/res/drawable/action_bar_item_background.xml b/res/drawable/action_bar_item_background.xml
index 8dd8d9b..7ccc3a9 100644
--- a/res/drawable/action_bar_item_background.xml
+++ b/res/drawable/action_bar_item_background.xml
@@ -14,8 +14,7 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-          android:exitFadeDuration="@android:integer/config_mediumAnimTime">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:state_pressed="true" android:drawable="@drawable/action_bar_item_pressed_holo_light"/>
     <item android:drawable="@android:color/transparent" />
 </selector>
diff --git a/res/drawable/group_list_item_background.xml b/res/drawable/group_list_item_background.xml
index 345117f..70cd308 100644
--- a/res/drawable/group_list_item_background.xml
+++ b/res/drawable/group_list_item_background.xml
@@ -14,8 +14,7 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:exitFadeDuration="@android:integer/config_mediumAnimTime">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:state_activated="true" android:drawable="@drawable/list_activated_holo" />
     <item android:state_pressed="true" android:drawable="@drawable/list_pressed_holo_light" />
     <item android:state_focused="true" android:drawable="@drawable/list_focused_holo" />
diff --git a/res/layout-sw580dp-w940dp/quickcontact_activity.xml b/res/layout-sw580dp-w940dp/quickcontact_activity.xml
new file mode 100644
index 0000000..5c38309
--- /dev/null
+++ b/res/layout-sw580dp-w940dp/quickcontact_activity.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.quickcontact.FloatingChildLayout"
+    android:id="@+id/floating_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:descendantFocusability="afterDescendants">
+    <LinearLayout
+        android:id="@android:id/content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="32dip"
+        android:orientation="horizontal">
+        <FrameLayout
+            android:layout_width="360dip"
+            android:layout_height="360dip">
+            <include layout="@layout/quickcontact_photo_container" />
+        </FrameLayout>
+        <LinearLayout
+            android:layout_width="360dip"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+            <include layout="@layout/quickcontact_track" />
+            <View
+                android:id="@+id/line_after_track"
+                android:layout_width="match_parent"
+                android:layout_height="2dip"
+                android:background="@color/quickcontact_tab_indicator" />
+            <android.support.v4.view.ViewPager
+                android:id="@+id/item_list_pager"
+                android:background="@color/quickcontact_list_background"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+        </LinearLayout>
+    </LinearLayout>
+</view>
diff --git a/res/layout-sw680dp-w1000dp/contact_detail_container.xml b/res/layout-sw680dp-w1000dp/contact_detail_container.xml
index 9e1eb33..8180e92 100644
--- a/res/layout-sw680dp-w1000dp/contact_detail_container.xml
+++ b/res/layout-sw680dp-w1000dp/contact_detail_container.xml
@@ -54,6 +54,7 @@
         android:id="@+id/updates_fragment_container"
         android:layout_width="0dip"
         android:layout_weight="2"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:visibility="gone" />
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout-sw680dp-w1000dp/quickcontact_activity.xml b/res/layout-sw680dp-w1000dp/quickcontact_activity.xml
new file mode 100644
index 0000000..5c38309
--- /dev/null
+++ b/res/layout-sw680dp-w1000dp/quickcontact_activity.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.quickcontact.FloatingChildLayout"
+    android:id="@+id/floating_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:descendantFocusability="afterDescendants">
+    <LinearLayout
+        android:id="@android:id/content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="32dip"
+        android:orientation="horizontal">
+        <FrameLayout
+            android:layout_width="360dip"
+            android:layout_height="360dip">
+            <include layout="@layout/quickcontact_photo_container" />
+        </FrameLayout>
+        <LinearLayout
+            android:layout_width="360dip"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+            <include layout="@layout/quickcontact_track" />
+            <View
+                android:id="@+id/line_after_track"
+                android:layout_width="match_parent"
+                android:layout_height="2dip"
+                android:background="@color/quickcontact_tab_indicator" />
+            <android.support.v4.view.ViewPager
+                android:id="@+id/item_list_pager"
+                android:background="@color/quickcontact_list_background"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+        </LinearLayout>
+    </LinearLayout>
+</view>
diff --git a/res/layout-sw680dp/quickcontact_activity.xml b/res/layout-sw680dp/quickcontact_activity.xml
new file mode 100644
index 0000000..8843fc9
--- /dev/null
+++ b/res/layout-sw680dp/quickcontact_activity.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:ex="http://schemas.android.com/apk/res/com.android.contacts"
+    class="com.android.contacts.quickcontact.FloatingChildLayout"
+    android:id="@+id/floating_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:descendantFocusability="afterDescendants">
+    <LinearLayout
+        android:id="@android:id/content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="32dip"
+        android:orientation="vertical" >
+        <FrameLayout
+            android:layout_width="360dip"
+            android:layout_height="@dimen/quick_contact_photo_container_height">
+            <include layout="@layout/quickcontact_photo_container" />
+        </FrameLayout>
+        <include layout="@layout/quickcontact_track" />
+        <View
+            android:id="@+id/line_after_track"
+            android:layout_width="match_parent"
+            android:layout_height="2dip"
+            android:background="@color/quickcontact_tab_indicator" />
+        <android.support.v4.view.ViewPager
+            android:id="@+id/item_list_pager"
+            android:layout_width="match_parent"
+            android:layout_height="160dip" />
+    </LinearLayout>
+</view>
diff --git a/res/layout/contact_detail_updates_fragment_container.xml b/res/layout/contact_detail_updates_fragment_container.xml
index fb3a8ac..7414f61 100644
--- a/res/layout/contact_detail_updates_fragment_container.xml
+++ b/res/layout/contact_detail_updates_fragment_container.xml
@@ -22,4 +22,5 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/updates_fragment_container"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"/>
\ No newline at end of file
+    android:layout_height="match_parent"
+    android:visibility="gone" />
diff --git a/res/values-sw580dp-w940dp/styles.xml b/res/values-sw580dp-w940dp/styles.xml
index 07ef912..d035240 100644
--- a/res/values-sw580dp-w940dp/styles.xml
+++ b/res/values-sw580dp-w940dp/styles.xml
@@ -35,6 +35,7 @@
         <item name="list_item_gap_between_label_and_data">5dip</item>
         <item name="list_item_vertical_divider_margin">5dip</item>
         <item name="list_item_presence_icon_margin">4dip</item>
+        <item name="list_item_presence_icon_size">16dip</item>
         <item name="list_item_photo_size">64dip</item>
         <item name="list_item_profile_photo_size">80dip</item>
         <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
diff --git a/res/values-sw580dp/styles.xml b/res/values-sw580dp/styles.xml
index 7a247ae..a8d935b 100644
--- a/res/values-sw580dp/styles.xml
+++ b/res/values-sw580dp/styles.xml
@@ -35,6 +35,7 @@
         <item name="list_item_gap_between_label_and_data">5dip</item>
         <item name="list_item_vertical_divider_margin">5dip</item>
         <item name="list_item_presence_icon_margin">4dip</item>
+        <item name="list_item_presence_icon_size">16dip</item>
         <item name="list_item_photo_size">64dip</item>
         <item name="list_item_profile_photo_size">80dip</item>
         <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
diff --git a/res/values-sw680dp/styles.xml b/res/values-sw680dp/styles.xml
index 3db3867..fb242d9 100644
--- a/res/values-sw680dp/styles.xml
+++ b/res/values-sw680dp/styles.xml
@@ -35,6 +35,7 @@
         <item name="list_item_gap_between_label_and_data">5dip</item>
         <item name="list_item_vertical_divider_margin">5dip</item>
         <item name="list_item_presence_icon_margin">4dip</item>
+        <item name="list_item_presence_icon_size">16dip</item>
         <item name="list_item_photo_size">64dip</item>
         <item name="list_item_profile_photo_size">80dip</item>
         <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 93c130f..e24a7c3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1573,6 +1573,9 @@
         <item quantity="other"><xliff:g id="count">%1$d</xliff:g> Voicemails</item>
     </plurals>
 
+    <!-- Used in the notification of a new voicemail for the action to play the voicemail. -->
+    <string name="notification_action_voicemail_play">Play</string>
+
     <!-- Used to build a list of names or phone numbers, to indicate the callers who left
          voicemails.
          The first argument may be one or more callers, the most recent ones.
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1702585..829b207 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -176,6 +176,7 @@
     </style>
 
     <style name="ContactsActionBarDropDownStyle" parent="@android:style/Widget.Holo.Light.Spinner">
+        <item name="android:background">@drawable/ab_dropdown_navigation_item_background</item>
     </style>
 
     <style name="ContactsActionBarTheme" parent="@android:style/Theme.Holo">
@@ -351,14 +352,10 @@
     <style name="PeopleNavigationDropDownTextAppearance">
         <item name="android:textColor">#333333</item>
         <item name="android:textSize">18sp</item>
-        <item name="android:textStyle">normal</item>
-        <item name="android:textAllCaps">false</item>
     </style>
 
     <style name="PeopleNavigationDropDownHeaderTextAppearance">
-        <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
-        <item name="android:textSize">14sp</item>
-        <item name="android:textStyle">bold</item>
-        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">#cdffffff</item>
+        <item name="android:textSize">18sp</item>
     </style>
 </resources>
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 1fac6c8..47441ce 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -97,6 +97,8 @@
     public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
     /** If we should immediately start playback of the voicemail, this extra will be set to true. */
     public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK";
+    /** If the activity was triggered from a notification. */
+    public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
 
     private CallTypeHelper mCallTypeHelper;
     private PhoneNumberHelper mPhoneNumberHelper;
@@ -273,6 +275,9 @@
         mContactInfoHelper = new ContactInfoHelper(this, ContactsUtils.getCurrentCountryIso(this));
         configureActionBar();
         optionallyHandleVoicemail();
+        if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
+            closeSystemDialogs();
+        }
     }
 
     @Override
@@ -879,6 +884,10 @@
         mPhoneNumberActionMode = startActionMode(new PhoneNumberActionModeCallback(targetView));
     }
 
+    private void closeSystemDialogs() {
+        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+    }
+
     private class PhoneNumberActionModeCallback implements ActionMode.Callback {
         private final View mTargetView;
         private final Drawable mOriginalViewBackground;
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 0502d4d..075c59f 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -150,7 +150,7 @@
 
     private ContactsUnavailableFragment mContactsUnavailableFragment;
     private ProviderStatusWatcher mProviderStatusWatcher;
-    private int mProviderStatus;
+    private ProviderStatusWatcher.Status mProviderStatus;
 
     private boolean mOptionsMenuContactsAvailable;
 
@@ -217,7 +217,8 @@
     }
 
     public boolean areContactsAvailable() {
-        return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+        return (mProviderStatus != null)
+                && mProviderStatus.status == ProviderStatus.STATUS_NORMAL;
     }
 
     private boolean areContactWritableAccountsAvailable() {
@@ -991,14 +992,15 @@
     }
 
     private void updateViewConfiguration(boolean forceUpdate) {
-        int providerStatus = mProviderStatusWatcher.getProviderStatus();
-        if (!forceUpdate && (providerStatus == mProviderStatus)) return;
+        ProviderStatusWatcher.Status providerStatus = mProviderStatusWatcher.getProviderStatus();
+        if (!forceUpdate && (mProviderStatus != null)
+                && (providerStatus.status == mProviderStatus.status)) return;
         mProviderStatus = providerStatus;
 
         View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view);
         View mainView = findViewById(R.id.main_view);
 
-        if (mProviderStatus == ProviderStatus.STATUS_NORMAL) {
+        if (mProviderStatus.status == ProviderStatus.STATUS_NORMAL) {
             // Ensure that the mTabPager is visible; we may have made it invisible below.
             contactsUnavailableView.setVisibility(View.GONE);
             if (mTabPager != null) {
@@ -1033,9 +1035,8 @@
                 getFragmentManager().beginTransaction()
                         .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment)
                         .commitAllowingStateLoss();
-            } else {
-                mContactsUnavailableFragment.update();
             }
+            mContactsUnavailableFragment.updateStatus(mProviderStatus);
 
             // Show the contactsUnavailableView, and hide the mTabPager so that we don't
             // see it sliding in underneath the contactsUnavailableView at the edges.
diff --git a/src/com/android/contacts/calllog/DefaultVoicemailNotifier.java b/src/com/android/contacts/calllog/DefaultVoicemailNotifier.java
index 59dfcd4..1c2a595 100644
--- a/src/com/android/contacts/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/contacts/calllog/DefaultVoicemailNotifier.java
@@ -152,14 +152,13 @@
         // TODO: Use the photo of contact if all calls are from the same person.
         final int icon = android.R.drawable.stat_notify_voicemail;
 
-        Notification notification = new Notification.Builder(mContext)
+        Notification.Builder notificationBuilder = new Notification.Builder(mContext)
                 .setSmallIcon(icon)
                 .setContentTitle(title)
                 .setContentText(callers)
                 .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
                 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
-                .setAutoCancel(true)
-                .getNotification();
+                .setAutoCancel(true);
 
         // Determine the intent to fire when the notification is clicked on.
         final Intent contentIntent;
@@ -169,19 +168,29 @@
             contentIntent.setData(newCalls[0].callsUri);
             contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
                     newCalls[0].voicemailUri);
+            Intent playIntent = new Intent(mContext, CallDetailActivity.class);
+            playIntent.setData(newCalls[0].callsUri);
+            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+                    newCalls[0].voicemailUri);
+            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+            playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true);
+            notificationBuilder.addAction(R.drawable.ic_play_holo_dark,
+                    resources.getString(R.string.notification_action_voicemail_play),
+                    PendingIntent.getActivity(mContext, 0, playIntent, 0));
         } else {
             // Open the call log.
             contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
         }
-        notification.contentIntent = PendingIntent.getActivity(mContext, 0, contentIntent, 0);
+        notificationBuilder.setContentIntent(
+                PendingIntent.getActivity(mContext, 0, contentIntent, 0));
 
         // The text to show in the ticker, describing the new event.
         if (callToNotify != null) {
-            notification.tickerText = resources.getString(
-                    R.string.notification_new_voicemail_ticker, names.get(callToNotify.number));
+            notificationBuilder.setTicker(resources.getString(
+                    R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
         }
 
-        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
     }
 
     /** Creates a pending intent that marks all new voicemails as old. */
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index 89585e8..745c044 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -73,6 +73,7 @@
 import com.android.contacts.activities.DialtactsActivity;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.PhoneNumberFormatter;
+import com.android.contacts.util.StopWatch;
 import com.android.internal.telephony.ITelephony;
 import com.android.phone.CallLogAsync;
 import com.android.phone.HapticFeedback;
@@ -468,17 +469,25 @@
     public void onResume() {
         super.onResume();
 
+        final StopWatch stopWatch = StopWatch.start("Dialpad.onResume");
+
         // Query the last dialed number. Do it first because hitting
         // the DB is 'slow'. This call is asynchronous.
         queryLastOutgoingCall();
 
+        stopWatch.lap("qloc");
+
         // retrieve the DTMF tone play back setting.
         mDTMFToneEnabled = Settings.System.getInt(getActivity().getContentResolver(),
                 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
 
+        stopWatch.lap("dtwd");
+
         // Retrieve the haptic feedback setting.
         mHaptic.checkSystemSetting();
 
+        stopWatch.lap("hptc");
+
         // if the mToneGenerator creation fails, just continue without it.  It is
         // a local audio signal, and is not as important as the dtmf tone itself.
         synchronized (mToneGeneratorLock) {
@@ -491,6 +500,7 @@
                 }
             }
         }
+        stopWatch.lap("tg");
         // Prevent unnecessary confusion. Reset the press count anyway.
         mDialpadPressCount = 0;
 
@@ -501,6 +511,8 @@
             fillDigitsIfNecessary(parent.getIntent());
         }
 
+        stopWatch.lap("fdin");
+
         // While we're in the foreground, listen for phone state changes,
         // purely so that we can take down the "dialpad chooser" if the
         // phone becomes idle while the chooser UI is visible.
@@ -508,6 +520,8 @@
                 (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
         telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
 
+        stopWatch.lap("tm");
+
         // Potentially show hint text in the mDigits field when the user
         // hasn't typed any digits yet.  (If there's already an active call,
         // this hint text will remind the user that he's about to add a new
@@ -531,7 +545,13 @@
             showDialpadChooser(false);
         }
 
+        stopWatch.lap("hnt");
+
         updateDialAndDeleteButtonEnabledState();
+
+        stopWatch.lap("bes");
+
+        stopWatch.stopAndLog(TAG, 50);
     }
 
     @Override
diff --git a/src/com/android/contacts/list/ContactsUnavailableFragment.java b/src/com/android/contacts/list/ContactsUnavailableFragment.java
index becc704..b645425 100644
--- a/src/com/android/contacts/list/ContactsUnavailableFragment.java
+++ b/src/com/android/contacts/list/ContactsUnavailableFragment.java
@@ -18,6 +18,7 @@
 import com.android.contacts.R;
 
 import android.app.Fragment;
+import android.content.Context;
 import android.os.Bundle;
 import android.provider.ContactsContract.ProviderStatus;
 import android.view.Gravity;
@@ -35,8 +36,6 @@
  */
 public class ContactsUnavailableFragment extends Fragment implements OnClickListener {
 
-    private ProviderStatusWatcher mProviderStatusWatcher;
-
     private View mView;
     private TextView mMessageView;
     private TextView mSecondaryMessageView;
@@ -51,10 +50,11 @@
 
     private OnContactsUnavailableActionListener mListener;
 
+    private ProviderStatusWatcher.Status mProviderStatus;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(getActivity());
     }
 
     @Override
@@ -74,7 +74,11 @@
         mRetryUpgradeButton = (Button) mView.findViewById(R.id.import_failure_retry_button);
         mRetryUpgradeButton.setOnClickListener(this);
         mProgress = (ProgressBar) mView.findViewById(R.id.progress);
-        update();
+
+        if (mProviderStatus != null) {
+            updateStatus(mProviderStatus);
+        }
+
         return mView;
     }
 
@@ -83,9 +87,13 @@
         mListener = listener;
     }
 
-    public void update() {
-        int providerStatus = mProviderStatusWatcher.getProviderStatus();
-        switch (providerStatus) {
+    public void updateStatus(ProviderStatusWatcher.Status providerStatus) {
+        mProviderStatus = providerStatus;
+        if (mView == null) {
+            // The view hasn't been inflated yet.
+            return;
+        }
+        switch (providerStatus.status) {
             case ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS:
                 setMessageText(mNoContactsMsgResId, mNSecNoContactsMsgResId);
                 mCreateContactButton.setVisibility(View.VISIBLE);
@@ -122,7 +130,7 @@
 
             case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
                 String message = getResources().getString(R.string.upgrade_out_of_memory,
-                        new Object[] { mProviderStatusWatcher.getProviderStatusData() });
+                        new Object[] { providerStatus.data});
                 mMessageView.setText(message);
                 mMessageView.setGravity(Gravity.LEFT);
                 mMessageView.setVisibility(View.VISIBLE);
@@ -155,7 +163,10 @@
                 mListener.onFreeInternalStorageAction();
                 break;
             case R.id.import_failure_retry_button:
-                mProviderStatusWatcher.retryUpgrade();
+                final Context context = getActivity();
+                if (context != null) { // Just in case.
+                    ProviderStatusWatcher.retryUpgrade(context);
+                }
                 break;
         }
     }
@@ -167,9 +178,8 @@
     public void setMessageText(int resId, int secResId) {
         mNoContactsMsgResId = resId;
         mNSecNoContactsMsgResId = secResId;
-        if (mMessageView != null &&
-                mProviderStatusWatcher.getProviderStatus() ==
-                    ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
+        if ((mMessageView != null) && (mProviderStatus != null) &&
+                (mProviderStatus.status == ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS)) {
             if (resId != -1) {
                 mMessageView.setText(mNoContactsMsgResId);
                 mMessageView.setGravity(Gravity.CENTER_HORIZONTAL);
diff --git a/src/com/android/contacts/list/ProviderStatusWatcher.java b/src/com/android/contacts/list/ProviderStatusWatcher.java
index 3ce4b78..ae0b779 100644
--- a/src/com/android/contacts/list/ProviderStatusWatcher.java
+++ b/src/com/android/contacts/list/ProviderStatusWatcher.java
@@ -47,6 +47,19 @@
         public void onProviderStatusChange();
     }
 
+    public static class Status {
+        /** See {@link ProviderStatus#STATUS} */
+        public final int status;
+
+        /** See {@link ProviderStatus#DATA1} */
+        public final String data;
+
+        public Status(int status, String data) {
+            this.status = status;
+            this.data = data;
+        }
+    }
+
     private static final String[] PROJECTION = new String[] {
         ProviderStatus.STATUS,
         ProviderStatus.DATA1
@@ -57,8 +70,6 @@
      */
     private static final int LOAD_WAIT_TIMEOUT_MS = 1000;
 
-    private static final int STATUS_UNKNOWN = -1;
-
     private static ProviderStatusWatcher sInstance;
 
     private final Context mContext;
@@ -71,10 +82,7 @@
     private LoaderTask mLoaderTask;
 
     /** Last known provider status.  This can be changed on a worker thread. */
-    private int mProviderStatus = STATUS_UNKNOWN;
-
-    /** Last known provider status data.  This can be changed on a worker thread. */
-    private String mProviderData;
+    private Status mProviderStatus;
 
     private final ArrayList<ProviderStatusListener> mListeners = Lists.newArrayList();
 
@@ -177,32 +185,18 @@
      * (If {@link ProviderStatus#STATUS_UPGRADING} is returned, the app (should) shows an according
      * message, like "contacts are being updated".)
      */
-    public int getProviderStatus() {
+    public Status getProviderStatus() {
         waitForLoaded();
 
-        if (mProviderStatus == STATUS_UNKNOWN) {
-            return ProviderStatus.STATUS_UPGRADING;
+        if (mProviderStatus == null) {
+            return new Status(ProviderStatus.STATUS_UPGRADING, null);
         }
 
         return mProviderStatus;
     }
 
-    /**
-     * @return last known provider status data.  See also {@link #getProviderStatus()}.
-     */
-    public String getProviderStatusData() {
-        waitForLoaded();
-
-        if (mProviderStatus == STATUS_UNKNOWN) {
-            // STATUS_UPGRADING has no data.
-            return "";
-        }
-
-        return mProviderData;
-    }
-
     private void waitForLoaded() {
-        if (mProviderStatus == STATUS_UNKNOWN) {
+        if (mProviderStatus == null) {
             if (mLoaderTask == null) {
                 // For some reason the loader couldn't load the status.  Let's start it again.
                 startLoading();
@@ -238,8 +232,10 @@
                 if (cursor != null) {
                     try {
                         if (cursor.moveToFirst()) {
-                            mProviderStatus = cursor.getInt(0);
-                            mProviderData = cursor.getString(1);
+                            // Note here we can't just say "Status", as AsyncTask has the "Status"
+                            // enum too.
+                            mProviderStatus = new ProviderStatusWatcher.Status(
+                                    cursor.getInt(0), cursor.getString(1));
                             return true;
                         }
                     } finally {
@@ -291,14 +287,14 @@
     /**
      * Sends a provider status update, which will trigger a retry of database upgrade
      */
-    public void retryUpgrade() {
+    public static void retryUpgrade(final Context context) {
         Log.i(TAG, "retryUpgrade");
         final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
                 ContentValues values = new ContentValues();
                 values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
-                mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values,
+                context.getContentResolver().update(ProviderStatus.CONTENT_URI, values,
                         null, null);
                 return null;
             }
diff --git a/src/com/android/contacts/quickcontact/FloatingChildLayout.java b/src/com/android/contacts/quickcontact/FloatingChildLayout.java
index dca738c..20a3c1e 100644
--- a/src/com/android/contacts/quickcontact/FloatingChildLayout.java
+++ b/src/com/android/contacts/quickcontact/FloatingChildLayout.java
@@ -17,15 +17,17 @@
 package com.android.contacts.quickcontact;
 
 import com.android.contacts.R;
+import com.android.contacts.test.NeededForReflection;
+import com.android.contacts.util.SchedulingUtils;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.TransitionDrawable;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -53,11 +55,13 @@
     private View mChild;
     private Rect mTargetScreen = new Rect();
     private final int mAnimationDuration;
-    private final TransitionDrawable mBackground;
 
     /** The phase of the background dim. This is one of the values of {@link BackgroundPhase}  */
     private int mBackgroundPhase = BackgroundPhase.BEFORE;
 
+    private ObjectAnimator mBackgroundAnimator = ObjectAnimator.ofInt(this,
+            "backgroundColorAlpha", 0, DIM_BACKGROUND_ALPHA);
+
     private interface BackgroundPhase {
         public static final int BEFORE = 0;
         public static final int APPEARING_OR_VISIBLE = 1;
@@ -76,7 +80,7 @@
     }
 
     // Black, 50% alpha as per the system default.
-    private static final int DIM_BACKGROUND_COLOR = 0x7F000000;
+    private static final int DIM_BACKGROUND_ALPHA = 0x7F;
 
     public FloatingChildLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -85,10 +89,7 @@
                 resources.getDimensionPixelOffset(R.dimen.quick_contact_top_position);
         mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime);
 
-        final ColorDrawable[] drawables =
-            { new ColorDrawable(0), new ColorDrawable(DIM_BACKGROUND_COLOR) };
-        mBackground = new TransitionDrawable(drawables);
-        super.setBackground(mBackground);
+        super.setBackground(new ColorDrawable(0));
     }
 
     @Override
@@ -178,17 +179,35 @@
         child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
     }
 
+    @NeededForReflection
+    public void setBackgroundColorAlpha(int alpha) {
+        setBackgroundColor(alpha << 24);
+    }
+
     public void fadeInBackground() {
         if (mBackgroundPhase == BackgroundPhase.BEFORE) {
             mBackgroundPhase = BackgroundPhase.APPEARING_OR_VISIBLE;
-            mBackground.startTransition(mAnimationDuration);
+
+            createChildLayer();
+
+            SchedulingUtils.doAfterDraw(this, new Runnable() {
+                @Override
+                public void run() {
+                    mBackgroundAnimator.setDuration(mAnimationDuration).start();
+                }
+            });
         }
     }
 
     public void fadeOutBackground() {
         if (mBackgroundPhase == BackgroundPhase.APPEARING_OR_VISIBLE) {
             mBackgroundPhase = BackgroundPhase.DISAPPEARING_OR_GONE;
-            mBackground.reverseTransition(mAnimationDuration);
+            if (mBackgroundAnimator.isRunning()) {
+                mBackgroundAnimator.reverse();
+            } else {
+                ObjectAnimator.ofInt(this, "backgroundColorAlpha", DIM_BACKGROUND_ALPHA, 0).
+                        setDuration(mAnimationDuration).start();
+            }
         }
     }
 
@@ -212,6 +231,9 @@
         if (mForegroundPhase == ForegroundPhase.APPEARING ||
                 mForegroundPhase == ForegroundPhase.IDLE) {
             mForegroundPhase = ForegroundPhase.DISAPPEARING;
+
+            createChildLayer();
+
             animateScale(true, onAnimationEndRunnable);
             return true;
         } else {
@@ -219,6 +241,12 @@
         }
     }
 
+    private void createChildLayer() {
+        mChild.invalidate();
+        mChild.setLayerType(LAYER_TYPE_HARDWARE, null);
+        mChild.buildLayer();
+    }
+
     /** Creates the open/close animation */
     private void animateScale(
             final boolean isExitAnimation,
@@ -231,7 +259,7 @@
                 : android.R.interpolator.decelerate_quint;
         final float scaleTarget = isExitAnimation ? 0.5f : 1.0f;
 
-        mChild.animate().withLayer()
+        mChild.animate()
                 .setDuration(mAnimationDuration)
                 .setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator))
                 .scaleX(scaleTarget)
@@ -240,6 +268,7 @@
                 .setListener(new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationEnd(Animator animation) {
+                        mChild.setLayerType(LAYER_TYPE_NONE, null);
                         if (isExitAnimation) {
                             if (mForegroundPhase == ForegroundPhase.DISAPPEARING) {
                                 mForegroundPhase = ForegroundPhase.AFTER;
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 3c91716..9c71ee2 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -25,6 +25,7 @@
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.ImageViewDrawableSetter;
 import com.android.contacts.util.SchedulingUtils;
+import com.android.contacts.util.StopWatch;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
@@ -91,6 +92,8 @@
     private static final boolean TRACE_LAUNCH = false;
     private static final String TRACE_TAG = "quickcontact";
     private static final int POST_DRAW_WAIT_DURATION = 60;
+    private static final boolean ENABLE_STOPWATCH = false;
+
 
     @SuppressWarnings("deprecation")
     private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
@@ -148,18 +151,49 @@
     /** Id for the background loader */
     private static final int LOADER_ID = 0;
 
+    private StopWatch mStopWatch = ENABLE_STOPWATCH
+            ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch();
+
     @Override
     protected void onCreate(Bundle icicle) {
+        mStopWatch.lap("c"); // create start
         super.onCreate(icicle);
 
+        mStopWatch.lap("sc"); // super.onCreate
+
         if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG);
 
+        // Parse intent
+        final Intent intent = getIntent();
+
+        Uri lookupUri = intent.getData();
+
+        // Check to see whether it comes from the old version.
+        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
+            final long rawContactId = ContentUris.parseId(lookupUri);
+            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+        }
+
+        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
+
+        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
+
+        mStopWatch.lap("i"); // intent parsed
+
+        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
+                LOADER_ID, null, mLoaderCallbacks);
+
+        mStopWatch.lap("ld"); // loader started
+
         // Show QuickContact in front of soft input
         getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
 
         setContentView(R.layout.quickcontact_activity);
 
+        mStopWatch.lap("l"); // layout inflated
+
         mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
         mTrack = (ViewGroup) findViewById(R.id.track);
         mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
@@ -192,35 +226,25 @@
         mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
         mListPager.setOnPageChangeListener(new PageChangeListener());
 
-        final Intent intent = getIntent();
-
-        Uri lookupUri = intent.getData();
-
-        // Check to see whether it comes from the old version.
-        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
-            final long rawContactId = ContentUris.parseId(lookupUri);
-            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
-                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
-        }
-
-        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
-
-        // Read requested parameters for displaying
         final Rect sourceBounds = intent.getSourceBounds();
         if (sourceBounds != null) {
             mFloatingLayout.setChildTargetScreen(sourceBounds);
         }
 
-        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
-
         // find and prepare correct header view
         mPhotoContainer = findViewById(R.id.photo_container);
         setHeaderNameText(R.id.name, R.string.missing_name);
 
-        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
-                LOADER_ID, null, mLoaderCallbacks);
+        mStopWatch.lap("v"); // view initialized
 
-        mFloatingLayout.fadeInBackground();
+        SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
+            @Override
+            public void run() {
+                mFloatingLayout.fadeInBackground();
+            }
+        });
+
+        mStopWatch.lap("cf"); // onCreate finished
     }
 
     private void handleOutsideTouch() {
@@ -315,11 +339,16 @@
 
         mDefaultsMap.clear();
 
+        mStopWatch.lap("atm"); // AccountTypeManager initialization start
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
                 context.getApplicationContext());
+        mStopWatch.lap("fatm"); // AccountTypeManager initialization finished
+
         final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
         mPhotoSetter.setupContactPhoto(data, photoView);
 
+        mStopWatch.lap("ph"); // Photo set
+
         for (Entity entity : data.getEntities()) {
             final ContentValues entityValues = entity.getEntityValues();
             final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
@@ -370,11 +399,15 @@
             }
         }
 
+        mStopWatch.lap("e"); // Entities inflated
+
         // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
         for (List<Action> actionChildren : mActions.values()) {
             Collapser.collapseList(actionChildren);
         }
 
+        mStopWatch.lap("c"); // List collapsed
+
         setHeaderNameText(R.id.name, data.getDisplayName());
 
         // All the mime-types to add.
@@ -404,6 +437,8 @@
             }
         }
 
+        mStopWatch.lap("mt"); // Mime types initialized
+
         // Add buttons for each mimetype
         mTrack.removeAllViews();
         for (String mimeType : mSortedActionMimeTypes) {
@@ -411,6 +446,8 @@
             mTrack.addView(actionView);
         }
 
+        mStopWatch.lap("mt"); // Buttons added
+
         final boolean hasData = !mSortedActionMimeTypes.isEmpty();
         mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
         mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
@@ -476,6 +513,7 @@
 
         @Override
         public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+            mStopWatch.lap("lf"); // onLoadFinished
             if (isFinishing()) {
                 close(false);
                 return;
@@ -495,6 +533,8 @@
 
             bindData(data);
 
+            mStopWatch.lap("bd"); // bindData finished
+
             if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
             if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
                 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
@@ -513,6 +553,8 @@
                     });
                 }
             });
+            mStopWatch.stopAndLog(TAG, 0);
+            mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
         }
 
         @Override
diff --git a/src/com/android/contacts/test/NeededForReflection.java b/src/com/android/contacts/test/NeededForReflection.java
new file mode 100644
index 0000000..feacca5
--- /dev/null
+++ b/src/com/android/contacts/test/NeededForReflection.java
@@ -0,0 +1,30 @@
+/*
+ * 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.contacts.test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the class, constructor, method or field is used by tests and therefore cannot be
+ * removed by tools like ProGuard.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
+public @interface NeededForReflection{}
diff --git a/src/com/android/contacts/util/StopWatch.java b/src/com/android/contacts/util/StopWatch.java
new file mode 100644
index 0000000..1accfe2
--- /dev/null
+++ b/src/com/android/contacts/util/StopWatch.java
@@ -0,0 +1,110 @@
+/*
+ * 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.contacts.util;
+
+import com.google.android.collect.Lists;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link StopWatch} records start, laps and stop, and print them to logcat.
+ */
+public class StopWatch {
+
+    private final String mLabel;
+
+    private final ArrayList<Long> mTimes = Lists.newArrayList();
+    private final ArrayList<String> mLapLabels = Lists.newArrayList();
+
+    private StopWatch(String label) {
+        mLabel = label;
+        lap("");
+    }
+
+    /**
+     * Create a new instance and start it.
+     */
+    public static StopWatch start(String label) {
+        return new StopWatch(label);
+    }
+
+    /**
+     * Record a lap.
+     */
+    public void lap(String lapLabel) {
+        mTimes.add(System.currentTimeMillis());
+        mLapLabels.add(lapLabel);
+    }
+
+    /**
+     * Stop it and log the result, if the total time >= {@code timeThresholdToLog}.
+     */
+    public void stopAndLog(String TAG, int timeThresholdToLog) {
+
+        lap("");
+
+        final long start = mTimes.get(0);
+        final long stop = mTimes.get(mTimes.size() - 1);
+
+        final long total = stop - start;
+        if (total < timeThresholdToLog) return;
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append(mLabel);
+        sb.append(",");
+        sb.append(total);
+        sb.append(": ");
+
+        long last = start;
+        for (int i = 1; i < mTimes.size(); i++) {
+            final long current = mTimes.get(i);
+            sb.append(mLapLabels.get(i));
+            sb.append(",");
+            sb.append((current - last));
+            sb.append(" ");
+            last = current;
+        }
+        Log.v(TAG, sb.toString());
+    }
+
+    /**
+     * Return a dummy instance that does no operations.
+     */
+    public static StopWatch getNullStopWatch() {
+        return NullStopWatch.INSTANCE;
+    }
+
+    private static class NullStopWatch extends StopWatch {
+        public static final NullStopWatch INSTANCE = new NullStopWatch();
+
+        public NullStopWatch() {
+            super(null);
+        }
+
+        @Override
+        public void lap(String lapLabel) {
+            // noop
+        }
+
+        @Override
+        public void stopAndLog(String TAG, int timeThresholdToLog) {
+            // noop
+        }
+    }
+}