Snap for 10447354 from 8f99a063b20dd5a18da30d1f1b09568bf5624117 to mainline-cellbroadcast-release

Change-Id: I36f44a5bfbb4adf7a4773c05355ff8378debaece
diff --git a/ClusterHomeSample/AndroidManifest.xml b/ClusterHomeSample/AndroidManifest.xml
index 1dc7eb5..b2ca15f 100644
--- a/ClusterHomeSample/AndroidManifest.xml
+++ b/ClusterHomeSample/AndroidManifest.xml
@@ -21,6 +21,8 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <!-- for IActivityManager.registerTaskStackListener() -->
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+    <!-- for PackageManager.queryIntentActivitiesAsUser() -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <!-- for ClusterHomeManager -->
     <uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/>
     <!-- for CarInputManager.requestInputEventCapture() -->
diff --git a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
index c85fa41..2e656a2 100644
--- a/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
+++ b/ClusterHomeSample/src/com/android/car/cluster/home/ClusterHomeApplication.java
@@ -35,6 +35,8 @@
 import android.app.TaskInfo;
 import android.app.TaskStackListener;
 import android.car.Car;
+import android.car.CarAppFocusManager;
+import android.car.CarAppFocusManager.OnAppFocusChangedListener;
 import android.car.CarOccupantZoneManager;
 import android.car.cluster.ClusterActivityState;
 import android.car.cluster.ClusterHomeManager;
@@ -46,40 +48,47 @@
 import android.car.user.UserLifecycleEventFilter;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.ArraySet;
 import android.util.Log;
 import android.view.Display;
 import android.view.KeyEvent;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public final class ClusterHomeApplication extends Application {
     public static final String TAG = "ClusterHome";
-    private static final boolean DBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int UI_TYPE_HOME = UI_TYPE_CLUSTER_HOME;
     private static final int UI_TYPE_MAPS = UI_TYPE_HOME + 1;
     private static final int UI_TYPE_MUSIC = UI_TYPE_HOME + 2;
     private static final int UI_TYPE_PHONE = UI_TYPE_HOME + 3;
     private static final int UI_TYPE_START = UI_TYPE_MAPS;
 
-    private static final byte HOME_AVAILABILITY = 1;
-    private static final byte MAPS_AVAILABILITY = 1;
-    private static final byte PHONE_AVAILABILITY = 1;
-    private static final byte MUSIC_AVAILABILITY = 1;
+    private static final byte UI_UNAVAILABLE = 0;
+    private static final byte UI_AVAILABLE = 1;
 
+    private PackageManager mPackageManager;
     private IActivityTaskManager mAtm;
     private InputManager mInputManager;
     private ClusterHomeManager mHomeManager;
     private CarUserManager mUserManager;
     private CarInputManager mCarInputManager;
+    private CarAppFocusManager mAppFocusManager;
     private ClusterState mClusterState;
     private byte mUiAvailability[];
     private int mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_STARTING;
 
-    private ComponentName[] mClusterActivities;
+    private ArrayList<ComponentName> mClusterActivities = new ArrayList<>();
+    private int mDefaultClusterActivitySize = 0;
 
     private int mLastLaunchedUiType = UI_TYPE_CLUSTER_NONE;
     private int mLastReportedUiType = UI_TYPE_CLUSTER_NONE;
@@ -87,15 +96,16 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        mClusterActivities = new ComponentName[] {
-                new ComponentName(getApplicationContext(), ClusterHomeActivity.class),
-                ComponentName.unflattenFromString(
-                        getString(R.string.config_clusterMapActivity)),
-                ComponentName.unflattenFromString(
-                        getString(R.string.config_clusterMusicActivity)),
-                ComponentName.unflattenFromString(
-                        getString(R.string.config_clusterPhoneActivity)),
-        };
+        mClusterActivities.add(UI_TYPE_HOME,
+                new ComponentName(getApplicationContext(), ClusterHomeActivity.class));
+        mClusterActivities.add(UI_TYPE_MAPS,
+                ComponentName.unflattenFromString(getString(R.string.config_clusterMapActivity)));
+        mClusterActivities.add(UI_TYPE_MUSIC,
+                ComponentName.unflattenFromString(getString(R.string.config_clusterMusicActivity)));
+        mClusterActivities.add(UI_TYPE_PHONE,
+                ComponentName.unflattenFromString(getString(R.string.config_clusterPhoneActivity)));
+        mDefaultClusterActivitySize = mClusterActivities.size();
+        mPackageManager = getApplicationContext().getPackageManager();
         mAtm = ActivityTaskManager.getService();
         try {
             mAtm.registerTaskStackListener(mTaskStackListener);
@@ -111,6 +121,8 @@
                     mHomeManager = (ClusterHomeManager) car.getCarManager(Car.CLUSTER_HOME_SERVICE);
                     mUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
                     mCarInputManager = (CarInputManager) car.getCarManager(Car.CAR_INPUT_SERVICE);
+                    mAppFocusManager = (CarAppFocusManager) car.getCarManager(
+                            Car.APP_FOCUS_SERVICE);
                     initClusterHome();
                 });
     }
@@ -126,7 +138,7 @@
         if (!mClusterState.on) {
             mHomeManager.requestDisplay(UI_TYPE_HOME);
         }
-        mUiAvailability = buildUiAvailability();
+        mUiAvailability = buildUiAvailability(ActivityManager.getCurrentUser());
         mHomeManager.reportState(mClusterState.uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability);
         mHomeManager.registerClusterStateListener(getMainExecutor(), mClusterHomeCalback);
 
@@ -137,6 +149,9 @@
                 .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED).build();
         mUserManager.addListener(getMainExecutor(), filter, mUserLifecycleListener);
 
+        mAppFocusManager.addFocusListener(mAppFocusChangedListener,
+                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+
         int r = mCarInputManager.requestInputEventCapture(
                 DISPLAY_TYPE_INSTRUMENT_CLUSTER,
                 new int[]{CarInputManager.INPUT_TYPE_ALL_INPUTS},
@@ -175,7 +190,7 @@
             return;
         }
         mLastLaunchedUiType = uiType;
-        ComponentName activity = mClusterActivities[uiType];
+        ComponentName activity = mClusterActivities.get(uiType);
 
         Intent intent = new Intent(ACTION_MAIN).setComponent(activity);
         if (mClusterState.bounds != null && mClusterState.insets != null) {
@@ -195,17 +210,63 @@
         mHomeManager.startFixedActivityModeAsUser(intent, options.toBundle(), userId);
     }
 
-    private byte[] buildUiAvailability() {
-        // TODO(b/183115088): populate uiAvailability based on the package availability
-        return new byte[] {
-                HOME_AVAILABILITY, MAPS_AVAILABILITY, PHONE_AVAILABILITY, MUSIC_AVAILABILITY
-        };
+    private void add3PNavigationActivities(int currentUser) {
+        // Clean up the 3P Navigations from the previous user.
+        mClusterActivities.subList(mDefaultClusterActivitySize, mClusterActivities.size()).clear();
+
+        ArraySet<String> clusterPackages = new ArraySet<>();
+        for (int i = mDefaultClusterActivitySize - 1; i >= 0; --i) {
+            clusterPackages.add(mClusterActivities.get(i).getPackageName());
+        }
+        Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Car.CAR_CATEGORY_NAVIGATION);
+        List<ResolveInfo> resolveList = mPackageManager.queryIntentActivitiesAsUser(
+                intent, ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER),
+                UserHandle.of(currentUser));
+        for (int i = resolveList.size() - 1; i >= 0; --i) {
+            ActivityInfo activityInfo = resolveList.get(i).activityInfo;
+            if (DBG) Log.d(TAG, "Found: " + activityInfo.packageName + "/" + activityInfo.name);
+            // Some package can have multiple navigation Activities, we choose the default one only.
+            if (clusterPackages.contains(activityInfo.packageName)) {
+                if (DBG) {
+                    Log.d(TAG, "Skip this, because another Activity in the package is registered.");
+                };
+                continue;
+            }
+            mClusterActivities.add(new ComponentName(activityInfo.packageName, activityInfo.name));
+        }
+        mUiAvailability = buildUiAvailability(currentUser);
+    }
+
+    private byte[] buildUiAvailability(int currentUser) {
+        byte[] availability = new byte[mClusterActivities.size()];
+        Intent intent = new Intent(ACTION_MAIN);
+        for (int i = mClusterActivities.size() - 1; i >= 0; --i) {
+            ComponentName clusterActivity = mClusterActivities.get(i);
+            if (clusterActivity.getPackageName().equals(getPackageName())) {
+                // Assume that all Activities in ClusterHome are available.
+                availability[i] = UI_AVAILABLE;
+                continue;
+            }
+            intent.setComponent(clusterActivity);
+            ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(
+                    intent, PackageManager.MATCH_DEFAULT_ONLY, currentUser);
+            availability[i] = resolveInfo == null ? UI_UNAVAILABLE : UI_AVAILABLE;
+            if (DBG) {
+                Log.d(TAG, "availability=" + availability[i] + ", activity=" + clusterActivity
+                        + ", userId=" + currentUser);
+            }
+        }
+        return availability;
     }
 
     private final ClusterStateListener mClusterHomeCalback = new ClusterStateListener() {
         @Override
         public void onClusterStateChanged(
                 ClusterState state, @ClusterHomeManager.Config int changes) {
+            if (DBG) {
+                Log.d(TAG, "onClusterStateChanged: changes=" + Integer.toHexString(changes) +
+                        ", state=" + clusterStateToString(state));
+            }
             mClusterState = state;
             // We'll restart Activity when the display bounds or insets are changed, to let Activity
             // redraw itself to fit the changed attributes.
@@ -255,8 +316,8 @@
     }
 
     private int identifyTopTask(TaskInfo taskInfo) {
-        for (int i = mClusterActivities.length - 1; i >=0; --i) {
-            if (mClusterActivities[i].equals(taskInfo.topActivity)) {
+        for (int i = mClusterActivities.size() - 1; i >=0; --i) {
+            if (mClusterActivities.get(i).equals(taskInfo.topActivity)) {
                 return i;
             }
         }
@@ -269,9 +330,11 @@
         mUserLifeCycleEvent = event.getEventType();
         if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_STARTING) {
             startClusterActivity(UI_TYPE_HOME);
-        } else if (UI_TYPE_HOME != UI_TYPE_START
-                && mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
-            startClusterActivity(UI_TYPE_START);
+        } else if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
+            add3PNavigationActivities(event.getUserId());
+            if (UI_TYPE_START != UI_TYPE_HOME) {
+                startClusterActivity(UI_TYPE_START);
+            }
         }
     };
 
@@ -287,11 +350,74 @@
         if (DBG) Log.d(TAG, "onKeyEvent: " + keyEvent);
         if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MENU) {
             if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) return;
-            int nextUiType = (mLastLaunchedUiType + 1) % mUiAvailability.length;
+            int nextUiType;
+            do {
+                // Select the Cluster Activity within the preinstalled ones.
+                nextUiType = mLastLaunchedUiType + 1;
+                if (nextUiType >= mDefaultClusterActivitySize) nextUiType = 0;
+            } while (mUiAvailability[nextUiType] == UI_UNAVAILABLE);
             startClusterActivity(nextUiType);
             return;
         }
         // Use Android InputManager to forward KeyEvent.
         mInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_ASYNC);
     }
+
+    private OnAppFocusChangedListener mAppFocusChangedListener = new OnAppFocusChangedListener() {
+        @Override
+        public void onAppFocusChanged(int appType, boolean active) {
+            if (!active || appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) {
+                return;
+            }
+            int navigationUi = getFocusedNavigationUi();
+            if (navigationUi != UI_TYPE_CLUSTER_NONE) {
+                startClusterActivity(navigationUi);
+            }
+        }
+    };
+
+    private int getFocusedNavigationUi() {
+        List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner(
+                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+        if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) {
+            Log.e(TAG, "Can't find the navigation owner");
+            return UI_TYPE_CLUSTER_NONE;
+        }
+        for (int i = 0; i < focusOwnerPackageNames.size(); ++i) {
+            String focusOwnerPackage = focusOwnerPackageNames.get(i);
+            for (int j = mClusterActivities.size() - 1; j >= 0; --j) {
+                if (mUiAvailability[j] == UI_UNAVAILABLE) {
+                    continue;
+                }
+                if (mClusterActivities.get(j).getPackageName().equals(focusOwnerPackage)) {
+                    if (DBG) {
+                        Log.d(TAG, "Found focused NavigationUI: " + j
+                                + ", package=" + focusOwnerPackage);
+                    }
+                    return j;
+                }
+            }
+        }
+        Log.e(TAG, "Can't find the navigation UI for "
+                + String.join(", ", focusOwnerPackageNames) + ".");
+        return UI_TYPE_CLUSTER_NONE;
+    }
+
+    private static String clusterStateToString(ClusterState state) {
+        StringBuilder sb = new StringBuilder("ClusterState[");
+        sb.append("on="); sb.append(state.on);
+        if (state.bounds != null) {
+            sb.append(", bounds="); sb.append(state.bounds);
+        }
+        if (state.insets != null) {
+            sb.append(", insets="); sb.append(state.insets);
+        }
+        if (state.insets != null) {
+            sb.append(", insets="); sb.append(state.insets);
+        }
+        sb.append(", uiType="); sb.append(state.uiType);
+        sb.append(", displayId="); sb.append(state.displayId);
+        sb.append(']');
+        return sb.toString();
+    }
 }
diff --git a/ClusterOsDouble/AndroidManifest.xml b/ClusterOsDouble/AndroidManifest.xml
index 5e8d334..e57b260 100644
--- a/ClusterOsDouble/AndroidManifest.xml
+++ b/ClusterOsDouble/AndroidManifest.xml
@@ -13,9 +13,10 @@
      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.cluster.osdouble">
+    package="com.android.car.cluster.osdouble"
+    android:sharedUserId="android.uid.system">
+    <!-- system uid is required to access the local private display -->
 
     <!-- Required to show car sensor data -->
     <uses-permission android:name="android.car.permission.CAR_SPEED"/>
diff --git a/ClusterOsDouble/res/values/config.xml b/ClusterOsDouble/res/values/config.xml
index 7f1cb52..e85c206 100644
--- a/ClusterOsDouble/res/values/config.xml
+++ b/ClusterOsDouble/res/values/config.xml
@@ -18,5 +18,8 @@
 <!-- Resources to configure based on each OEM's preference. -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!--  DisplayPort to launch ClusterOsDouble -->
-    <integer name="config_clusterDisplayPort">1</integer>
+    <integer name="config_clusterDisplayPort">-1</integer>
+    <!--  Display uniqueId to launch ClusterOsDouble. -->
+    <!--  To use this values, set config_clusterDisplayPort to -1. -->
+    <string name="config_clusterDisplayUniqueId" />
 </resources>
diff --git a/ClusterOsDouble/res/values/overlayable.xml b/ClusterOsDouble/res/values/overlayable.xml
index 465c39f..329ffee 100644
--- a/ClusterOsDouble/res/values/overlayable.xml
+++ b/ClusterOsDouble/res/values/overlayable.xml
@@ -19,6 +19,7 @@
     <overlayable name="ClusterOsConfig">
         <policy type="product|system|vendor">
             <item type="integer" name="config_clusterDisplayPort" />
+            <item type="string" name="config_clusterDisplayUniqueId" />
         </policy>
     </overlayable>
 </resources>
diff --git a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java
index 8ba7eb3..f86b001 100644
--- a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java
+++ b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleActivity.java
@@ -20,23 +20,26 @@
 import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
 import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
 
 import static com.android.car.cluster.osdouble.ClusterOsDoubleApplication.TAG;
 
 import android.car.Car;
-import android.car.cluster.navigation.NavigationState.NavigationStateProto;
 import android.car.VehiclePropertyIds;
+import android.car.cluster.navigation.NavigationState.NavigationStateProto;
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyManager;
 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
+import android.content.res.CompatibilityInfo;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.os.Bundle;
 import android.util.ArrayMap;
+import android.util.DisplayMetrics;
+import android.util.IntArray;
 import android.util.Log;
+import android.view.DisplayInfo;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
@@ -83,19 +86,23 @@
     private static final int VENDOR_CLUSTER_DISPLAY_STATE = toVendorId(
             VehiclePropertyIds.CLUSTER_DISPLAY_STATE);
 
+    // For the detail, please refer to vehicle/2.0/types.hal.
+    private static final int REPORT_STATE_MAIN_UI_INDEX = 9;
+    private static final int REPORT_STATE_UI_AVAILABILITY_INDEX = 11;
+
     private DisplayManager mDisplayManager;
     private CarPropertyManager mPropertyManager;
 
     private SurfaceView mSurfaceView;
     private Rect mBounds;
     private Insets mInsets;
-    private VirtualDisplay mVirtualDisplay;
+    private static VirtualDisplay sVirtualDisplay;
 
     private ClusterViewModel mClusterViewModel;
     private final ArrayMap<Sensors.Gear, View> mGearsToIcon = new ArrayMap<>();
     private final ArrayList<View> mUiToButton = new ArrayList<>();
     int mCurrentUi = UI_TYPE_CLUSTER_HOME;
-    int mTotalUiSize;
+    private final IntArray mUiAvailability = new IntArray();
 
     private NavStateController mNavStateController;
 
@@ -143,15 +150,6 @@
                 findViewById(R.id.navigation_state), imageResolver);
     }
 
-    @Override
-    protected void onDestroy() {
-        if (mVirtualDisplay != null) {
-            mVirtualDisplay.release();
-            mVirtualDisplay = null;
-        }
-        super.onDestroy();
-    }
-
     private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() {
         @Override
         public void surfaceCreated(SurfaceHolder holder) {
@@ -173,10 +171,23 @@
             // Adds some empty space in the boundary of the display to verify if mBounds works.
             mBounds.inset(/* dx= */ 12, /* dy= */ 12);
             mInsets = Insets.of(obscuredWidth, obscuredHeight, obscuredWidth, obscuredHeight);
-            if (mVirtualDisplay == null) {
-                mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
+            if (sVirtualDisplay == null) {
+                sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
             } else {
-                mVirtualDisplay.setSurface(holder.getSurface());
+                DisplayInfo displayInfo = new DisplayInfo();
+                DisplayMetrics boundsMetrics = new DisplayMetrics();
+                boolean isDisplayValid = sVirtualDisplay.getDisplay().getDisplayInfo(displayInfo);
+                displayInfo.getLogicalMetrics(boundsMetrics,
+                        CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, /* configuration= */ null);
+                if (isDisplayValid && boundsMetrics.widthPixels == width
+                        && boundsMetrics.heightPixels == height) {
+                    sVirtualDisplay.setSurface(holder.getSurface());
+                } else {
+                    // Display was resized, delete existing and create new display.
+                    // TODO(b/254931119): Resize the display instead of replacing it.
+                    sVirtualDisplay.release();
+                    sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
+                }
             }
         }
 
@@ -185,7 +196,7 @@
             Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from"
                     + " display, surface: " + holder.getSurface());
             // detaching surface is similar to turning off the display
-            mVirtualDisplay.setSurface(null);
+            sVirtualDisplay.setSurface(null);
         }
     };
 
@@ -234,15 +245,17 @@
         if (values.length < 11) {
             throw new IllegalArgumentException("Insufficient size of CLUSTER_REPORT_STATE");
         }
-        // mainUI is the 10th element, refer to vehicle/2.0/types.hal.
-        int mainUi = (Integer) values[9];
-        if (mainUi >= 0 && mainUi < mTotalUiSize) {
-            selectUiButton(mainUi);
+        int mainUi = (Integer) values[REPORT_STATE_MAIN_UI_INDEX];
+        int totalUiSize = values.length - REPORT_STATE_UI_AVAILABILITY_INDEX;
+        mUiAvailability.resize(totalUiSize);
+        for (int i = 0; i < totalUiSize; ++i) {
+            mUiAvailability.set(i, (Byte) values[i + REPORT_STATE_UI_AVAILABILITY_INDEX]);
         }
+        selectUiButton(mainUi);
     }
 
     private void selectUiButton(int mainUi) {
-        for (int i = 0; i < mTotalUiSize; ++i) {
+        for (int i = mUiToButton.size() - 1; i >= 0; --i) {
             View button = mUiToButton.get(i);
             button.setSelected(i == mainUi);
         }
@@ -265,7 +278,6 @@
         sendDisplayState();
     }
 
-
     private static int toVendorId(int propId) {
         return (propId & ~MASK) | VENDOR;
     }
@@ -300,7 +312,6 @@
     private void registerUi(View view) {
         int currentUi = mUiToButton.size();
         mUiToButton.add(view);
-        mTotalUiSize = mUiToButton.size();
         view.setOnTouchListener((v, event) -> {
             if (event.getAction() == MotionEvent.ACTION_DOWN) {
                 Log.d(TAG, "onTouch: " + currentUi);
@@ -329,7 +340,12 @@
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         Log.d(TAG, "onKeyDown: " + keyCode);
         if (keyCode == KeyEvent.KEYCODE_MENU) {
-            switchUi((mCurrentUi + 1) % mTotalUiSize);
+            int nextUi = mCurrentUi;
+            do {
+                nextUi = nextUi + 1;
+                if (nextUi >= mUiToButton.size()) nextUi = 0;
+            } while (mUiAvailability.get(nextUi) == 0);
+            switchUi(nextUi);
             return true;
         }
         return super.onKeyDown(keyCode, event);
diff --git a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java
index dd102c7..4b63a9a 100644
--- a/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java
+++ b/ClusterOsDouble/src/com/android/car/cluster/osdouble/ClusterOsDoubleApplication.java
@@ -18,12 +18,16 @@
 
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 
+import android.annotation.NonNull;
 import android.app.ActivityOptions;
 import android.app.Application;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.Display;
 import android.view.DisplayAddress;
@@ -34,45 +38,126 @@
 public class ClusterOsDoubleApplication extends Application {
     public static final String TAG = "ClusterOsDouble";
 
-    private DisplayManager mDisplayManager;
-
     @Override
     public void onCreate() {
         super.onCreate();
         Context context = getApplicationContext();
-        mDisplayManager = context.getSystemService(DisplayManager.class);
         int displayPort = context.getResources().getInteger(R.integer.config_clusterDisplayPort);
-        if (displayPort == 0) {
-            Log.e(TAG, "Invalid resource: config_clusterDisplayPort");
-            // Won't throw the exception, if so, it'll restart the application continuously,
-            // because this is the persistent application.
-            return;
-        }
-        int displayId = findDisplay(displayPort);
-        if (displayId == Display.INVALID_DISPLAY) {
-            Log.e(TAG, "Can't find the display with portId: " + displayPort);
+        String displayUniqueId = context.getResources().getString(
+                R.string.config_clusterDisplayUniqueId);
+
+        if (displayPort <= 0 && TextUtils.isEmpty(displayUniqueId)) {
+            Log.e(TAG, "Cluster display isn't configured.");
             return;
         }
 
-        Intent intent = Intent.makeMainActivity(
-                ComponentName.createRelative(context, ClusterOsDoubleActivity.class.getName()));
-        intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
-
-        ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
-        context.startActivity(intent, options.toBundle());
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        ClusterDisplayMonitor clusterDisplayMonitor = new ClusterDisplayMonitor(context,
+                displayManager, displayPort, displayUniqueId);
+        clusterDisplayMonitor.start(new Handler(Looper.myLooper()));
     }
 
-    private int findDisplay(int displayPort) {
-        for (Display display : mDisplayManager.getDisplays()) {
-            DisplayAddress address = display.getAddress();
-            if (!(address instanceof DisplayAddress.Physical)) {
-                continue;
-            }
-            DisplayAddress.Physical physical = (DisplayAddress.Physical) address;
-            if (physical.getPort() == displayPort) {
-                return display.getDisplayId();
-            }
+    /**
+     * Monitors displays and starts the cluster activity when the correct display becomes available.
+     */
+    private static class ClusterDisplayMonitor {
+        private final Context mContext;
+        private final DisplayManager mDisplayManager;
+        private final int mDisplayPort;
+        private final String mDisplayUniqueId;
+
+        private final DisplayManager.DisplayListener mDisplayListener =
+                new DisplayManager.DisplayListener() {
+                    @Override
+                    public void onDisplayAdded(int displayId) {
+                        int clusterDisplayId = findClusterDisplayId();
+                        if (clusterDisplayId == displayId) {
+                            Log.d(TAG, "Display " + displayId + " was added. Starting cluster.");
+                            onDisplayReadyForCluster(displayId);
+                        }
+                    }
+
+                    @Override
+                    public void onDisplayRemoved(int displayId) {
+                        // No-op
+                    }
+
+                    @Override
+                    public void onDisplayChanged(int displayId) {
+                        // No-op
+                    }
+                };
+
+        public ClusterDisplayMonitor(Context context, DisplayManager displayManager,
+                int displayPort, String displayUniqueId) {
+            mContext = context;
+            mDisplayManager = displayManager;
+            mDisplayPort = displayPort;
+            mDisplayUniqueId = displayUniqueId;
         }
-        return Display.INVALID_DISPLAY;
+
+        public void start(Handler handler) {
+            int clusterDisplayId = findClusterDisplayId();
+            if (clusterDisplayId != Display.INVALID_DISPLAY) {
+                onDisplayReadyForCluster(clusterDisplayId);
+            }
+            // This listener will never get unregistered. This is only ok as long as this is a
+            // persistent app that is not expected to stop.
+            mDisplayManager.registerDisplayListener(mDisplayListener, handler);
+        }
+
+        private void onDisplayReadyForCluster(int displayId) {
+            Intent intent = Intent.makeMainActivity(
+                    ComponentName.createRelative(mContext,
+                            ClusterOsDoubleActivity.class.getName()));
+            intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+
+            ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
+            mContext.startActivity(intent, options.toBundle());
+        }
+
+        private int findClusterDisplayId() {
+            int displayId = Display.INVALID_DISPLAY;
+            if (mDisplayPort > 0) {
+                displayId = findDisplayByPort(mDisplayPort);
+                if (displayId == Display.INVALID_DISPLAY) {
+                    Log.e(TAG, "Can't find the display with portId: " + mDisplayPort);
+                }
+            } else if (!TextUtils.isEmpty(mDisplayUniqueId)) {
+                displayId = findDisplayIdByUniqueId(mDisplayUniqueId);
+                if (displayId == Display.INVALID_DISPLAY) {
+                    Log.e(TAG, "Can't find the display with uniqueId: " + mDisplayUniqueId);
+                }
+            } else {
+                // This should not ever happen.
+                Log.wtf(TAG, "No valid cluster display configs found.");
+            }
+
+            return displayId;
+        }
+
+        private int findDisplayIdByUniqueId(@NonNull String displayUniqueId) {
+            for (Display display : mDisplayManager.getDisplays()) {
+                if (displayUniqueId.equals(display.getUniqueId())) {
+                    return display.getDisplayId();
+                }
+            }
+            return Display.INVALID_DISPLAY;
+        }
+
+        private int findDisplayByPort(int displayPort) {
+            for (Display display : mDisplayManager.getDisplays()) {
+                DisplayAddress address = display.getAddress();
+                if (!(address instanceof DisplayAddress.Physical)) {
+                    continue;
+                }
+                DisplayAddress.Physical physical = (DisplayAddress.Physical) address;
+                if (physical.getPort() == displayPort) {
+                    return display.getDisplayId();
+                }
+            }
+            return Display.INVALID_DISPLAY;
+        }
     }
+
 }
diff --git a/DirectRenderingCluster/AndroidManifest.xml b/DirectRenderingCluster/AndroidManifest.xml
index 6f1ce2b..c4eaa87 100644
--- a/DirectRenderingCluster/AndroidManifest.xml
+++ b/DirectRenderingCluster/AndroidManifest.xml
@@ -20,7 +20,7 @@
     android:process="android.car.cluster"
     android:sharedUserId="android.uid.cluster">
 
-    <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="29"/>
+    <uses-sdk android:targetSdkVersion="30"/>
 
     <protected-broadcast android:name="android.car.cluster.NAVIGATION_STATE_UPDATE"/>
 
@@ -42,8 +42,11 @@
     <uses-permission android:name="android.permission.MANAGE_USERS" />
     <!-- Required to launch navigation apps -->
     <uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/>
+    <!-- Required to use CarOccupantZoneManager.getDisplayIdForDriver() -->
+    <uses-permission android:name="android.car.permission.ACCESS_PRIVATE_DISPLAY_ID"/>
     <!-- Required to watch activities running on the cluster -->
     <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>
+
     <!-- Required to show car sensor data -->
     <uses-permission android:name="android.car.permission.CAR_ENERGY"/>
     <uses-permission android:name="android.car.permission.CAR_POWERTRAIN"/>
@@ -53,7 +56,11 @@
     <!-- Required to query packages in Android 11+ -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
+    <!-- Required to use PhoneStateListener.onCallStateChanged() -->
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
+
 
     <application android:label="@string/app_name"
                  android:icon="@mipmap/ic_launcher"
diff --git a/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java b/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java
index 4050099..5949a50 100644
--- a/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java
+++ b/DirectRenderingCluster/src/android/car/cluster/ClusterDisplayProvider.java
@@ -18,7 +18,6 @@
 
 import android.car.Car;
 import android.car.CarOccupantZoneManager;
-import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.content.Context;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.util.Log;
@@ -26,8 +25,6 @@
 
 import com.android.internal.util.Preconditions;
 
-import java.util.List;
-
 /**
  * This class provides a display for instrument cluster renderer.
  * <p>
@@ -37,7 +34,7 @@
  */
 public class ClusterDisplayProvider {
     private static final String TAG = "Cluster.DisplayProvider";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final DisplayListener mListener;
     private final Car mCar;
@@ -66,35 +63,30 @@
         Preconditions.checkArgument(
                 occupantZoneManager != null,"Can't get CarOccupantZoneManager");
         mOccupantZoneManager = occupantZoneManager;
-        checkClusterDisplayAdded();
+        checkClusterDisplayChanged();
         mOccupantZoneManager.registerOccupantZoneConfigChangeListener(
                 new ClusterDisplayChangeListener());
     }
 
-    private void checkClusterDisplayAdded() {
-        Display clusterDisplay = getClusterDisplay();
-        if (clusterDisplay != null) {
-            Log.i(TAG, String.format("Found display: %s (id: %d, owner: %s)",
-                    clusterDisplay.getName(), clusterDisplay.getDisplayId(),
-                    clusterDisplay.getOwnerPackageName()));
-            mClusterDisplayId = clusterDisplay.getDisplayId();
-            mListener.onDisplayAdded(clusterDisplay.getDisplayId());
+    private void checkClusterDisplayChanged() {
+        int clusterDisplayId = getClusterDisplayId();
+        if (clusterDisplayId == mClusterDisplayId) {
+            return;
+        }
+        if (mClusterDisplayId != Display.INVALID_DISPLAY) {
+            Log.i(TAG, "Cluster display is removed");
+            mListener.onDisplayRemoved(mClusterDisplayId);
+        }
+        mClusterDisplayId = clusterDisplayId;
+        if (clusterDisplayId != Display.INVALID_DISPLAY) {
+            Log.i(TAG, "Found cluster displayId=" + clusterDisplayId);
+            mListener.onDisplayAdded(clusterDisplayId);
         }
     }
 
-    private Display getClusterDisplay() {
-        List<OccupantZoneInfo> zones = mOccupantZoneManager.getAllOccupantZones();
-        int zones_size = zones.size();
-        for (int i = 0; i < zones_size; ++i) {
-            OccupantZoneInfo zone = zones.get(i);
-            // Assumes that a Car has only one driver.
-            if (zone.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
-                return mOccupantZoneManager.getDisplayForOccupant(
-                        zone, CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
-            }
-        }
-        Log.e(TAG, "Can't find the OccupantZoneInfo for driver");
-        return null;
+    private int getClusterDisplayId() {
+        return mOccupantZoneManager.getDisplayIdForDriver(
+                CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
     }
 
     private final class ClusterDisplayChangeListener implements
@@ -105,15 +97,7 @@
             if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) == 0) {
                 return;
             }
-            if (mClusterDisplayId == Display.INVALID_DISPLAY) {
-                checkClusterDisplayAdded();
-            } else {
-                Display clusterDisplay = getClusterDisplay();
-                if (clusterDisplay == null) {
-                    mListener.onDisplayRemoved(mClusterDisplayId);
-                    mClusterDisplayId = Display.INVALID_DISPLAY;
-                }
-            }
+            checkClusterDisplayChanged();
         }
     }
 
diff --git a/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java b/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java
index 7412ac6..d02bab3 100644
--- a/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java
+++ b/DirectRenderingCluster/src/android/car/cluster/ClusterViewModel.java
@@ -15,12 +15,15 @@
  */
 package android.car.cluster;
 
+import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
+
 import android.annotation.Nullable;
 import android.app.Application;
 import android.car.Car;
 import android.car.CarAppFocusManager;
 import android.car.CarNotConnectedException;
 import android.car.VehicleAreaType;
+import android.car.VehiclePropertyIds;
 import android.car.cluster.sensors.Sensor;
 import android.car.cluster.sensors.Sensors;
 import android.car.hardware.CarPropertyValue;
@@ -131,7 +134,7 @@
                     }
                     for (Sensor<?> sensorId : Sensors.getInstance()
                             .getSensorsForPropertyId(value.getPropertyId())) {
-                        if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID
+                        if (sensorId.mAreaId == VEHICLE_AREA_TYPE_GLOBAL
                                 || (sensorId.mAreaId & value.getAreaId()) != 0) {
                             setSensorValue(sensorId, value);
                         }
@@ -217,19 +220,23 @@
      * Returns the current value of the sensor, directly from the VHAL.
      *
      * @param sensor sensor to read
-     * @param <V>    VHAL data type
      * @param <T>    data type of such sensor
      */
     @Nullable
     public <T> T getSensorValue(@NonNull Sensor<T> sensor) {
-        try {
-            CarPropertyValue<?> value = mCarPropertyManager
-                    .getProperty(sensor.mPropertyId, sensor.mAreaId);
-            return sensor.mAdapter.apply(value);
-        } catch (CarNotConnectedException ex) {
-            Log.e(TAG, "We got disconnected from Car Service", ex);
+        if (mCarPropertyManager == null) {
+            Log.e(TAG, "CarPropertyManager reference is null, car service is disconnected.");
             return null;
         }
+        CarPropertyValue<?> carPropertyValue = mCarPropertyManager.getProperty(sensor.mPropertyId,
+                sensor.mAreaId);
+        if (carPropertyValue == null) {
+            Log.w(TAG, "Property ID: " + VehiclePropertyIds.toString(sensor.mPropertyId)
+                    + " Area ID: 0x" + Integer.toHexString(sensor.mAreaId)
+                    + " returned null from CarPropertyManager#getProperty()");
+            return null;
+        }
+        return sensor.mAdapter.apply(carPropertyValue);
     }
 
     /**
diff --git a/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java b/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java
index 873fef3..ac17371 100644
--- a/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java
+++ b/DirectRenderingCluster/src/android/car/cluster/MusicFragment.java
@@ -85,6 +85,7 @@
         new MetadataController(
             getViewLifecycleOwner(),
             playbackViewModel,
+            null,
             title,
             subtitle,
             null,
@@ -95,7 +96,8 @@
             seekBar,
             albumIcon,
             null,
-            new Size(artSize, artSize)
+            new Size(artSize, artSize),
+            null
         );
 
         return view;
diff --git a/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java b/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java
index 25f6572..65dea7e 100644
--- a/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java
+++ b/DirectRenderingCluster/src/android/car/cluster/sensors/Sensors.java
@@ -15,6 +15,8 @@
  */
 package android.car.cluster.sensors;
 
+import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
+
 import android.car.VehiclePropertyIds;
 import android.car.VehiclePropertyType;
 import android.car.hardware.CarPropertyValue;
@@ -31,9 +33,6 @@
  * The collection of all sensors supported by this application.
  */
 public class Sensors {
-    /** Area identifier used for sensors corresponding to global VHAL properties */
-    public static final int GLOBAL_AREA_ID = -1;
-
     private static Sensors sInstance;
     private static List<Sensor<?>> sSensors = new ArrayList<>();
     private Map<Integer, List<Sensor<?>>> mSensorsByPropertyId = new HashMap<>();
@@ -48,35 +47,34 @@
 
     /** Fuel of the car, measured in millimeters */
     public static final Sensor<Float> SENSOR_FUEL = registerSensor(
-            "Fuel", VehiclePropertyIds.FUEL_LEVEL, GLOBAL_AREA_ID, VehiclePropertyType.FLOAT,
+            "Fuel", VehiclePropertyIds.FUEL_LEVEL, VEHICLE_AREA_TYPE_GLOBAL,
+            VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
     /** Fuel capacity of the car, measured in millimeters */
     public static final Sensor<Float> SENSOR_FUEL_CAPACITY = registerSensor(
-            "Fuel Capacity", VehiclePropertyIds.INFO_FUEL_CAPACITY, GLOBAL_AREA_ID,
+            "Fuel Capacity", VehiclePropertyIds.INFO_FUEL_CAPACITY, VEHICLE_AREA_TYPE_GLOBAL,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
     /** RPMs */
     public static final Sensor<Float> SENSOR_RPM = registerSensor(
-            "RPM", VehiclePropertyIds.ENGINE_RPM, GLOBAL_AREA_ID,
+            "RPM", VehiclePropertyIds.ENGINE_RPM, VEHICLE_AREA_TYPE_GLOBAL,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
     /** Fuel range in meters */
     public static final Sensor<Float> SENSOR_FUEL_RANGE = registerSensor(
-            "Fuel Range", VehiclePropertyIds.RANGE_REMAINING, GLOBAL_AREA_ID,
+            "Fuel Range", VehiclePropertyIds.RANGE_REMAINING, VEHICLE_AREA_TYPE_GLOBAL,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
     /** Speed in meters per second */
     public static final Sensor<Float> SENSOR_SPEED = registerSensor(
-            "Speed", VehiclePropertyIds.PERF_VEHICLE_SPEED, GLOBAL_AREA_ID,
+            "Speed", VehiclePropertyIds.PERF_VEHICLE_SPEED, VEHICLE_AREA_TYPE_GLOBAL,
             VehiclePropertyType.FLOAT,
             value -> (Float) value.getValue());
     /** Current gear of the car */
     public static final Sensor<Gear> SENSOR_GEAR = registerSensor(
-            "Gear", VehiclePropertyIds.GEAR_SELECTION, GLOBAL_AREA_ID, VehiclePropertyType.INT32,
+            "Gear", VehiclePropertyIds.GEAR_SELECTION, VEHICLE_AREA_TYPE_GLOBAL,
+            VehiclePropertyType.INT32,
             value -> {
-                if (value == null) {
-                    return null;
-                }
                 Integer gear = (Integer) value.getValue();
                 if ((gear & CarSensorEvent.GEAR_REVERSE) != 0) {
                     return Gear.REVERSE;