Add suport for additional injected metadata

To support security patch injection, adding support for additional
injection metadata including keyhint, icon_tintable, summary_uri, and
icon_uri.

Bug: 172352420
Test: manual, atest CarSettingsUnitTests, make -j50 RunCarSettingsRoboTests ROBOTEST_FILTER=com.android.car.settings.common
Change-Id: I6a2f55817679ef1f370cd72925392dcdee2a0831
diff --git a/res/drawable/ic_find_device_disabled.xml b/res/drawable/ic_find_device_disabled.xml
new file mode 100644
index 0000000..631d3e8
--- /dev/null
+++ b/res/drawable/ic_find_device_disabled.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FFF44336"
+        android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13C19,5.13 15.87,2 12,2zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9zM11,13h2v2h-2V13zM13,6h-2v5h2V6z"/>
+</vector>
diff --git a/res/drawable/ic_find_device_enabled.xml b/res/drawable/ic_find_device_enabled.xml
new file mode 100644
index 0000000..d676a70
--- /dev/null
+++ b/res/drawable/ic_find_device_enabled.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FF43A047"
+        android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13C19,5.13 15.87,2 12,2zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9zM14.5,9c0,1.38 -1.12,2.5 -2.5,2.5S9.5,10.38 9.5,9s1.12,-2.5 2.5,-2.5S14.5,7.62 14.5,9z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_current.xml b/res/drawable/ic_ota_update_current.xml
new file mode 100644
index 0000000..d21f68a
--- /dev/null
+++ b/res/drawable/ic_ota_update_current.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FF43A047"
+        android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11.14,16l-3.84,-3.84l1.41,-1.42l2.43,2.42l4.16,-4.16l1.42,1.41L11.14,16z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_none.xml b/res/drawable/ic_ota_update_none.xml
new file mode 100644
index 0000000..df6c8fe
--- /dev/null
+++ b/res/drawable/ic_ota_update_none.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FFF44336"
+        android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_stale.xml b/res/drawable/ic_ota_update_stale.xml
new file mode 100644
index 0000000..df6c8fe
--- /dev/null
+++ b/res/drawable/ic_ota_update_stale.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FFF44336"
+        android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_disabled.xml b/res/drawable/ic_package_verifier_disabled.xml
new file mode 100644
index 0000000..8ddc04e
--- /dev/null
+++ b/res/drawable/ic_package_verifier_disabled.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FFEF6C00"
+        android:pathData="M12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_enabled.xml b/res/drawable/ic_package_verifier_enabled.xml
new file mode 100644
index 0000000..b46c5ea
--- /dev/null
+++ b/res/drawable/ic_package_verifier_enabled.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#FF43A047"
+        android:pathData="M11.14,16l-3.84,-3.84l1.41,-1.42l2.43,2.42l4.16,-4.16l1.42,1.41L11.14,16zM12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_removed.xml b/res/drawable/ic_package_verifier_removed.xml
new file mode 100644
index 0000000..a8902ae
--- /dev/null
+++ b/res/drawable/ic_package_verifier_removed.xml
@@ -0,0 +1,24 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFF44336"
+        android:pathData="M15.5,9.91L14.09,8.5L12,10.59L9.91,8.5L8.5,9.91L10.59,12L8.5,14.09l1.41,1.41L12,13.42l2.09,2.08l1.41,-1.41L13.42,12L15.5,9.91zM12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2z"/>
+</vector>
diff --git a/res/drawable/ic_placeholder.xml b/res/drawable/ic_placeholder.xml
new file mode 100644
index 0000000..cc3801e
--- /dev/null
+++ b/res/drawable/ic_placeholder.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<!-- Transparent icon to be used as a placeholder for URI-based icons that take time to load. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/icon_size"
+    android:height="@dimen/icon_size"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="@android:color/transparent"
+        android:pathData="M 0 0 1 0 1 1 0 1 z"/>
+</vector>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c6bfc4b..247699f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -34,6 +34,8 @@
     <string name="keywords_display_night_display">dim screen, night, tint</string>
     <!-- Label for night mode toggle tile in quick setting [CHAR LIMIT=20] -->
     <string name="night_mode_tile_label">Night mode</string>
+    <!-- Empty placeholder string to be used while waiting for a string to be loaded asynchronously. [CHAR LIMIT=NONE] -->
+    <string name="empty_placeholder" translatable="false">&#160;</string>
 
     <!-- Network and internet settings [CHAR LIMIT=40] -->
     <string name="network_and_internet">Network &amp; internet</string>
diff --git a/src/com/android/car/settings/common/ExtraSettingsLoader.java b/src/com/android/car/settings/common/ExtraSettingsLoader.java
index ef59b3d..a0e387a 100644
--- a/src/com/android/car/settings/common/ExtraSettingsLoader.java
+++ b/src/com/android/car/settings/common/ExtraSettingsLoader.java
@@ -18,8 +18,12 @@
 
 import static com.android.settingslib.drawer.CategoryKey.CATEGORY_DEVICE;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
 
 import android.app.ActivityManager;
 import android.content.Context;
@@ -32,6 +36,7 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 
+import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 
 import com.android.car.apps.common.util.Themes;
@@ -49,14 +54,21 @@
 public class ExtraSettingsLoader {
     private static final Logger LOG = new Logger(ExtraSettingsLoader.class);
     private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category";
-    private Map<Preference, Bundle> mPreferenceBundleMap;
     private final Context mContext;
+    private Map<Preference, Bundle> mPreferenceBundleMap;
+    private PackageManager mPm;
 
     public ExtraSettingsLoader(Context context) {
         mContext = context;
+        mPm = context.getPackageManager();
         mPreferenceBundleMap = new HashMap<>();
     }
 
+    @VisibleForTesting
+    void setPackageManager(PackageManager pm) {
+        mPm = pm;
+    }
+
     /**
      * Returns a map of {@link Preference} and {@link Bundle} representing settings injected from
      * system apps and their metadata. The given intent must specify the action to use for
@@ -66,8 +78,7 @@
      * @param intent intent specifying the extra settings category to load
      */
     public Map<Preference, Bundle> loadPreferences(Intent intent) {
-        PackageManager pm = mContext.getPackageManager();
-        List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
+        List<ResolveInfo> results = mPm.queryIntentActivitiesAsUser(intent,
                 PackageManager.GET_META_DATA, ActivityManager.getCurrentUser());
 
         String extraCategory = intent.getStringExtra(META_DATA_PREFERENCE_CATEGORY);
@@ -76,40 +87,32 @@
                 // Do not allow any app to be added to settings, only system ones.
                 continue;
             }
+            String key = null;
             String title = null;
             String summary = null;
             String category = null;
             ActivityInfo activityInfo = resolved.activityInfo;
             Bundle metaData = activityInfo.metaData;
             try {
-                Resources res = pm.getResourcesForApplication(activityInfo.packageName);
-                if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
-                    if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
-                        title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
-                    } else {
-                        title = metaData.getString(META_DATA_PREFERENCE_TITLE);
+                Resources res = mPm.getResourcesForApplication(activityInfo.packageName);
+                if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) {
+                    key = extractMetaDataString(metaData, META_DATA_PREFERENCE_KEYHINT, res);
+                }
+                if (!metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
+                    title = extractMetaDataString(metaData, META_DATA_PREFERENCE_TITLE, res);
+                    if (TextUtils.isEmpty(title)) {
+                        LOG.d("no title.");
+                        title = activityInfo.loadLabel(mPm).toString();
                     }
                 }
-                if (TextUtils.isEmpty(title)) {
-                    LOG.d("no title.");
-                    title = activityInfo.loadLabel(pm).toString();
-                }
-                if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
-                    if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
-                        summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
-                    } else {
-                        summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
+                if (!metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+                    summary = extractMetaDataString(metaData, META_DATA_PREFERENCE_SUMMARY, res);
+                    if (TextUtils.isEmpty(summary)) {
+                        LOG.d("no description.");
                     }
-                } else {
-                    LOG.d("no description.");
                 }
-                if (metaData.containsKey(META_DATA_PREFERENCE_CATEGORY)) {
-                    if (metaData.get(META_DATA_PREFERENCE_CATEGORY) instanceof Integer) {
-                        category = res.getString(metaData.getInt(META_DATA_PREFERENCE_CATEGORY));
-                    } else {
-                        category = metaData.getString(META_DATA_PREFERENCE_CATEGORY);
-                    }
-                } else {
+                category = extractMetaDataString(metaData, META_DATA_PREFERENCE_CATEGORY, res);
+                if (TextUtils.isEmpty(category)) {
                     LOG.d("no category.");
                 }
             } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
@@ -119,7 +122,7 @@
             if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
                 int iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
                 icon = Icon.createWithResource(activityInfo.packageName, iconRes);
-            } else {
+            } else if (!metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
                 icon = Icon.createWithResource(mContext, R.drawable.ic_settings_gear);
                 LOG.d("use default icon.");
             }
@@ -136,14 +139,33 @@
             CarUiPreference preference = new CarUiPreference(mContext);
             preference.setTitle(title);
             preference.setSummary(summary);
+            if (key != null) {
+                preference.setKey(key);
+            }
             if (icon != null) {
                 preference.setIcon(icon.loadDrawable(mContext));
-                preference.getIcon().setTintList(
-                        Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+                if (ExtraSettingsUtil.isIconTintable(metaData)) {
+                    preference.getIcon().setTintList(
+                            Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+                }
             }
             preference.setIntent(extraSettingIntent);
             mPreferenceBundleMap.put(preference, metaData);
         }
         return mPreferenceBundleMap;
     }
+
+    /**
+     * Extracts the value in the metadata specified by the key.
+     * If it is resource, resolve the string and return. Otherwise, return the string itself.
+     */
+    private String extractMetaDataString(Bundle metaData, String key, Resources res) {
+        if (metaData.containsKey(key)) {
+            if (metaData.get(key) instanceof Integer) {
+                return res.getString(metaData.getInt(key));
+            }
+            return metaData.getString(key);
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java b/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
index 82e222c..480c22b 100644
--- a/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
+++ b/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
@@ -16,15 +16,42 @@
 
 package com.android.car.settings.common;
 
+import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY;
+import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE;
+import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+
 import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.IContentProvider;
 import android.content.Intent;
+import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Pair;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
 
+import com.android.car.apps.common.util.Themes;
+import com.android.car.settings.R;
+import com.android.settingslib.drawer.TileUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -50,16 +77,23 @@
  */
 // TODO: investigate using SettingsLib Tiles.
 public class ExtraSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
+    private static final Logger LOG = new Logger(ExtraSettingsPreferenceController.class);
 
     @VisibleForTesting
     static final String META_DATA_DISTRACTION_OPTIMIZED = "distractionOptimized";
 
+    private Context mContext;
+    private ContentResolver mContentResolver;
     private ExtraSettingsLoader mExtraSettingsLoader;
     private boolean mSettingsLoaded;
+    @VisibleForTesting
+    List<DynamicDataObserver> mObservers = new ArrayList<>();
 
     public ExtraSettingsPreferenceController(Context context, String preferenceKey,
             FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
         super(context, preferenceKey, fragmentController, restrictionInfo);
+        mContext = context;
+        mContentResolver = context.getContentResolver();
         mExtraSettingsLoader = new ExtraSettingsLoader(context);
     }
 
@@ -109,6 +143,20 @@
         preference.setVisible(preference.getPreferenceCount() > 0);
     }
 
+    @Override
+    protected void onStartInternal() {
+        mObservers.forEach(observer -> {
+            observer.register(mContentResolver, /* register= */ true);
+        });
+    }
+
+    @Override
+    protected void onStopInternal() {
+        mObservers.forEach(observer -> {
+            observer.register(mContentResolver, /* register= */ false);
+        });
+    }
+
     /**
      * Adds the extra settings from the system based on the intent that is passed in the preference
      * group. All the preferences that resolve these intents will be added in the preference group.
@@ -125,7 +173,148 @@
                         metaData.getBoolean(META_DATA_DISTRACTION_OPTIMIZED);
             }
             setting.getExtras().putBoolean(META_DATA_DISTRACTION_OPTIMIZED, distractionOptimized);
+            setting.getExtras().putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE,
+                    ExtraSettingsUtil.isIconTintable(metaData));
+            getDynamicData(setting, metaData);
             getPreference().addPreference(setting);
         }
     }
+
+    /**
+     * Retrieve dynamic injected preference data and create observers for updates.
+     */
+    protected void getDynamicData(Preference preference, Bundle metaData) {
+        if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
+            // Set a placeholder title before starting to fetch real title to prevent vertical
+            // preference shift.
+            preference.setTitle(R.string.empty_placeholder);
+            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_TITLE_URI,
+                    METHOD_GET_DYNAMIC_TITLE);
+            refreshTitle(uri, preference);
+            mObservers.add(new DynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference));
+        }
+        if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+            // Set a placeholder summary before starting to fetch real summary to prevent vertical
+            // preference shift.
+            preference.setSummary(R.string.empty_placeholder);
+            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_SUMMARY_URI,
+                    METHOD_GET_DYNAMIC_SUMMARY);
+            refreshSummary(uri, preference);
+            mObservers.add(new DynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference));
+        }
+        if (metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
+            // Set a placeholder icon before starting to fetch real icon to prevent horizontal
+            // preference shift.
+            preference.setIcon(R.drawable.ic_placeholder);
+            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_ICON_URI,
+                    METHOD_GET_PROVIDER_ICON);
+            refreshIcon(uri, preference);
+            mObservers.add(new DynamicDataObserver(METHOD_GET_PROVIDER_ICON, uri, preference));
+        }
+    }
+
+    @VisibleForTesting
+    void executeBackgroundTask(Runnable r) {
+        ThreadUtils.postOnBackgroundThread(r);
+    }
+
+    @VisibleForTesting
+    void executeUiTask(Runnable r) {
+        ThreadUtils.postOnMainThread(r);
+    }
+
+    private void refreshTitle(Uri uri, Preference preference) {
+        executeBackgroundTask(() -> {
+            Map<String, IContentProvider> providerMap = new ArrayMap<>();
+            String titleFromUri = TileUtils.getTextFromUri(
+                    mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
+            if (!TextUtils.equals(titleFromUri, preference.getTitle())) {
+                executeUiTask(() -> preference.setTitle(titleFromUri));
+            }
+        });
+    }
+
+    private void refreshSummary(Uri uri, Preference preference) {
+        executeBackgroundTask(() -> {
+            Map<String, IContentProvider> providerMap = new ArrayMap<>();
+            String summaryFromUri = TileUtils.getTextFromUri(
+                    mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
+            if (!TextUtils.equals(summaryFromUri, preference.getSummary())) {
+                executeUiTask(() -> preference.setSummary(summaryFromUri));
+            }
+        });
+    }
+
+    private void refreshIcon(Uri uri, Preference preference) {
+        executeBackgroundTask(() -> {
+            Intent intent = preference.getIntent();
+            String packageName = null;
+            if (!TextUtils.isEmpty(intent.getPackage())) {
+                packageName = intent.getPackage();
+            } else if (intent.getComponent() != null) {
+                packageName = intent.getComponent().getPackageName();
+            }
+            Map<String, IContentProvider> providerMap = new ArrayMap<>();
+            Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
+                    mContext, packageName, uri, providerMap);
+            Icon icon;
+            if (iconInfo != null) {
+                icon = Icon.createWithResource(iconInfo.first, iconInfo.second);
+            } else {
+                LOG.w("Failed to get icon from uri " + uri);
+                icon = Icon.createWithResource(mContext, R.drawable.ic_settings_gear);
+                LOG.d("use default icon.");
+            }
+            if (icon != null) {
+                executeUiTask(() -> {
+                    preference.setIcon(icon.loadDrawable(mContext));
+                    if (preference.getExtras().getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE)) {
+                        preference.getIcon().setTintList(
+                                Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * Observer for updating injected dynamic data.
+     */
+    private class DynamicDataObserver extends ContentObserver {
+        private final String mMethod;
+        private final Uri mUri;
+        private final Preference mPreference;
+
+        DynamicDataObserver(String method, Uri uri, Preference preference) {
+            super(new Handler(Looper.getMainLooper()));
+            mMethod = method;
+            mUri = uri;
+            mPreference = preference;
+        }
+
+        /** Registers or unregisters this observer to the given content resolver. */
+        void register(ContentResolver cr, boolean register) {
+            if (register) {
+                cr.registerContentObserver(mUri, /* notifyForDescendants= */ false,
+                        /* observer= */ this);
+            } else {
+                cr.unregisterContentObserver(this);
+            }
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            switch (mMethod) {
+                case METHOD_GET_DYNAMIC_TITLE:
+                    refreshTitle(mUri, mPreference);
+                    break;
+                case METHOD_GET_DYNAMIC_SUMMARY:
+                    refreshSummary(mUri, mPreference);
+                    break;
+                case METHOD_GET_PROVIDER_ICON:
+                    refreshIcon(mUri, mPreference);
+                    break;
+            }
+        }
+    }
 }
diff --git a/src/com/android/car/settings/common/ExtraSettingsUtil.java b/src/com/android/car/settings/common/ExtraSettingsUtil.java
new file mode 100644
index 0000000..7937fdf
--- /dev/null
+++ b/src/com/android/car/settings/common/ExtraSettingsUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 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.settings.common;
+
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/** Contains utility functions for injected settings. */
+public class ExtraSettingsUtil {
+    private static final Logger LOG = new Logger(ExtraSettingsUtil.class);
+
+    /**
+     * Returns whether or not an icon is tintable given the injected setting metadata.
+     */
+    public static boolean isIconTintable(Bundle metaData) {
+        if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
+            return metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
+        }
+        return false;
+    }
+
+    /**
+     * Returns the complete uri from the meta data key of the injected setting metadata.
+     *
+     * A complete uri should contain at least one path segment and be one of the following types:
+     *      content://authority/method
+     *      content://authority/method/key
+     *
+     * If the uri from the tile is not complete, build a uri by the default method and the
+     * preference key.
+     */
+    public static Uri getCompleteUri(Bundle metaData, String metaDataKey, String defaultMethod) {
+        String uriString = metaData.getString(metaDataKey);
+        if (TextUtils.isEmpty(uriString)) {
+            return null;
+        }
+
+        Uri uri = Uri.parse(uriString);
+        List<String> pathSegments = uri.getPathSegments();
+        if (pathSegments != null && !pathSegments.isEmpty()) {
+            return uri;
+        }
+
+        String key = metaData.getString(META_DATA_PREFERENCE_KEYHINT);
+        if (TextUtils.isEmpty(key)) {
+            LOG.w("Please specify the meta-data " + META_DATA_PREFERENCE_KEYHINT
+                    + " in AndroidManifest.xml for " + uriString);
+            return buildUri(uri.getAuthority(), defaultMethod);
+        }
+        return buildUri(uri.getAuthority(), defaultMethod, key);
+    }
+
+    private static Uri buildUri(String authority, String method, String key) {
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(authority)
+                .appendPath(method)
+                .appendPath(key)
+                .build();
+    }
+
+    private static Uri buildUri(String authority, String method) {
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(authority)
+                .appendPath(method)
+                .build();
+    }
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 86ffe81..d240ed1 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -22,6 +22,12 @@
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
+
+        <provider
+            android:name="com.android.car.settings.testutils.TestContentProvider"
+            android:authorities="com.android.car.settings.testutils.TestContentProvider"
+            android:exported="true">
+        </provider>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/unit/res/drawable/test_icon.xml b/tests/unit/res/drawable/test_icon.xml
new file mode 100644
index 0000000..93ab1a7
--- /dev/null
+++ b/tests/unit/res/drawable/test_icon.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#ffffff"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java b/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java
new file mode 100644
index 0000000..6f6703a
--- /dev/null
+++ b/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 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.settings.common;
+
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class ExtraSettingsLoaderTest {
+    private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category";
+    private static final String FAKE_CATEGORY = "fake_category";
+    private static final String FAKE_TITLE = "fake_title";
+    private static final String FAKE_SUMMARY = "fake_summary";
+    private static final String FAKE_CONTENT_PROVIDER = "content://android.content.FakeProvider";
+
+    private Context mContext = ApplicationProvider.getApplicationContext();
+    private ExtraSettingsLoader mExtraSettingsLoader;
+
+    @Mock
+    private PackageManager mPm;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mExtraSettingsLoader = new ExtraSettingsLoader(mContext);
+        mExtraSettingsLoader.setPackageManager(mPm);
+    }
+
+    @Test
+    public void testLoadPreference_uriResources_shouldNotLoadStaticResources() {
+        Intent intent = new Intent();
+        intent.putExtra(META_DATA_PREFERENCE_CATEGORY, FAKE_CATEGORY);
+        Bundle bundle = new Bundle();
+        bundle.putString(META_DATA_PREFERENCE_TITLE, FAKE_TITLE);
+        bundle.putString(META_DATA_PREFERENCE_SUMMARY, FAKE_SUMMARY);
+        bundle.putString(META_DATA_PREFERENCE_CATEGORY, FAKE_CATEGORY);
+        bundle.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_CONTENT_PROVIDER);
+        bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_CONTENT_PROVIDER);
+        bundle.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_CONTENT_PROVIDER);
+
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.metaData = bundle;
+        activityInfo.packageName = "package_name";
+        activityInfo.name = "class_name";
+
+        ResolveInfo resolveInfoSystem = new ResolveInfo();
+        resolveInfoSystem.system = true;
+        resolveInfoSystem.activityInfo = activityInfo;
+
+        when(mPm.queryIntentActivitiesAsUser(eq(intent), eq(PackageManager.GET_META_DATA),
+                anyInt())).thenReturn(Collections.singletonList(resolveInfoSystem));
+        Map<Preference, Bundle> preferenceToBundleMap = mExtraSettingsLoader.loadPreferences(
+                intent);
+
+        assertThat(preferenceToBundleMap).hasSize(1);
+
+        for (Preference p : preferenceToBundleMap.keySet()) {
+            assertThat(p.getTitle()).isNull();
+            assertThat(p.getSummary()).isNull();
+            assertThat(p.getIcon()).isNull();
+        }
+    }
+}
diff --git a/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java b/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
index 62ded8b..6c7d36b 100644
--- a/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
@@ -17,7 +17,14 @@
 package com.android.car.settings.common;
 
 import static com.android.car.settings.common.ExtraSettingsPreferenceController.META_DATA_DISTRACTION_OPTIMIZED;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -25,6 +32,7 @@
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 
 import androidx.lifecycle.LifecycleOwner;
@@ -35,7 +43,9 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.car.settings.R;
 import com.android.car.settings.testutils.ResourceTestUtils;
+import com.android.car.settings.testutils.TestContentProvider;
 import com.android.car.ui.preference.CarUiPreference;
 import com.android.car.ui.preference.DisabledPreferenceCallback;
 import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -43,6 +53,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
@@ -65,6 +76,12 @@
             new CarUxRestrictions.Builder(/* reqOpt= */ false,
                     CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build();
 
+    // Fake provider that won't actually resolve to anything
+    private static final String FAKE_PROVIDER = "content://android.content.FakeProvider";
+    // Real test provider
+    private static final String TEST_PROVIDER =
+            "content://com.android.car.settings.testutils.TestContentProvider";
+
     private LifecycleOwner mLifecycleOwner;
     private Lifecycle mLifecycle;
 
@@ -73,7 +90,8 @@
     private PreferenceScreen mScreen;
     private FakeExtraSettingsPreferenceController mPreferenceController;
     private CarUiPreference mPreference;
-    private Map<Preference, Bundle> mPreferenceBundleMap = new HashMap<>();
+    private Map<Preference, Bundle> mPreferenceBundleMap;
+    private Bundle mMetaData;
 
     @Mock
     private FragmentController mFragmentController;
@@ -97,18 +115,20 @@
         mScreen.setIntent(FAKE_INTENT);
         mPreferenceController.setPreference(mScreen);
         mPreference = spy(new CarUiPreference(mContext));
+        mPreference.setIntent(new Intent().setPackage("com.android.car.settings"));
 
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
         mPreferenceBundleMap = new HashMap<>();
-        mPreferenceBundleMap.put(mPreference, bundle);
-        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
-                mPreferenceBundleMap);
-        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+        mMetaData = new Bundle();
     }
 
     @Test
     public void onUxRestrictionsChanged_restricted_restrictedMessageSet() {
+        mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
         mPreferenceController.onCreate(mLifecycleOwner);
 
         Mockito.reset(mPreference);
@@ -121,6 +141,12 @@
 
     @Test
     public void onUxRestrictionsChanged_unrestricted_restrictedMessageUnset() {
+        mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
         mPreferenceController.onCreate(mLifecycleOwner);
 
         Mockito.reset(mPreference);
@@ -132,6 +158,12 @@
 
     @Test
     public void onUxRestrictionsChanged_restricted_viewOnly_restrictedMessageUnset() {
+        mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
         mPreferenceController.setAvailabilityStatus(PreferenceController.AVAILABLE_FOR_VIEWING);
         mPreferenceController.onCreate(mLifecycleOwner);
 
@@ -142,6 +174,135 @@
                 .setMessageToShowWhenDisabledPreferenceClicked("");
     }
 
+    @Test
+    public void onCreate_hasDynamicTitleData_placeholderAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        verify(mPreference).setTitle(
+                ResourceTestUtils.getString(mContext, "empty_placeholder"));
+    }
+
+    @Test
+    public void onCreate_hasDynamicSummaryData_placeholderAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        verify(mPreference).setSummary(
+                ResourceTestUtils.getString(mContext, "empty_placeholder"));
+    }
+
+    @Test
+    public void onCreate_hasDynamicIconData_placeholderAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        verify(mPreference).setIcon(R.drawable.ic_placeholder);
+    }
+
+    @Test
+    public void onCreate_hasDynamicTitleData_TitleSet() {
+        mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI,
+                TEST_PROVIDER + "/getText/textKey");
+
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        assertThat(mPreference.getTitle()).isEqualTo(TestContentProvider.TEST_TEXT_CONTENT);
+    }
+
+    @Test
+    public void onCreate_hasDynamicSummaryData_summarySet() {
+        mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI,
+                TEST_PROVIDER + "/getText/textKey");
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        assertThat(mPreference.getSummary()).isEqualTo(TestContentProvider.TEST_TEXT_CONTENT);
+    }
+
+    @Test
+    public void onCreate_hasDynamicIconData_iconSet() {
+        mMetaData.putString(META_DATA_PREFERENCE_ICON_URI,
+                TEST_PROVIDER + "/getIcon/iconKey");
+
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+
+        InOrder inOrder = inOrder(mPreference);
+        inOrder.verify(mPreference).setIcon(R.drawable.ic_placeholder);
+        inOrder.verify(mPreference).setIcon(any(Drawable.class));
+    }
+
+    @Test
+    public void onStart_hasDynamicTitleData_observerAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+        mPreferenceController.onStart(mLifecycleOwner);
+
+        assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+    }
+
+    @Test
+    public void onStart_hasDynamicSummaryData_observerAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+        mPreferenceController.onStart(mLifecycleOwner);
+
+        assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+    }
+
+    @Test
+    public void onStart_hasDynamicIconData_observerAdded() {
+        mMetaData.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_PROVIDER);
+        mPreferenceBundleMap.put(mPreference, mMetaData);
+        when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+                mPreferenceBundleMap);
+        mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+        mPreferenceController.onCreate(mLifecycleOwner);
+        mPreferenceController.onStart(mLifecycleOwner);
+
+        assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+    }
+
     private static class FakeExtraSettingsPreferenceController extends
             ExtraSettingsPreferenceController {
 
@@ -154,6 +315,18 @@
         }
 
         @Override
+        void executeBackgroundTask(Runnable r) {
+            // run task immediately on main thread
+            r.run();
+        }
+
+        @Override
+        void executeUiTask(Runnable r) {
+            // run task immediately on main thread
+            r.run();
+        }
+
+        @Override
         protected int getAvailabilityStatus() {
             return mAvailabilityStatus;
         }
diff --git a/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java b/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java
new file mode 100644
index 0000000..0e7f994
--- /dev/null
+++ b/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 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.settings.testutils;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/**
+ * A simple content provider for tests.  This provider runs in the same process as the test.
+ */
+public class TestContentProvider extends ContentProvider {
+
+    public static final String TEST_TEXT_CONTENT = "TestContentProviderText";
+
+    private static final String SCHEME = "content";
+    private static final String AUTHORITY =
+            "com.android.car.settings.testutils.TestContentProvider";
+    private static final String METHOD_GET_TEXT = "getText";
+    private static final String METHOD_GET_ICON = "getIcon";
+    private static final String KEY_PREFERENCE_TITLE = "com.android.settings.title";
+    private static final String KEY_PREFERENCE_SUMMARY = "com.android.settings.summary";
+    private static final String KEY_PREFERENCE_ICON = "com.android.settings.icon";
+    private static final String KEY_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";
+    private static final String TEXT_KEY = "textKey";
+    private static final String ICON_KEY = "iconKey";
+
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public Bundle call(String methodName, String uriString, Bundle extras) {
+        if (TextUtils.isEmpty(methodName)) {
+            return null;
+        }
+        if (TextUtils.isEmpty(uriString)) {
+            return null;
+        }
+        Uri uri = Uri.parse(uriString);
+        if (uri == null) {
+            return null;
+        }
+        // Only process URIs with valid scheme, authority, and no assigned port.
+        if (!(SCHEME.equals(uri.getScheme())
+                && AUTHORITY.equals(uri.getAuthority())
+                && (uri.getPort() == -1))) {
+            return null;
+        }
+        List<String> pathSegments = uri.getPathSegments();
+        // Path segments should consist of the method name and the argument. If the number of path
+        // segments is not exactly two, the content provider is not being called as intended.
+        if ((pathSegments == null) || (pathSegments.size() != 2)) {
+            return null;
+        }
+        // The first path segment needs to match the methodName.
+        if (!methodName.equals(pathSegments.get(0))) {
+            return null;
+        }
+        String key = pathSegments.get(1);
+        if (METHOD_GET_TEXT.equals(methodName)) {
+            if (TEXT_KEY.equals(key)) {
+                Bundle bundle = new Bundle();
+                bundle.putString(KEY_PREFERENCE_TITLE, TEST_TEXT_CONTENT);
+                bundle.putString(KEY_PREFERENCE_SUMMARY, TEST_TEXT_CONTENT);
+                return bundle;
+            }
+        } else if (METHOD_GET_ICON.equals(methodName)) {
+            if (ICON_KEY.equals(key)) {
+                try {
+                    String packageName = getContext().getPackageName();
+                    PackageManager manager = getContext().getPackageManager();
+                    Resources resources = manager.getResourcesForApplication(packageName);
+                    int iconRes = resources.getIdentifier("test_icon", "drawable",
+                            packageName);
+                    if (iconRes == 0) {
+                        return null;
+                    }
+
+                    Bundle bundle = new Bundle();
+                    bundle.putInt(KEY_PREFERENCE_ICON, iconRes);
+                    bundle.putString(KEY_PREFERENCE_ICON_PACKAGE, packageName);
+                    return bundle;
+
+                } catch (PackageManager.NameNotFoundException e) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+}