Merge "Run getDisplayNameAndAvatarUri in background thread" into pi-car-dev
diff --git a/androidx-room/Android.mk b/androidx-room/Android.mk
index e9d215c..675dac8 100644
--- a/androidx-room/Android.mk
+++ b/androidx-room/Android.mk
@@ -36,6 +36,7 @@
     car-apache-commons-codec-nodeps:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar \
     car-auto-common-nodeps:$(COMMON_LIBS_PATH)/com/google/auto/auto-common/0.9/auto-common-0.9.jar \
     car-javapoet-nodeps:$(COMMON_LIBS_PATH)/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar \
+    car-jetbrains-annotations-nodeps:$(COMMON_LIBS_PATH)/org/jetbrains/annotations/13.0/annotations-13.0.jar \
     car-kotlin-metadata-nodeps:$(COMMON_LIBS_PATH)/me/eugeniomarletti/kotlin-metadata/1.2.1/kotlin-metadata-1.2.1.jar \
     car-sqlite-jdbc-nodeps:$(COMMON_LIBS_PATH)/org/xerial/sqlite-jdbc/3.20.1/sqlite-jdbc-3.20.1.jar
 
diff --git a/car-apps-common/res/layout/control_bar_slot.xml b/car-apps-common/res/layout/control_bar_slot.xml
index bc09eed..41ef13e 100644
--- a/car-apps-common/res/layout/control_bar_slot.xml
+++ b/car-apps-common/res/layout/control_bar_slot.xml
@@ -15,8 +15,8 @@
 -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/control_bar_button_slot_size"
-    android:layout_height="@dimen/control_bar_button_slot_size"
+    android:layout_width="@dimen/control_bar_button_slot_width"
+    android:layout_height="@dimen/control_bar_button_slot_height"
     android:layout_gravity="center_vertical"
     android:visibility="visible"
     android:foregroundGravity="center"
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-apps-common/res/values-h600dp/dimens.xml
similarity index 72%
copy from car-media-common/res/values-h668dp/dimens.xml
copy to car-apps-common/res/values-h600dp/dimens.xml
index 3ca1445..dcbea47 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-apps-common/res/values-h600dp/dimens.xml
@@ -15,7 +15,8 @@
   limitations under the License.
 -->
 <resources>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">192dp</dimen>
+    <dimen name="control_bar_button_background_radius">48dp</dimen>
+    <dimen name="control_bar_button_size">96dp</dimen>
+    <dimen name="control_bar_button_padding">26dp</dimen>
+    <dimen name="minimized_control_bar_button_size">96dp</dimen>
 </resources>
diff --git a/car-apps-common/res/values/dimens.xml b/car-apps-common/res/values/dimens.xml
index b2993e3..1a17bd3 100644
--- a/car-apps-common/res/values/dimens.xml
+++ b/car-apps-common/res/values/dimens.xml
@@ -27,10 +27,11 @@
     <dimen name="control_bar_margin_x">@*android:dimen/car_margin</dimen>
     <dimen name="control_bar_margin_bottom">@*android:dimen/car_padding_2</dimen>
     <dimen name="control_bar_button_size">76dp</dimen>
-    <dimen name="control_bar_button_slot_size">@dimen/control_bar_height</dimen>
+    <dimen name="control_bar_button_slot_height">@dimen/control_bar_height</dimen>
+    <dimen name="control_bar_button_slot_width">@dimen/control_bar_button_size</dimen>
     <dimen name="control_bar_elevation">0dp</dimen>
     <dimen name="control_bar_button_padding">16dp</dimen>
-    <dimen name="control_bar_button_background_radius">48dp</dimen>
+    <dimen name="control_bar_button_background_radius">38dp</dimen>
 
     <!-- Overflow button in control Bar -->
     <dimen name="overflow_button_icon_size">44dp</dimen>
diff --git a/car-assist-client-lib/Android.mk b/car-assist-client-lib/Android.mk
index 82dc48d..ae687ac 100644
--- a/car-assist-client-lib/Android.mk
+++ b/car-assist-client-lib/Android.mk
@@ -39,3 +39,7 @@
     car-assist-lib \
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
+
+ifeq (,$(ONE_SHOT_MAKEFILE))
+    include $(call all-makefiles-under,$(LOCAL_PATH))
+endif
diff --git a/car-assist-client-lib/tests/Android.mk b/car-assist-client-lib/tests/Android.mk
new file mode 100644
index 0000000..9f0a4e8
--- /dev/null
+++ b/car-assist-client-lib/tests/Android.mk
@@ -0,0 +1,19 @@
+# Copyright (C) 2019 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# Include all makefiles in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/car-assist-client-lib/tests/robotests/Android.mk b/car-assist-client-lib/tests/robotests/Android.mk
new file mode 100644
index 0000000..93613b6
--- /dev/null
+++ b/car-assist-client-lib/tests/robotests/Android.mk
@@ -0,0 +1,78 @@
+LOCAL_PATH := $(call my-dir)
+
+############################################################
+# CarAssistClient app just for Robolectric test target.     #
+############################################################
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := CarAssistClient
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_JAVA_LIBRARIES := android.car
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+    car-assist-client-lib
+
+include $(BUILD_PACKAGE)
+
+#############################################
+# Car-Assist-Client Robolectric test target. #
+#############################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := CarAssistClientRoboTests
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+# Include the testing libraries
+LOCAL_JAVA_LIBRARIES := \
+    android.car \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    truth-prebuilt
+
+LOCAL_INSTRUMENTATION_FOR := CarAssistClient
+
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+#############################################################
+# CarAssistClient runner target to run the previous target. #
+#############################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunCarAssistClientRoboTests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    CarAssistClientRoboTests
+
+LOCAL_JAVA_LIBRARIES := \
+    CarAssistClientRoboTests \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    truth-prebuilt \
+    android.car
+
+LOCAL_TEST_PACKAGE := CarAssistClient
+
+LOCAL_ROBOTEST_FILES := \
+    $(call find-files-in-subdirs,$(LOCAL_PATH)/src,*Test.java,.))
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
+
+include external/robolectric-shadows/run_robotests.mk
diff --git a/car-assist-client-lib/tests/robotests/AndroidManifest.xml b/car-assist-client-lib/tests/robotests/AndroidManifest.xml
new file mode 100644
index 0000000..f744c6c
--- /dev/null
+++ b/car-assist-client-lib/tests/robotests/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2019 Google Inc.
+
+    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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.assist.client.robotests">
+
+</manifest>
diff --git a/car-assist-client-lib/tests/robotests/config/robolectric_properties b/car-assist-client-lib/tests/robotests/config/robolectric_properties
new file mode 100644
index 0000000..12ca377
--- /dev/null
+++ b/car-assist-client-lib/tests/robotests/config/robolectric_properties
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2019 Google Inc.
+#
+# 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.
+#
+manifest=packages/apps/Car/libs/car-assist-client-lib/tests/robotests/AndroidManifest.xml
+sdk=NEWEST_SDK
diff --git a/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/CarAssistUtilsTest.java b/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/CarAssistUtilsTest.java
new file mode 100644
index 0000000..8e9770f
--- /dev/null
+++ b/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/CarAssistUtilsTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 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.car.assist.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationCompat.Action;
+import androidx.core.app.NotificationCompat.MessagingStyle;
+import androidx.core.app.Person;
+import androidx.core.app.RemoteInput;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CarAssistUtilsTest {
+
+    private Context mContext;
+    private static final String PKG_1 = "package_1";
+    private static final String OP_PKG = "OpPackage";
+    private static final int ID = 1;
+    private static final String TAG = "Tag";
+    private static final int UID = 2;
+    private static final int INITIAL_PID = 3;
+    private static final String CHANNEL_ID = "CHANNEL_ID";
+    private static final String CONTENT_TITLE = "CONTENT_TITLE";
+    private static final String STATIC_USER_NAME = "STATIC_USER_NAME";
+    private static final String SENDER_NAME = "Larry";
+    private static final String SENDER_CONTACT_URI = "TEST_SENDER_URI";
+    private static final String REMOTE_INPUT_KEY = "REMOTE_INPUT_KEY";
+    private static final String REPLY_ACTION = "test.package.REPLY";
+    private static final String READ_ACTION = "test.package.READ";
+    private static final long POST_TIME = 12345L;
+    private static final int ICON = android.R.drawable.ic_media_play;
+    private static final String OVERRIDE_GROUP_KEY = "OVERRIDE_GROUP_KEY";
+    private static final UserHandle USER_HANDLE = new UserHandle(12);
+
+    @Before
+    public void setup() {
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void testCarCompatMessagingNotification_qualifyingNotification() {
+        assertThat(CarAssistUtils.isCarCompatibleMessagingNotification(
+                buildStatusBarNotification(/* hasReplyAction */ true, /* hasMessagingStyle */
+                        true))).isTrue();
+    }
+
+    @Test
+    public void testCarCompatMessagingNotification_noReplyNotification() {
+        assertThat(CarAssistUtils.isCarCompatibleMessagingNotification(
+                buildStatusBarNotification(/* hasReplyAction */ false, /* hasMessagingStyle */
+                        true))).isTrue();
+    }
+
+    @Test
+    public void testCarCompatMessagingNotifcation_noMessagingStyleNotification() {
+        assertThat(CarAssistUtils.isCarCompatibleMessagingNotification(
+                buildStatusBarNotification(/* hasReplyAction */ true, /* hasMessagingStyle */
+                        false))).isFalse();
+    }
+
+    private StatusBarNotification buildStatusBarNotification(boolean hasReplyAction,
+            boolean hasMessagingStyle) {
+
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
+                .setContentTitle(CONTENT_TITLE)
+                .setCategory(Notification.CATEGORY_MESSAGE)
+                .addAction(buildMarkAsReadAction())
+                .setShowWhen(true);
+
+        if (hasReplyAction) {
+            builder.addAction(buildReplyAction());
+        }
+
+        if (hasMessagingStyle) {
+            builder.setStyle(buildMessagingStyle());
+        }
+
+        return new StatusBarNotification(PKG_1, OP_PKG,
+                ID, TAG, UID, INITIAL_PID, builder.build(), USER_HANDLE,
+                OVERRIDE_GROUP_KEY, POST_TIME);
+    }
+
+    private Action buildMarkAsReadAction() {
+        Intent intent = new Intent(mContext, this.getClass()).setAction(READ_ACTION);
+        PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        return new Action.Builder(ICON, "read", pendingIntent)
+                .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
+                .setShowsUserInterface(false)
+                .build();
+    }
+
+    private Action buildReplyAction() {
+        Intent intent = new Intent(mContext, this.getClass())
+                .setAction(REPLY_ACTION);
+        PendingIntent replyIntent = PendingIntent.getService(mContext, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+
+        return new Action.Builder(ICON,
+                "reply", replyIntent)
+                .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)
+                .setShowsUserInterface(false)
+                .addRemoteInput(
+                        new RemoteInput.Builder(REMOTE_INPUT_KEY)
+                                .build()
+                )
+                .build();
+    }
+
+    private MessagingStyle buildMessagingStyle() {
+        Person user = new Person.Builder()
+                .setName(STATIC_USER_NAME)
+                .build();
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle(user);
+        Person sender = new Person.Builder()
+                .setName(SENDER_NAME)
+                .setUri(SENDER_CONTACT_URI)
+                .build();
+        messagingStyle.addMessage("Hello World", POST_TIME, sender);
+        return messagingStyle;
+    }
+}
diff --git a/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/TestConfig.java b/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/TestConfig.java
new file mode 100644
index 0000000..6c2a92b
--- /dev/null
+++ b/car-assist-client-lib/tests/robotests/src/com/android/car/assist/client/TestConfig.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 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.car.assist.client;
+
+public class TestConfig {
+    public static final int SDK_VERSION = 28;
+    public static final String MANIFEST_PATH =
+            "packages/apps/Car/libs/car-assist-client-lib/AndroidManifest.xml";
+}
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-media-common/res/values-h600dp/dimens.xml
similarity index 79%
rename from car-media-common/res/values-h668dp/dimens.xml
rename to car-media-common/res/values-h600dp/dimens.xml
index 3ca1445..b1bcd84 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-media-common/res/values-h600dp/dimens.xml
@@ -15,7 +15,8 @@
   limitations under the License.
 -->
 <resources>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
     <dimen name="appbar_2_rows_height">192dp</dimen>
+    <dimen name="appbar_view_icon_touch_target_size">96dp</dimen>
+    <dimen name="appbar_view_icon_padding">26dp</dimen>
+    <dimen name="appbar_view_icon_background_radius">48dp</dimen>
 </resources>
diff --git a/car-media-common/res/values/bools.xml b/car-media-common/res/values/bools.xml
index e6b0d91..ffe316b 100644
--- a/car-media-common/res/values/bools.xml
+++ b/car-media-common/res/values/bools.xml
@@ -24,9 +24,9 @@
     -->
     <bool name="flag_invalid_media_art">false</bool>
 
-    <!-- Whether to show a linear progress bar in control bar and minimized control bar or not. -->
-    <bool name="show_linear_progress_bar">false</bool>
+    <!-- Whether to show a linear progress bar in minimized control bar or not. -->
+    <bool name="show_linear_progress_bar">true</bool>
     <!-- Whether to show a circular progress bar in control bar and minimized control bar or not. -->
-    <bool name="show_circular_progress_bar">true</bool>
+    <bool name="show_circular_progress_bar">false</bool>
 
 </resources>
diff --git a/car-media-common/res/values/dimens.xml b/car-media-common/res/values/dimens.xml
index c5ea201..8bf9a60 100644
--- a/car-media-common/res/values/dimens.xml
+++ b/car-media-common/res/values/dimens.xml
@@ -27,10 +27,10 @@
     <dimen name="appbar_second_row_height">0dp</dimen>
     <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
     <dimen name="appbar_2_rows_height">160dp</dimen>
-    <dimen name="appbar_view_icon_touch_target_size">96dp</dimen>
+    <dimen name="appbar_view_icon_touch_target_size">76dp</dimen>
     <dimen name="appbar_view_icon_size">@*android:dimen/car_primary_icon_size</dimen>
     <!-- Padding of primary icon, equals (@dimen/appbar_view_icon_touch_target_size - @dimen/appbar_view_icon_size) / 2. -->
-    <dimen name="appbar_view_icon_padding">26dp</dimen>
+    <dimen name="appbar_view_icon_padding">16dp</dimen>
 
     <!-- playback_fragment.xml -->
     <dimen name="playback_fragment_text_margin_top">@*android:dimen/car_padding_4</dimen>
@@ -61,5 +61,5 @@
     <dimen name="minimized_progress_bar_track_thickness">4dp</dimen>
 
     <!-- appbar_view_icon_background.xml -->
-    <dimen name="appbar_view_icon_background_radius">48dp</dimen>
+    <dimen name="appbar_view_icon_background_radius">38dp</dimen>
 </resources>
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index ed4e9ae..34516bd 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -44,7 +44,7 @@
 
 /**
  * This represents a source of media content. It provides convenient methods to access media source
- * metadata, such as primary color and application name.
+ * metadata, such as application name and icon.
  */
 public class MediaSource {
     private static final String TAG = "MediaSource";
@@ -59,14 +59,11 @@
     }
 
     @NonNull
-    private final String mPackageName;
-    @Nullable
-    private String mBrowseServiceClassName;
+    private final ComponentName mBrowseService;
     @NonNull
-    private final Context mContext;
-    @Nullable
-    private final ServiceInfo mServiceInfo;
-    private CharSequence mName;
+    private final CharSequence mDisplayName;
+    @NonNull
+    private final Drawable mIcon;
 
     /**
      * Creates a {@link MediaSource} for the given {@link ComponentName}
@@ -74,19 +71,32 @@
     @Nullable
     public static MediaSource create(@NonNull Context context,
             @NonNull ComponentName componentName) {
-        MediaSource mediaSource = new MediaSource(context, componentName);
-        if (mediaSource.mBrowseServiceClassName == null) {
+        ServiceInfo serviceInfo = getBrowseServiceInfo(context, componentName);
+
+        String className = serviceInfo != null ? serviceInfo.name : null;
+        if (TextUtils.isEmpty(className)) {
+            Log.w(TAG,
+                    "No MediaBrowserService found in component " + componentName.flattenToString());
             return null;
         }
-        return mediaSource;
+
+        try {
+            String packageName = componentName.getPackageName();
+            CharSequence displayName = extractDisplayName(context, serviceInfo, packageName);
+            Drawable icon = extractIcon(context, serviceInfo, packageName);
+            ComponentName browseService = new ComponentName(packageName, className);
+            return new MediaSource(browseService, displayName, icon);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Component not found " + componentName.flattenToString());
+            return null;
+        }
     }
 
-    // TODO(b/136274456): Clean up the internals of MediaSource.
-    private MediaSource(@NonNull Context context, @NonNull ComponentName componentName) {
-        mContext = context;
-        mPackageName = componentName.getPackageName();
-        mServiceInfo = getBrowseServiceInfo(componentName);
-        extractComponentInfo();
+    private MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
+            @NonNull Drawable icon) {
+        mBrowseService = browseService;
+        mDisplayName = displayName;
+        mIcon = icon;
     }
 
     /**
@@ -96,8 +106,9 @@
      * connect and handle rejections gracefully.
      */
     @Nullable
-    private ServiceInfo getBrowseServiceInfo(@NonNull ComponentName componentName) {
-        PackageManager packageManager = mContext.getPackageManager();
+    private static ServiceInfo getBrowseServiceInfo(@NonNull Context context,
+            @NonNull ComponentName componentName) {
+        PackageManager packageManager = context.getPackageManager();
         Intent intent = new Intent();
         intent.setAction(MediaBrowserService.SERVICE_INTERFACE);
         intent.setPackage(componentName.getPackageName());
@@ -119,99 +130,82 @@
         return null;
     }
 
-    private void extractComponentInfo() {
-        mBrowseServiceClassName = mServiceInfo != null ? mServiceInfo.name : null;
-        try {
-            // Gets a proper app name. Checks service label first. If failed, uses application
-            // label as fallback.
-            if (mServiceInfo != null && mServiceInfo.labelRes != 0) {
-                mName = mServiceInfo.loadLabel(mContext.getPackageManager());
-            } else {
-                ApplicationInfo applicationInfo =
-                        mContext.getPackageManager().getApplicationInfo(mPackageName,
-                                PackageManager.GET_META_DATA);
-                mName = applicationInfo.loadLabel(mContext.getPackageManager());
-            }
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Unable to update media client package attributes.", e);
+    /**
+     * @return a proper app name. Checks service label first. If failed, uses application label
+     * as fallback.
+     */
+    @NonNull
+    private static CharSequence extractDisplayName(@NonNull Context context,
+            @Nullable ServiceInfo serviceInfo, @NonNull String packageName)
+            throws PackageManager.NameNotFoundException {
+        if (serviceInfo != null && serviceInfo.labelRes != 0) {
+            return serviceInfo.loadLabel(context.getPackageManager());
         }
+        ApplicationInfo applicationInfo =
+                context.getPackageManager().getApplicationInfo(packageName,
+                        PackageManager.GET_META_DATA);
+        return applicationInfo.loadLabel(context.getPackageManager());
+    }
+
+    /**
+     * @return a proper icon. Checks service icon first. If failed, uses application icon as
+     * fallback.
+     */
+    @NonNull
+    private static Drawable extractIcon(@NonNull Context context, @Nullable ServiceInfo serviceInfo,
+            @NonNull String packageName) throws PackageManager.NameNotFoundException {
+        return serviceInfo != null ? serviceInfo.loadIcon(context.getPackageManager())
+                : context.getPackageManager().getApplicationIcon(packageName);
     }
 
     /**
      * @return media source human readable name for display.
      */
+    @NonNull
     public CharSequence getDisplayName() {
-        return mName;
+        return mDisplayName;
     }
 
     /**
      * @return the package name of this media source.
      */
+    @NonNull
     public String getPackageName() {
-        return mPackageName;
+        return mBrowseService.getPackageName();
     }
 
     /**
      * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService},
      * or NULL if this media source doesn't implement such service.
      */
-    @Nullable
+    @NonNull
     public ComponentName getBrowseServiceComponentName() {
-        if (mBrowseServiceClassName != null) {
-            return new ComponentName(mPackageName, mBrowseServiceClassName);
-        } else {
-            return null;
-        }
+        return mBrowseService;
     }
 
     /**
      * @return a {@link Drawable} as the media source's icon.
      */
+    @NonNull
     public Drawable getIcon() {
-        // Checks service icon first. If failed, uses application icon as fallback.
-        try {
-            if (mServiceInfo != null) {
-                return mServiceInfo.loadIcon(mContext.getPackageManager());
-            }
-            return mContext.getPackageManager().getApplicationIcon(getPackageName());
-        } catch (PackageManager.NameNotFoundException e) {
-            return null;
-        }
+        return mIcon;
     }
 
     /**
      * Returns this media source's icon cropped to a circle.
      */
     public Bitmap getRoundPackageIcon() {
-        Drawable icon = getIcon();
-        if (icon != null) {
-            return getRoundCroppedBitmap(drawableToBitmap(icon));
-        }
-        return null;
+        return getRoundCroppedBitmap(drawableToBitmap(mIcon));
     }
 
     /**
      * Returns {@code true} iff this media source should not be templatized.
      */
     public boolean isCustom() {
-        return isCustom(mPackageName);
+        return CUSTOM_MEDIA_SOURCES.contains(getPackageName());
     }
 
-    /**
-     * Returns {@code true} iff the provided media package should not be templatized.
-     */
-    public static boolean isCustom(String packageName) {
-        return CUSTOM_MEDIA_SOURCES.contains(packageName);
-    }
-
-    /**
-     * Returns {@code true} iff this media source has a browse service to connect to.
-     */
-    public boolean isBrowsable() {
-        return mBrowseServiceClassName != null;
-    }
-
-    private Bitmap drawableToBitmap(Drawable drawable) {
+    private static Bitmap drawableToBitmap(Drawable drawable) {
         Bitmap bitmap = null;
 
         if (drawable instanceof BitmapDrawable) {
@@ -234,7 +228,7 @@
         return bitmap;
     }
 
-    private Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
+    private static Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
         Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
                 Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(output);
@@ -258,24 +252,17 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         MediaSource that = (MediaSource) o;
-        return Objects.equals(mPackageName, that.mPackageName)
-                && Objects.equals(mBrowseServiceClassName, that.mBrowseServiceClassName);
+        return Objects.equals(mBrowseService, that.mBrowseService);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPackageName, mBrowseServiceClassName);
+        return Objects.hash(mBrowseService);
     }
 
     @Override
     @NonNull
     public String toString() {
-        return mPackageName + (mBrowseServiceClassName == null ? "" : mBrowseServiceClassName);
-    }
-
-    /** Returns the package name of the given source, or null. */
-    @Nullable
-    public static String getPackageName(@Nullable MediaSource source) {
-        return (source != null) ? source.getPackageName() : null;
+        return mBrowseService.flattenToString();
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
index 17479dc..d25ebe9 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
@@ -231,9 +231,6 @@
         }
 
         ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
-        if (browseService == null) {
-            Log.e(TAG, "No browseService for source: " + newMediaSource.getPackageName());
-        }
         mBrowserConnector.connectTo(browseService);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
index 475fd81..6abdab2 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
@@ -97,11 +97,6 @@
                             Log.w(TAG, "Media source is null");
                             return false;
                         }
-                        if (mediaSource.getDisplayName() == null) {
-                            Log.w(TAG,
-                                    "Found media source without name: " + mediaSource.toString());
-                            return false;
-                        }
                         return true;
                     })
                     .sorted(Comparator.comparing(
@@ -112,37 +107,20 @@
     }
 
     /**
-     * Generates a set of all possible activities and media services to choose from.
+     * Generates a set of all possible media services to choose from.
      */
     private Set<ComponentName> getComponentNames() {
         PackageManager packageManager = mAppContext.getPackageManager();
-        Intent intent = new Intent(Intent.ACTION_MAIN, null);
-        intent.addCategory(Intent.CATEGORY_APP_MUSIC);
-
         Intent mediaIntent = new Intent();
         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
-        List<ResolveInfo> availableActivities = packageManager.queryIntentActivities(intent, 0);
         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
                 PackageManager.GET_RESOLVED_FILTER);
 
         Set<ComponentName> components = new HashSet<>();
-        Set<String> packages = new HashSet<>();
         for (ResolveInfo info : mediaServices) {
             ComponentName componentName = new ComponentName(info.serviceInfo.packageName,
                     info.serviceInfo.name);
             components.add(componentName);
-            packages.add(info.serviceInfo.packageName);
-        }
-        for (ResolveInfo info : availableActivities) {
-            // If a service has been added to the map, don't add the activity belonging to the
-            // same package.
-            if (packages.contains(info.activityInfo.packageName)) {
-                continue;
-            }
-            ComponentName componentName = new ComponentName(info.activityInfo.packageName,
-                    info.activityInfo.name);
-            components.add(componentName);
         }
         return components;
     }
diff --git a/car-telephony-common/src/com/android/car/telephony/common/Contact.java b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
index e95cb3e..43e2cdc 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/Contact.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
@@ -38,6 +38,8 @@
  */
 public class Contact implements Parcelable, Comparable<Contact> {
     private static final String TAG = "CD.Contact";
+    private static final String PHONEBOOK_LABEL = "phonebook_label";
+    private static final String PHONEBOOK_LABEL_ALT = "phonebook_label_alt";
 
     /**
      * Contact belongs to TYPE_LETTER if its display name starts with a letter
@@ -102,6 +104,25 @@
     private String mAltDisplayName;
 
     /**
+     * The phonebook label.
+     * <p>
+     * For {@link #mDisplayName}s starting with letters, label will be the first character of
+     * {@link #mDisplayName}. For {@link #mDisplayName}s starting with numbers, the label will
+     * be "#". For {@link #mDisplayName}s starting with other characters, the label will be "...".
+     * </p>
+     */
+    private String mPhoneBookLabel;
+
+    /**
+     * The alternative phonebook label.
+     * <p>
+     * It is similar with {@link #mPhoneBookLabel}. But instead of generating from
+     * {@link #mDisplayName}, it will use {@link #mAltDisplayName}.
+     * </p>
+     */
+    private String mPhoneBookLabelAlt;
+
+    /**
      * A URI that can be used to retrieve a thumbnail of the contact's photo.
      */
     private Uri mAvatarThumbnailUri;
@@ -138,6 +159,8 @@
                 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
         int altDisplayNameColumn = cursor.getColumnIndex(
                 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_ALTERNATIVE);
+        int phoneBookLabelColumn = cursor.getColumnIndex(PHONEBOOK_LABEL);
+        int phoneBookLabelAltColumn = cursor.getColumnIndex(PHONEBOOK_LABEL_ALT);
         int avatarUriColumn = cursor.getColumnIndex(
                 ContactsContract.CommonDataKinds.Phone.PHOTO_URI);
         int avatarThumbnailColumn = cursor.getColumnIndex(
@@ -149,6 +172,8 @@
         contact.mId = cursor.getLong(contactIdColumn);
         contact.mDisplayName = cursor.getString(displayNameColumn);
         contact.mAltDisplayName = cursor.getString(altDisplayNameColumn);
+        contact.mPhoneBookLabel = cursor.getString(phoneBookLabelColumn);
+        contact.mPhoneBookLabelAlt = cursor.getString(phoneBookLabelAltColumn);
 
         PhoneNumber number = PhoneNumber.fromCursor(context, cursor);
         contact.mPhoneNumbers.add(number);
@@ -203,6 +228,20 @@
         return mAltDisplayName;
     }
 
+    /**
+     * Returns {@link #mPhoneBookLabel}
+     */
+    public String getPhonebookLabel() {
+        return mPhoneBookLabel;
+    }
+
+    /**
+     * Returns {@link #mPhoneBookLabelAlt}
+     */
+    public String getPhonebookLabelAlt() {
+        return mPhoneBookLabelAlt;
+    }
+
     public boolean isVoicemail() {
         return mIsVoiceMail;
     }
@@ -300,6 +339,8 @@
         }
         dest.writeString(mDisplayName);
         dest.writeString(mAltDisplayName);
+        dest.writeString(mPhoneBookLabel);
+        dest.writeString(mPhoneBookLabelAlt);
         dest.writeParcelable(mAvatarThumbnailUri, 0);
         dest.writeParcelable(mAvatarUri, 0);
         dest.writeString(mLookupKey);
@@ -335,6 +376,8 @@
         }
         contact.mDisplayName = source.readString();
         contact.mAltDisplayName = source.readString();
+        contact.mPhoneBookLabel = source.readString();
+        contact.mPhoneBookLabelAlt = source.readString();
         contact.mAvatarThumbnailUri = source.readParcelable(Uri.class.getClassLoader());
         contact.mAvatarUri = source.readParcelable(Uri.class.getClassLoader());
         contact.mLookupKey = source.readString();
@@ -354,7 +397,8 @@
      * letters, numbers, then special characters.
      */
     public int compareByDisplayName(@NonNull Contact otherContact) {
-        return compareNames(mDisplayName, otherContact.getDisplayName());
+        return compareNames(mDisplayName, otherContact.getDisplayName(),
+                mPhoneBookLabel, otherContact.getPhonebookLabel());
     }
 
     /**
@@ -362,15 +406,16 @@
      * letters, numbers, then special characters.
      */
     public int compareByAltDisplayName(@NonNull Contact otherContact) {
-        return compareNames(mAltDisplayName, otherContact.getAltDisplayName());
+        return compareNames(mAltDisplayName, otherContact.getAltDisplayName(),
+                mPhoneBookLabelAlt, otherContact.getPhonebookLabelAlt());
     }
 
     /**
      * Compares two strings in an order of letters, numbers, then special characters.
      */
-    private int compareNames(String name, String otherName) {
-        int type = getNameType(name);
-        int otherType = getNameType(otherName);
+    private int compareNames(String name, String otherName, String label, String otherLabel) {
+        int type = getNameType(label);
+        int otherType = getNameType(otherLabel);
         if (type != otherType) {
             return Integer.compare(type, otherType);
         }
@@ -378,13 +423,17 @@
         return collator.compare(name == null ? "" : name, otherName == null ? "" : otherName);
     }
 
-    private static int getNameType(String displayName) {
+    /**
+     * Returns the type of the name string.
+     * Types can be {@link #TYPE_LETTER}, {@link #TYPE_DIGIT} and {@link #TYPE_OTHER}.
+     */
+    private static int getNameType(String label) {
         // A helper function to classify Contacts
-        if (!TextUtils.isEmpty(displayName)) {
-            if (Character.isLetter(displayName.charAt(0))) {
+        if (!TextUtils.isEmpty(label)) {
+            if (Character.isLetter(label.charAt(0))) {
                 return TYPE_LETTER;
             }
-            if (Character.isDigit(displayName.charAt(0))) {
+            if (label.contains("#")) {
                 return TYPE_DIGIT;
             }
         }
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index 24529b8..73a31c9 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -88,11 +88,14 @@
     private InMemoryPhoneBook(Context context) {
         mContext = context;
 
+        // TODO(b/138749585): clean up filtering once contact cloud sync is disabled.
         QueryParam contactListQueryParam = new QueryParam(
                 ContactsContract.Data.CONTENT_URI,
                 null,
-                ContactsContract.Data.MIMETYPE + " = ?",
-                new String[]{ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE},
+                ContactsContract.Data.MIMETYPE + " = ? and "
+                        + ContactsContract.RawContacts.ACCOUNT_TYPE + " != ?",
+                new String[]{
+                        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, "com.google"},
                 ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
         mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
                 QueryParam.of(contactListQueryParam)) {
diff --git a/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java b/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
index 85893e0..5f38799 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
@@ -23,7 +23,9 @@
 import android.os.Parcelable;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Objects;
@@ -34,15 +36,17 @@
 public class PhoneNumber implements Parcelable {
 
     private final I18nPhoneNumberWrapper mI18nPhoneNumber;
-    private final int mType;
-    @Nullable
-    private final String mLabel;
+    @NonNull
+    private final String mAccountName;
+    @NonNull
+    private final String mAccountType;
 
+    private int mType;
+    @Nullable
+    private String mLabel;
     private boolean mIsPrimary;
     private long mId;
     private int mDataVersion;
-    private String mAccountName;
-    private String mAccountType;
 
     static PhoneNumber fromCursor(Context context, Cursor cursor) {
         int typeColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
@@ -99,22 +103,22 @@
         mLabel = label;
         mIsPrimary = isPrimary;
         mId = id;
-        mAccountName = accountName;
-        mAccountType = accountType;
+        mAccountName = TextUtils.emptyIfNull(accountName);
+        mAccountType = TextUtils.emptyIfNull(accountType);
         mDataVersion = dataVersion;
     }
 
     @Override
     public boolean equals(Object obj) {
         return obj instanceof PhoneNumber
-                && ((PhoneNumber) obj).mType == mType
-                && Objects.equals(((PhoneNumber) obj).mLabel, mLabel)
-                && mI18nPhoneNumber.equals(((PhoneNumber) obj).mI18nPhoneNumber);
+                && mI18nPhoneNumber.equals(((PhoneNumber) obj).mI18nPhoneNumber)
+                && mAccountName.equals(((PhoneNumber) obj).mAccountName)
+                && mAccountType.equals(((PhoneNumber) obj).mAccountType);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mI18nPhoneNumber, mType, mLabel);
+        return Objects.hash(mI18nPhoneNumber, mAccountName, mAccountType);
     }
 
     /**
@@ -188,8 +192,8 @@
                 mDataVersion = phoneNumber.mDataVersion;
                 mId = phoneNumber.mId;
                 mIsPrimary |= phoneNumber.mIsPrimary;
-                mAccountName = phoneNumber.mAccountName;
-                mAccountType = phoneNumber.mAccountType;
+                mType = phoneNumber.mType;
+                mLabel = phoneNumber.mLabel;
             }
         }
         return this;
@@ -205,7 +209,7 @@
 
     @Override
     public String toString() {
-        return getNumber() + " " + String.valueOf(mLabel);
+        return getNumber() + " " + mAccountName + " " + mAccountType;
     }
 
     @Override
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index a517c89..7cb60ae 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -338,17 +338,10 @@
             final String displayName) {
         LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName);
 
-        if (avatarUri != null) {
-            Glide.with(context)
-                    .load(avatarUri)
-                    .apply(new RequestOptions().centerCrop().error(letterTileDrawable))
-                    .into(icon);
-            return;
-        }
-
-        // Use the letter tile as avatar if there is no avatar available from content provider.
-        icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
-        icon.setImageDrawable(letterTileDrawable);
+        Glide.with(context)
+                .load(avatarUri)
+                .apply(new RequestOptions().centerCrop().error(letterTileDrawable))
+                .into(icon);
     }
 
     /** Create a {@link LetterTileDrawable} for the given display name. */