Merge "Remove the work badge icon when a contact in search bar is a Google caller id" into ub-contactsdialer-b-dev
diff --git a/src/com/android/contacts/common/activity/AppCompatPreferenceActivity.java b/src/com/android/contacts/common/activity/AppCompatPreferenceActivity.java
new file mode 100644
index 0000000..c3b7e94
--- /dev/null
+++ b/src/com/android/contacts/common/activity/AppCompatPreferenceActivity.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2015 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.common.activity;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public class AppCompatPreferenceActivity extends PreferenceActivity {
+    private AppCompatDelegate mDelegate;
+
+    private boolean mIsSafeToCommitTransactions;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        getDelegate().installViewFactory();
+        getDelegate().onCreate(savedInstanceState);
+        super.onCreate(savedInstanceState);
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        getDelegate().onPostCreate(savedInstanceState);
+    }
+
+    public ActionBar getSupportActionBar() {
+        return getDelegate().getSupportActionBar();
+    }
+
+    public void setSupportActionBar(Toolbar toolbar) {
+        getDelegate().setSupportActionBar(toolbar);
+    }
+
+    @Override
+    public MenuInflater getMenuInflater() {
+        return getDelegate().getMenuInflater();
+    }
+
+    @Override
+    public void setContentView(int layoutResID) {
+        getDelegate().setContentView(layoutResID);
+    }
+
+    @Override
+    public void setContentView(View view) {
+        getDelegate().setContentView(view);
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().setContentView(view, params);
+    }
+
+    @Override
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().addContentView(view, params);
+    }
+
+    @Override
+    protected void onPostResume() {
+        super.onPostResume();
+        getDelegate().onPostResume();
+    }
+
+    @Override
+    protected void onTitleChanged(CharSequence title, int color) {
+        super.onTitleChanged(title, color);
+        getDelegate().setTitle(title);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        getDelegate().onConfigurationChanged(newConfig);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        getDelegate().onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        getDelegate().onDestroy();
+    }
+
+    @Override
+    public void invalidateOptionsMenu() {
+        getDelegate().invalidateOptionsMenu();
+    }
+
+    private AppCompatDelegate getDelegate() {
+        if (mDelegate == null) {
+            mDelegate = AppCompatDelegate.create(this, null);
+        }
+        return mDelegate;
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mIsSafeToCommitTransactions = false;
+    }
+
+    /**
+     * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+     * whether {@link Activity#onSaveInstanceState} has been called or not.
+     *
+     * Make sure that the current activity calls into
+     * {@link super.onSaveInstanceState(Bundle outState)} (if that method is overridden),
+     * so the flag is properly set.
+     */
+    public boolean isSafeToCommitTransactions() {
+        return mIsSafeToCommitTransactions;
+    }
+}
diff --git a/src/com/android/contacts/common/activity/LicenseActivity.java b/src/com/android/contacts/common/activity/LicenseActivity.java
index d72b305..9e86ee8 100644
--- a/src/com/android/contacts/common/activity/LicenseActivity.java
+++ b/src/com/android/contacts/common/activity/LicenseActivity.java
@@ -17,7 +17,8 @@
 
 import com.android.contacts.common.R;
 
-import android.app.Activity;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
 import android.os.Bundle;
 import android.view.MenuItem;
 import android.webkit.WebView;
@@ -25,7 +26,7 @@
 /**
  * Displays the licenses for all open source libraries.
  */
-public class LicenseActivity extends Activity {
+public class LicenseActivity extends AppCompatActivity {
     private static final String LICENSE_FILE = "file:///android_asset/licenses.html";
     private WebView mWebView;
 
@@ -35,6 +36,10 @@
         setContentView(R.layout.licenses);
         mWebView = (WebView) findViewById(R.id.webview);
         mWebView.loadUrl(LICENSE_FILE);
+        final ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
     }
 
     @Override
diff --git a/src/com/android/contacts/common/compat/TelephonyManagerCompat.java b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
index 0f55c48..ec7907f 100644
--- a/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
+++ b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
@@ -16,10 +16,13 @@
 
 package com.android.contacts.common.compat;
 
-import android.content.Context;
+import android.net.Uri;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 
+import com.android.contacts.common.ContactsUtils;
+
 public class TelephonyManagerCompat {
     public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager";
 
@@ -131,4 +134,40 @@
         }
         return false;
     }
+
+    /**
+     * Returns the URI for the per-account voicemail ringtone set in Phone settings.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+     * retrieve the voicemail ringtone.
+     * @return The URI for the ringtone to play when receiving a voicemail from a specific
+     * PhoneAccount.
+     */
+    @Nullable
+    public static Uri getVoicemailRingtoneUri(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        if (!CompatUtils.isNCompatible()) {
+            return null;
+        }
+        return TelephonyManagerSdkCompat
+                .getVoicemailRingtoneUri(telephonyManager, accountHandle);
+    }
+
+    /**
+     * Returns whether vibration is set for voicemail notification in Phone settings.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+     * retrieve the voicemail vibration setting.
+     * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise.
+     */
+    public static boolean isVoicemailVibrationEnabled(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        if (!CompatUtils.isNCompatible()) {
+            return true;
+        }
+        return TelephonyManagerSdkCompat
+                .isVoicemailVibrationEnabled(telephonyManager, accountHandle);
+    }
 }
diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java
index d68788c..a2fb651 100644
--- a/src/com/android/contacts/common/list/ContactListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -23,11 +23,9 @@
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.SearchSnippets;
 import android.text.TextUtils;
-import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ListView;
 
-import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.R;
 import com.android.contacts.common.compat.ContactsCompat;
@@ -72,7 +70,9 @@
             Contacts.PHOTO_THUMBNAIL_URI,           // 5
             Contacts.LOOKUP_KEY,                    // 6
             Contacts.IS_USER_PROFILE,               // 7
-            SearchSnippets.SNIPPET,           // 8
+            Contacts.TIMES_CONTACTED,               // 8
+            Contacts.STARRED,                       // 9
+            SearchSnippets.SNIPPET,                 // 10
         };
 
         private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
@@ -84,7 +84,9 @@
             Contacts.PHOTO_THUMBNAIL_URI,           // 5
             Contacts.LOOKUP_KEY,                    // 6
             Contacts.IS_USER_PROFILE,               // 7
-            SearchSnippets.SNIPPET,           // 8
+            Contacts.TIMES_CONTACTED,               // 8
+            Contacts.STARRED,                       // 9
+            SearchSnippets.SNIPPET,                 // 10
         };
 
         public static final int CONTACT_ID               = 0;
@@ -95,7 +97,52 @@
         public static final int CONTACT_PHOTO_URI        = 5;
         public static final int CONTACT_LOOKUP_KEY       = 6;
         public static final int CONTACT_IS_USER_PROFILE  = 7;
-        public static final int CONTACT_SNIPPET          = 8;
+        public static final int CONTACT_TIMES_CONTACTED  = 8;
+        public static final int CONTACT_STARRED          = 9;
+        public static final int CONTACT_SNIPPET          = 10;
+    }
+
+    protected static class StrequentQuery {
+
+        private static final String[] FILTER_PROJECTION_PRIMARY = new String[] {
+                Contacts._ID,                           // 0
+                Contacts.DISPLAY_NAME_PRIMARY,          // 1
+                Contacts.CONTACT_PRESENCE,              // 2
+                Contacts.CONTACT_STATUS,                // 3
+                Contacts.PHOTO_ID,                      // 4
+                Contacts.PHOTO_THUMBNAIL_URI,           // 5
+                Contacts.LOOKUP_KEY,                    // 6
+                Contacts.IS_USER_PROFILE,               // 7
+                Contacts.TIMES_CONTACTED,               // 8
+                Contacts.STARRED,                       // 9
+                // SearchSnippets.SNIPPET not supported
+        };
+
+        private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
+                Contacts._ID,                           // 0
+                Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+                Contacts.CONTACT_PRESENCE,              // 2
+                Contacts.CONTACT_STATUS,                // 3
+                Contacts.PHOTO_ID,                      // 4
+                Contacts.PHOTO_THUMBNAIL_URI,           // 5
+                Contacts.LOOKUP_KEY,                    // 6
+                Contacts.IS_USER_PROFILE,               // 7
+                Contacts.TIMES_CONTACTED,               // 8
+                Contacts.STARRED,                       // 9
+                // SearchSnippets.SNIPPET not supported
+        };
+
+        public static final int CONTACT_ID               = 0;
+        public static final int CONTACT_DISPLAY_NAME     = 1;
+        public static final int CONTACT_PRESENCE_STATUS  = 2;
+        public static final int CONTACT_CONTACT_STATUS   = 3;
+        public static final int CONTACT_PHOTO_ID         = 4;
+        public static final int CONTACT_PHOTO_URI        = 5;
+        public static final int CONTACT_LOOKUP_KEY       = 6;
+        public static final int CONTACT_IS_USER_PROFILE  = 7;
+        public static final int CONTACT_TIMES_CONTACTED  = 8;
+        public static final int CONTACT_STARRED          = 9;
+        // SearchSnippets.SNIPPET not supported
     }
 
     private CharSequence mUnknownNameText;
@@ -384,4 +431,11 @@
             }
         }
     }
+
+    protected final String[] getStrequentProjection() {
+        final int sortOrder = getContactNameDisplayOrder();
+        return sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+                ? StrequentQuery.FILTER_PROJECTION_PRIMARY
+                : StrequentQuery.FILTER_PROJECTION_ALTERNATIVE;
+    }
 }
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index 90b3d60..79fd12d 100644
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -29,6 +29,7 @@
 import android.os.Bundle;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippets;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.graphics.drawable.DrawableCompat;
 import android.support.v7.widget.AppCompatCheckBox;
@@ -1481,7 +1482,8 @@
      * Shows search snippet.
      */
     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
-        if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
+        if (cursor.getColumnCount() <= summarySnippetColumnIndex
+            || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
             setSnippet(null);
             return;
         }
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
index e50e9ce..ec91753 100644
--- a/src/com/android/contacts/common/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -44,6 +44,10 @@
     public static final char SNIPPET_START_MATCH = '[';
     public static final char SNIPPET_END_MATCH = ']';
 
+    // Whether to show strequent contacts before the normal type-to-filter search results.
+    // TODO(wjang): set this using phenotype
+    private final boolean mShowStrequentsSearchResultsFirst = false;
+
     public DefaultContactListAdapter(Context context) {
         super(context);
     }
@@ -69,16 +73,26 @@
                 loader.setSelection("0");
             } else {
                 final Builder builder = ContactsCompat.getContentUri().buildUpon();
-                builder.appendPath(query); // Builder will encode the query
-                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
-                        String.valueOf(directoryId));
-                if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
-                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
-                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
-                }
-                builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1");
+                appendSearchParameters(builder, query, directoryId);
                 loader.setUri(builder.build());
                 loader.setProjection(getProjection(true));
+                if (mShowStrequentsSearchResultsFirst) {
+                    // Filter out starred and frequently contacted contacts from the main loader
+                    // query results
+                    loader.setSelection(Contacts.TIMES_CONTACTED + "=0 AND "
+                            + Contacts.STARRED + "=0");
+
+                    // Strequent contacts will be merged back in before the main loader query
+                    // results and after the profile (ME).
+                    final ProfileAndContactsLoader profileAndContactsLoader =
+                            (ProfileAndContactsLoader) loader;
+                    profileAndContactsLoader.setLoadStrequent(true);
+                    final Builder strequentBuilder =
+                            Contacts.CONTENT_STREQUENT_FILTER_URI.buildUpon();
+                    appendSearchParameters(strequentBuilder, query, directoryId);
+                    profileAndContactsLoader.setStrequentUri(strequentBuilder.build());
+                    profileAndContactsLoader.setStrequentProjection(getStrequentProjection());
+                }
             }
         } else {
             configureUri(loader, directoryId, filter);
@@ -96,6 +110,17 @@
         loader.setSortOrder(sortOrder);
     }
 
+    private void appendSearchParameters(Builder builder, String query, long directoryId) {
+        builder.appendPath(query); // Builder will encode the query
+        builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                String.valueOf(directoryId));
+        if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
+            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                    String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+        }
+        builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1");
+    }
+
     protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
         Uri uri = Contacts.CONTENT_URI;
         if (filter != null && filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
diff --git a/src/com/android/contacts/common/list/ProfileAndContactsLoader.java b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java
index 698ef96..e68d4a1 100644
--- a/src/com/android/contacts/common/list/ProfileAndContactsLoader.java
+++ b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java
@@ -20,7 +20,9 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MergeCursor;
+import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Profile;
 
 import com.google.common.collect.Lists;
@@ -34,7 +36,10 @@
 public class ProfileAndContactsLoader extends CursorLoader {
 
     private boolean mLoadProfile;
+    private boolean mLoadStrequent;
     private String[] mProjection;
+    private String[] mStrequentProjection;
+    private Uri mStrequentUri;
 
     public ProfileAndContactsLoader(Context context) {
         super(context);
@@ -44,11 +49,23 @@
         mLoadProfile = flag;
     }
 
+    public void setLoadStrequent(boolean flag) {
+        mLoadStrequent = flag;
+    }
+
     public void setProjection(String[] projection) {
         super.setProjection(projection);
         mProjection = projection;
     }
 
+    public void setStrequentProjection(String[] projection) {
+        mStrequentProjection = projection;
+    }
+
+    public void setStrequentUri(Uri uri) {
+        mStrequentUri = uri;
+    }
+
     @Override
     public Cursor loadInBackground() {
         // First load the profile, if enabled.
@@ -56,6 +73,9 @@
         if (mLoadProfile) {
             cursors.add(loadProfile());
         }
+        if (mLoadStrequent) {
+            cursors.add(loadStrequent());
+        }
         // ContactsCursor.loadInBackground() can return null; MergeCursor
         // correctly handles null cursors.
         Cursor cursor = null;
@@ -101,4 +121,12 @@
             cursor.close();
         }
     }
+
+    /**
+     * Loads starred and frequently contacted contacts
+     */
+    private Cursor loadStrequent() {
+        return getContext().getContentResolver().query(
+                mStrequentUri, mStrequentProjection, null, null, null);
+    }
 }
diff --git a/src/com/android/contacts/common/logging/Logger.java b/src/com/android/contacts/common/logging/Logger.java
new file mode 100644
index 0000000..4e24caa
--- /dev/null
+++ b/src/com/android/contacts/common/logging/Logger.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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.common.logging;
+
+import android.app.Activity;
+
+import com.android.contacts.commonbind.analytics.AnalyticsUtil;
+import com.android.contacts.commonbind.logging.ClearcutLoggerHelper;
+
+/**
+ * Logs analytics events.
+ */
+public abstract class Logger {
+    public static final String TAG = "Logger";
+
+    public static Logger getInstance() {
+        return ClearcutLoggerHelper.getInstance();
+    }
+
+    /**
+     * Logs an event indicating that a screen was displayed.
+     *
+     * @param screenType integer identifier of the displayed screen
+     * @param activity Parent activity of the displayed screen.
+     * @param tag Optional description of the displayed screen.
+     */
+    public static void logScreenView(int screenType, Activity activity, String tag) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logScreenViewImpl(screenType);
+        }
+        final String screenName = ScreenEvent.getScreenNameWithTag(
+                activity.getClass().getSimpleName(), tag);
+        AnalyticsUtil.sendScreenView(screenName, activity, tag);
+    }
+
+    public abstract void logScreenViewImpl(int screenType);
+    public abstract void logSearchEventImpl(SearchState searchState);
+}
diff --git a/src/com/android/contacts/common/logging/ScreenEvent.java b/src/com/android/contacts/common/logging/ScreenEvent.java
new file mode 100644
index 0000000..4d282d6
--- /dev/null
+++ b/src/com/android/contacts/common/logging/ScreenEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.common.logging;
+
+import android.text.TextUtils;
+
+/**
+ * Stores constants identifying individual screens/dialogs/fragments in the application, and also
+ * provides a mapping of integer id -> screen name mappings for analytics purposes.
+ */
+public final class ScreenEvent {
+    private static final String FRAGMENT_TAG_SEPARATOR = "#";
+
+    // Should match ContactsExtension.ScreenEvent.ScreenType values in
+    // http://cs/google3/logs/proto/wireless/android/contacts/contacts_extensions.proto
+    public static final int UNKNOWN = 0;
+    public static final int SEARCH = 1;
+    public static final int SEARCH_EXIT = 2;
+
+    public static final String TAG_SEARCH = "Search";
+    public static final String TAG_SEARCH_EXIT = "SearchExit";
+
+    /**
+     * Build a tagged version of the provided screenName if the tag is non-empty.
+     *
+     * @param screenName Name of the screen.
+     * @param tag Optional tag describing the screen.
+     * @return the unchanged screenName if the tag is {@code null} or empty, the tagged version of
+     *         the screenName otherwise.
+     */
+    public static String getScreenNameWithTag(String screenName, String tag) {
+        if (TextUtils.isEmpty(tag)) {
+            return screenName;
+        }
+        return screenName + FRAGMENT_TAG_SEPARATOR + tag;
+    }
+}
diff --git a/src/com/android/contacts/common/logging/SearchState.java b/src/com/android/contacts/common/logging/SearchState.java
new file mode 100644
index 0000000..f4719e4
--- /dev/null
+++ b/src/com/android/contacts/common/logging/SearchState.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 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.common.logging;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.common.base.Objects;
+
+/**
+ * Describes the results of a user search for a particular contact.
+ */
+public final class SearchState implements Parcelable {
+
+    /** The length of the query string input by the user. */
+    public int queryLength;
+
+    /** The number of partitions (groups of results) presented to the user. */
+    public int numPartitions;
+
+    /** The total number of results (across all partitions) presented to the user. */
+    public int numResults;
+
+    /** The number of results presented to the user in the partition that was selected. */
+    public int numResultsInSelectedPartition = -1;
+
+    /** The zero-based index of the partition in which the clicked query result resides. */
+    public int selectedPartition = -1;
+
+    /** The index of the clicked query result within its partition. */
+    public int selectedIndexInPartition = -1;
+
+    /**
+     * The zero-based index of the clicked query result among all results displayed to the user
+     * (across partitions).
+     */
+    public int selectedIndex = -1;
+
+    public static final Creator<SearchState> CREATOR = new Creator<SearchState>() {
+        @Override
+        public SearchState createFromParcel(Parcel in) {
+            return new SearchState(in);
+        }
+
+        @Override
+        public SearchState[] newArray(int size) {
+            return new SearchState[size];
+        }
+    };
+
+    public SearchState() {
+    }
+
+    protected SearchState(Parcel source) {
+        readFromParcel(source);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("queryLength", queryLength)
+                .add("numPartitions", numPartitions)
+                .add("numResults", numResults)
+                .add("numResultsInSelectedPartition", numResultsInSelectedPartition)
+                .add("selectedPartition", selectedPartition)
+                .add("selectedIndexInPartition", selectedIndexInPartition)
+                .add("selectedIndex", selectedIndex)
+                .toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(queryLength);
+        dest.writeInt(numPartitions);
+        dest.writeInt(numResults);
+        dest.writeInt(numResultsInSelectedPartition);
+        dest.writeInt(selectedPartition);
+        dest.writeInt(selectedIndexInPartition);
+        dest.writeInt(selectedIndex);
+    }
+
+    private void readFromParcel(Parcel source) {
+        queryLength = source.readInt();
+        numPartitions = source.readInt();
+        numResults = source.readInt();
+        numResultsInSelectedPartition = source.readInt();
+        selectedPartition = source.readInt();
+        selectedIndexInPartition = source.readInt();
+        selectedIndex = source.readInt();
+    }
+}
diff --git a/src/com/android/contacts/common/preference/AboutPreferenceFragment.java b/src/com/android/contacts/common/preference/AboutPreferenceFragment.java
new file mode 100644
index 0000000..3ab32d5
--- /dev/null
+++ b/src/com/android/contacts/common/preference/AboutPreferenceFragment.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.common.preference;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.activity.LicenseActivity;
+
+/**
+ * This fragment shows the preferences for "about".
+ */
+public class AboutPreferenceFragment extends PreferenceFragment {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the preferences from an XML resource
+        addPreferencesFromResource(R.xml.preference_about);
+
+        // Set build version of Contacts App.
+        final PackageManager manager = getActivity().getPackageManager();
+        try {
+            final PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
+            final Preference versionPreference = findPreference(
+                    getString(R.string.pref_build_version_key));
+            versionPreference.setSummary(info.versionName);
+        } catch (PackageManager.NameNotFoundException e) {
+            // Nothing
+        }
+
+        final Preference licensePreference = findPreference(
+                getString(R.string.pref_open_source_licenses_key));
+        licensePreference.setIntent(new Intent(getActivity(), LicenseActivity.class));
+    }
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+}
+
diff --git a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
new file mode 100644
index 0000000..3fd10d8
--- /dev/null
+++ b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2009 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.common.preference;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.view.MenuItem;
+
+import com.android.contacts.common.activity.AppCompatPreferenceActivity;
+import com.android.contacts.common.R;
+
+/**
+ * Contacts settings.
+ */
+public final class ContactsPreferenceActivity extends AppCompatPreferenceActivity {
+
+    private static final String TAG_ABOUT_CONTACTS = "about_contacts";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
+
+        if (savedInstanceState == null) {
+            getFragmentManager().beginTransaction()
+                    .replace(android.R.id.content, new DisplayOptionsPreferenceFragment())
+                    .commit();
+            setActivityTitle(R.string.activity_title_settings);
+        } else {
+            final AboutPreferenceFragment fragment = (AboutPreferenceFragment) getFragmentManager()
+                    .findFragmentByTag(TAG_ABOUT_CONTACTS);
+            setActivityTitle(fragment == null ?
+                    R.string.activity_title_settings : R.string.setting_about);
+        }
+    }
+
+    public void showAboutFragment() {
+        getFragmentManager().beginTransaction()
+                .replace(android.R.id.content, new AboutPreferenceFragment(), TAG_ABOUT_CONTACTS)
+                .addToBackStack(null)
+                .commit();
+        setActivityTitle(R.string.setting_about);
+    }
+
+    /**
+     * Returns true if there are no preferences to display and therefore the
+     * corresponding menu item can be removed.
+     */
+    public static boolean isEmpty(Context context) {
+        return !context.getResources().getBoolean(R.bool.config_sort_order_user_changeable)
+                && !context.getResources().getBoolean(R.bool.config_display_order_user_changeable)
+                && !context.getResources().getBoolean(
+                        R.bool.config_default_account_user_changeable);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            onBackPressed();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (getFragmentManager().getBackStackEntryCount() > 0) {
+            setActivityTitle(R.string.activity_title_settings);
+            getFragmentManager().popBackStack();
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private void setActivityTitle(int res) {
+        final ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setTitle(res);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
index 37448e3..5815ef0 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -72,7 +72,7 @@
 
     public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
 
-    public static final String DO_NOT_SYNC_CONTACT_METADATA_MSG = "Do not sync contact metadata.";
+    public static final String DO_NOT_SYNC_CONTACT_METADATA_MSG = "Do not sync metadata";
 
     public static final String CONTACT_METADATA_AUTHORITY = "com.android.contacts.metadata";
 
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index 3a5c536..6f91616 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -17,24 +17,21 @@
 package com.android.contacts.common.preference;
 
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.os.Bundle;
-import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
-import android.preference.PreferenceScreen;
 
 import com.android.contacts.common.R;
-import com.android.contacts.common.activity.LicenseActivity;
+import com.android.contacts.common.compat.MetadataSyncEnabledCompat;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.GoogleAccountType;
 
 import java.util.List;
 
 /**
- * This fragment shows the preferences for the first header.
+ * This fragment shows the preferences for "display options"
  */
 public class DisplayOptionsPreferenceFragment extends PreferenceFragment {
 
@@ -45,34 +42,51 @@
         // Load the preferences from an XML resource
         addPreferencesFromResource(R.xml.preference_display_options);
 
-        // Remove "Default account" setting if no writable accounts.
+        removeUnsupportedPreferences();
+
+        final Preference aboutPreference = findPreference("about");
+        aboutPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                ((ContactsPreferenceActivity) getActivity()).showAboutFragment();
+                return true;
+            }
+        });
+    }
+
+    private void removeUnsupportedPreferences() {
+        // Disable sort order for CJK locales where it is not supported
+        final Resources resources = getResources();
+        if (!resources.getBoolean(R.bool.config_sort_order_user_changeable)) {
+            getPreferenceScreen().removePreference(findPreference("sortOrder"));
+        }
+
+        // Disable display order for CJK locales as well
+        if (!resources.getBoolean(R.bool.config_display_order_user_changeable)) {
+            getPreferenceScreen().removePreference(findPreference("displayOrder"));
+        }
+
+        // Remove the "Default account" setting if there aren't any writable accounts
         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
         final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(
                 /* contactWritableOnly */ true);
         if (accounts.isEmpty()) {
-            final PreferenceScreen preferenceScreen = getPreferenceScreen();
-            preferenceScreen.removePreference((ListPreference) findPreference("accounts"));
+            getPreferenceScreen().removePreference(findPreference("accounts"));
         }
 
-        // STOPSHIP Show this option when 1) metadata sync is enabled and 2) at least one
-        // focus google account.
-        final PreferenceScreen preferenceScreen = getPreferenceScreen();
-        preferenceScreen.removePreference((ListPreference) findPreference("contactMetadata"));
-
-        // Set build version of Contacts App.
-        final PackageManager manager = getActivity().getPackageManager();
-        try {
-            final PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
-            final Preference versionPreference = findPreference(
-                    getString(R.string.pref_build_version_key));
-            versionPreference.setSummary(info.versionName);
-        } catch (PackageManager.NameNotFoundException e) {
-            // Nothing
+        // Show Contact metadata sync option when 1) metadata sync is enabled
+        // and 2) there is at least one focus google account
+        boolean hasFocusGoogleAccount = false;
+        for (AccountWithDataSet account : accounts) {
+            if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null) {
+                hasFocusGoogleAccount = true;
+                break;
+            }
         }
-
-        final Preference licensePreference = findPreference(
-                getString(R.string.pref_open_source_licenses_key));
-        licensePreference.setIntent(new Intent(getActivity(), LicenseActivity.class));
+        if (!hasFocusGoogleAccount
+                || !MetadataSyncEnabledCompat.isMetadataSyncEnabled(getContext())) {
+            getPreferenceScreen().removePreference(findPreference("contactMetadata"));
+        }
     }
 
     @Override
diff --git a/src/com/android/contacts/commonbind/logging/ClearcutLoggerHelper.java b/src/com/android/contacts/commonbind/logging/ClearcutLoggerHelper.java
new file mode 100644
index 0000000..25c401c
--- /dev/null
+++ b/src/com/android/contacts/commonbind/logging/ClearcutLoggerHelper.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.commonbind.logging;
+
+import com.android.contacts.common.logging.Logger;
+import com.android.contacts.common.logging.SearchState;
+
+/**
+ * No-op clearcut logger implementation.
+ */
+public class ClearcutLoggerHelper extends Logger {
+
+    private static ClearcutLoggerHelper sInstance;
+
+    public static ClearcutLoggerHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new ClearcutLoggerHelper();
+        }
+        return sInstance;
+    }
+
+    private ClearcutLoggerHelper() {
+    }
+
+    @Override
+    public void logScreenViewImpl(int screenType) {
+    }
+
+    @Override
+    public void logSearchEventImpl(SearchState searchState) {
+    }
+}