[automerger skipped] DO NOT MERGE: Prevent cluster service crash if video codec initialization fails am: 91e0357069 -s ours am: e268ad6100 am: 7e385ab97f -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Car/Cluster/+/11977380

Change-Id: I8b521a121df43f26529c03f9b7fd58a5e3903251
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..2103020
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//
+
+android_app {
+    name: "DirectRenderingCluster",
+
+    srcs: ["src/**/*.java"],
+
+    platform_apis: true,
+
+    // Each update should be signed by OEMs
+    certificate: "platform",
+    privileged: true,
+
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+        enabled: false,
+    },
+
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "android.car.cluster.navigation",
+        "androidx.legacy_legacy-support-v4",
+        "androidx-constraintlayout_constraintlayout",
+        "car-arch-common",
+        "car-media-common",
+        "car-telephony-common",
+        "car-apps-common",
+    ],
+
+    libs: ["android.car"],
+
+    required: ["privapp_whitelist_android.car.cluster"],
+
+    product_variables: {
+        pdk: {
+            enabled: false,
+        },
+    },
+}
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index 8da51c3..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-#
-ifneq ($(TARGET_BUILD_PDK), true)
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := DirectRenderingCluster
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-# Each update should be signed by OEMs
-LOCAL_CERTIFICATE := platform
-LOCAL_PRIVILEGED_MODULE := true
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android.car.cluster.navigation \
-    androidx.car_car-cluster
-
-LOCAL_JAVA_LIBRARIES += android.car
-LOCAL_STATIC_ANDROID_LIBRARIES += \
-    androidx.legacy_legacy-support-v4 \
-    androidx-constraintlayout_constraintlayout \
-    car-arch-common \
-    car-media-common \
-    car-telephony-common \
-    car-apps-common
-
-LOCAL_REQUIRED_MODULES := privapp_whitelist_android.car.cluster
-
-include $(BUILD_PACKAGE)
-
-# Use the following include to make our test apk.
-ifeq (,$(ONE_SHOT_MAKEFILE))
-include $(call all-makefiles-under,$(LOCAL_PATH))
-endif
-
-endif
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7dc2bb6..1cac376 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,7 +16,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     package="android.car.cluster"
-    android:sharedUserId="android.uid.system">
+    coreApp="true"
+    android:process="android.car.cluster"
+    android:sharedUserId="android.uid.cluster">
 
     <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="25"/>
 
@@ -67,6 +69,7 @@
             android:exported="false"
             android:showForAllUsers="true"
             android:theme="@style/Theme.ClusterTheme">
+            <meta-data android:name="distractionOptimized" android:value="true"/>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -78,8 +81,8 @@
             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
             android:launchMode="singleInstance"
             android:resizeableActivity="true"
-            android:permission="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
             android:allowEmbedded="true">
+            <meta-data android:name="distractionOptimized" android:value="true"/>
         </activity>
     </application>
 </manifest>
diff --git a/res/layout/include_navigation_state.xml b/res/layout/include_navigation_state.xml
index 5cd6ff5..d5f227c 100644
--- a/res/layout/include_navigation_state.xml
+++ b/res/layout/include_navigation_state.xml
@@ -28,7 +28,8 @@
         android:id="@+id/section_service_status"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:orientation="vertical">
+        android:orientation="vertical"
+        android:visibility="invisible">
 
         <TextView
             android:id="@+id/service_status"
diff --git a/src/android/car/cluster/ActivityMonitor.java b/src/android/car/cluster/ActivityMonitor.java
index 28c8147..f61572c 100644
--- a/src/android/car/cluster/ActivityMonitor.java
+++ b/src/android/car/cluster/ActivityMonitor.java
@@ -166,6 +166,9 @@
                 }
                 List<StackInfo> infos = mActivityManager.getAllStackInfos();
                 for (StackInfo info : infos) {
+                    if (!info.visible) {
+                        continue;
+                    }
                     Set<ActivityListener> listeners = mListeners.get(info.displayId);
                     if (listeners != null && !listeners.isEmpty()) {
                         for (ActivityListener listener : listeners) {
diff --git a/src/android/car/cluster/ClusterDisplayProvider.java b/src/android/car/cluster/ClusterDisplayProvider.java
index 78be39b..4050099 100644
--- a/src/android/car/cluster/ClusterDisplayProvider.java
+++ b/src/android/car/cluster/ClusterDisplayProvider.java
@@ -16,140 +16,111 @@
 
 package android.car.cluster;
 
-import android.annotation.Nullable;
+import android.car.Car;
+import android.car.CarOccupantZoneManager;
+import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.content.Context;
-import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
-import android.os.SystemProperties;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.Display;
-import android.view.DisplayAddress;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
 
 /**
  * This class provides a display for instrument cluster renderer.
  * <p>
  * By default it will try to provide physical secondary display if it is connected, if secondary
- * display is not connected during creation of this class then it will start networked virtual
- * display and listens for incoming connections.
- *
- * @see {@link NetworkedVirtualDisplay}
+ * display is not connected during creation of this class then it will wait for the display will
+ * be added.
  */
 public class ClusterDisplayProvider {
     private static final String TAG = "Cluster.DisplayProvider";
-
-    private static final String RO_CLUSTER_DISPLAY_PORT = "ro.car.cluster.displayport";
-    private static final String PERSIST_CLUSTER_DISPLAY_PORT =
-            "persist.car.cluster.displayport";
-    private static final int NETWORKED_DISPLAY_WIDTH = 1280;
-    private static final int NETWORKED_DISPLAY_HEIGHT = 720;
-    private static final int NETWORKED_DISPLAY_DPI = 320;
+    private static final boolean DEBUG = false;
 
     private final DisplayListener mListener;
-    private final DisplayManager mDisplayManager;
+    private final Car mCar;
+    private CarOccupantZoneManager mOccupantZoneManager;
 
-    private NetworkedVirtualDisplay mNetworkedVirtualDisplay;
-    private int mClusterDisplayId = -1;
+    private int mClusterDisplayId = Display.INVALID_DISPLAY;
 
     ClusterDisplayProvider(Context context, DisplayListener clusterDisplayListener) {
         mListener = clusterDisplayListener;
-        mDisplayManager = context.getSystemService(DisplayManager.class);
+        mCar = Car.createCar(context, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+                (car, ready) -> {
+                    if (!ready) return;
+                    initClusterDisplayProvider(context, (CarOccupantZoneManager) car.getCarManager(
+                            Car.CAR_OCCUPANT_ZONE_SERVICE));
+                });
+    }
 
-        Display clusterDisplay = getInstrumentClusterDisplay(mDisplayManager);
+    void release() {
+        if (mCar != null && mCar.isConnected()) {
+            mCar.disconnect();
+        }
+    }
+
+    private void initClusterDisplayProvider(
+            Context context, CarOccupantZoneManager occupantZoneManager) {
+        Preconditions.checkArgument(
+                occupantZoneManager != null,"Can't get CarOccupantZoneManager");
+        mOccupantZoneManager = occupantZoneManager;
+        checkClusterDisplayAdded();
+        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();
-            clusterDisplayListener.onDisplayAdded(clusterDisplay.getDisplayId());
-            trackClusterDisplay(null /* no need to track display by name */);
-        } else {
-            Log.i(TAG, "No physical cluster display found, starting network display");
-            setupNetworkDisplay(context);
+            mListener.onDisplayAdded(clusterDisplay.getDisplayId());
         }
     }
 
-    private void setupNetworkDisplay(Context context) {
-        mNetworkedVirtualDisplay = new NetworkedVirtualDisplay(context,
-                NETWORKED_DISPLAY_WIDTH, NETWORKED_DISPLAY_HEIGHT, NETWORKED_DISPLAY_DPI);
-        String displayName = mNetworkedVirtualDisplay.start();
-        trackClusterDisplay(displayName);
-    }
-
-    private void trackClusterDisplay(@Nullable String displayName) {
-        mDisplayManager.registerDisplayListener(new DisplayListener() {
-            @Override
-            public void onDisplayAdded(int displayId) {
-                boolean clusterDisplayAdded = false;
-
-                if (displayName == null && mClusterDisplayId == -1) {
-                    mClusterDisplayId = displayId;
-                    clusterDisplayAdded = true;
-                } else {
-                    Display display = mDisplayManager.getDisplay(displayId);
-                    if (display != null && TextUtils.equals(display.getName(), displayName)) {
-                        mClusterDisplayId = displayId;
-                        clusterDisplayAdded = true;
-                    }
-                }
-
-                if (clusterDisplayAdded) {
-                    mListener.onDisplayAdded(displayId);
-                }
-            }
-
-            @Override
-            public void onDisplayRemoved(int displayId) {
-                if (displayId == mClusterDisplayId) {
-                    mClusterDisplayId = -1;
-                    mListener.onDisplayRemoved(displayId);
-                }
-            }
-
-            @Override
-            public void onDisplayChanged(int displayId) {
-                if (displayId == mClusterDisplayId) {
-                    mListener.onDisplayChanged(displayId);
-                }
-            }
-
-        }, null);
-    }
-
-    private static Display getInstrumentClusterDisplay(DisplayManager displayManager) {
-        Display[] displays = displayManager.getDisplays();
-        Log.d(TAG, "There are currently " + displays.length + " displays connected.");
-
-        final int displayPortPrimary = 0;  // primary port should not be instrument cluster.
-        int displayPort = SystemProperties.getInt(PERSIST_CLUSTER_DISPLAY_PORT,
-                displayPortPrimary);
-        if (displayPort == displayPortPrimary) {
-            displayPort = SystemProperties.getInt(RO_CLUSTER_DISPLAY_PORT,
-                    displayPortPrimary);
-            if (displayPort == displayPortPrimary) {
-                return null;
+    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);
             }
         }
-        // match port for system display ( = null getOwnerPackageName())
-        // with separate check for main display as main display should be never picked up.
-        for (Display display : displays) {
-            if (display.getDisplayId() != Display.DEFAULT_DISPLAY
-                    && display.getOwnerPackageName() == null
-                    && display.getAddress() != null
-                    && display.getAddress() instanceof DisplayAddress.Physical) {
-                final byte port = ((DisplayAddress.Physical) display.getAddress()).getPort();
-                if (displayPort == port) {
-                    return display;
-                }
-            }
-        }
+        Log.e(TAG, "Can't find the OccupantZoneInfo for driver");
         return null;
     }
 
+    private final class ClusterDisplayChangeListener implements
+            CarOccupantZoneManager.OccupantZoneConfigChangeListener {
+        @Override
+        public void onOccupantZoneConfigChanged(int changeFlags) {
+            if (DEBUG) Log.d(TAG, "onOccupantZoneConfigChanged changeFlags=" + changeFlags);
+            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;
+                }
+            }
+        }
+    }
+
     @Override
     public String toString() {
         return getClass().getSimpleName() + "{"
                 + " clusterDisplayId = " + mClusterDisplayId
                 + "}";
     }
-}
+}
\ No newline at end of file
diff --git a/src/android/car/cluster/ClusterRenderingService.java b/src/android/car/cluster/ClusterRenderingService.java
index 8d0cafa..acaa4b2 100644
--- a/src/android/car/cluster/ClusterRenderingService.java
+++ b/src/android/car/cluster/ClusterRenderingService.java
@@ -15,7 +15,6 @@
  */
 package android.car.cluster;
 
-import static android.content.Intent.ACTION_USER_SWITCHED;
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.view.Display.INVALID_DISPLAY;
@@ -24,7 +23,7 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
-import android.car.CarNotConnectedException;
+import android.car.Car;
 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
 import android.car.cluster.renderer.InstrumentClusterRenderingService;
 import android.car.cluster.renderer.NavigationRenderer;
@@ -50,8 +49,6 @@
 import android.view.InputDevice;
 import android.view.KeyEvent;
 
-import androidx.versionedparcelable.ParcelUtils;
-
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import java.io.FileDescriptor;
@@ -70,7 +67,6 @@
     private static final String TAG = "Cluster.Service";
     private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
 
-    static final int NAV_STATE_EVENT_ID = 1;
     static final String LOCAL_BINDING_ACTION = "local";
     static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
 
@@ -124,23 +120,17 @@
     };
 
     public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
-        try {
-            ActivityOptions options = displayId != INVALID_DISPLAY
-                    ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
-                    : null;
-            setClusterActivityLaunchOptions(CarInstrumentClusterManager.CATEGORY_NAVIGATION,
-                    options);
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
-                        options, options.getLaunchDisplayId()));
-            }
-            setClusterActivityState(CarInstrumentClusterManager.CATEGORY_NAVIGATION,
-                    state.toBundle());
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, String.format("activity state set: %s", state));
-            }
-        } catch (CarNotConnectedException ex) {
-            Log.e(TAG, "Unable to update service", ex);
+        ActivityOptions options = displayId != INVALID_DISPLAY
+                ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
+                : null;
+        setClusterActivityLaunchOptions(options);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
+                    options, options != null ? options.getLaunchDisplayId() : -1));
+        }
+        setClusterActivityState(state);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("activity state set: %s", state));
         }
     }
 
@@ -182,6 +172,7 @@
     public void onDestroy() {
         super.onDestroy();
         mUserReceiver.unregister(this);
+        mDisplayProvider.release();
     }
 
     private void launchMainActivity() {
@@ -217,16 +208,18 @@
             Log.e(TAG, "Failed to resolve the navigation activity");
             return null;
         }
-        Rect displaySize = new Rect(0, 0, 320, 240);  // Arbitrary size, better than nothing.
-        DisplayManager dm = (DisplayManager) getSystemService(DisplayManager.class);
+        Rect displaySize = new Rect(0, 0, 240, 320);  // Arbitrary size, better than nothing.
+        DisplayManager dm = getSystemService(DisplayManager.class);
         Display display = dm.getDisplay(displayId);
         if (display != null) {
             display.getRectSize(displaySize);
         }
+        setClusterActivityState(ClusterActivityState.create(/* visible= */ true,
+                    /* unobscuredBounds= */ new Rect(0, 0, 240, 320)));
         return new Intent(Intent.ACTION_MAIN)
             .setComponent(component)
             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-            .putExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE,
+            .putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
                 ClusterActivityState.create(/* visible= */ true,
                     /* unobscuredBounds= */ displaySize).toBundle());
     }
@@ -262,39 +255,27 @@
             }
 
             @Override
-            public void onEvent(int eventType, Bundle bundle) {
+            public void onNavigationStateChanged(Bundle bundle) {
                 StringBuilder bundleSummary = new StringBuilder();
-                if (eventType == NAV_STATE_EVENT_ID) {
-                    // Required to prevent backwards compatibility crash with old map providers
-                    // sending androidx.versionedparcelables
-                    bundle.setClassLoader(ParcelUtils.class.getClassLoader());
-                    
-                    // Attempt to read proto byte array
-                    byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
-                    if (protoBytes != null) {
-                        try {
-                            NavigationStateProto navState = NavigationStateProto.parseFrom(
-                                    protoBytes);
-                            bundleSummary.append(navState.toString());
 
-                            // Update clients
-                            broadcastClientEvent(
-                                    client -> client.onNavigationStateChange(navState));
-                        } catch (InvalidProtocolBufferException e) {
-                            Log.e(TAG, "Error parsing navigation state proto", e);
-                        }
-                    } else {
-                        Log.e(TAG, "Received nav state byte array is null");
+                // Attempt to read proto byte array
+                byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
+                if (protoBytes != null) {
+                    try {
+                        NavigationStateProto navState = NavigationStateProto.parseFrom(
+                                protoBytes);
+                        bundleSummary.append(navState.toString());
+
+                        // Update clients
+                        broadcastClientEvent(
+                                client -> client.onNavigationStateChange(navState));
+                    } catch (InvalidProtocolBufferException e) {
+                        Log.e(TAG, "Error parsing navigation state proto", e);
                     }
                 } else {
-                    for (String key : bundle.keySet()) {
-                        bundleSummary.append(key);
-                        bundleSummary.append("=");
-                        bundleSummary.append(bundle.get(key));
-                        bundleSummary.append(" ");
-                    }
+                    Log.e(TAG, "Received nav state byte array is null");
                 }
-                Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
+                Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")");
             }
         };
 
@@ -377,14 +358,9 @@
 
             case "setUnobscuredArea": {
                 if (args.length > 5) {
-                    Rect unobscuredArea = new Rect(parseInt(args[2]), parseInt(args[3]),
-                            parseInt(args[4]), parseInt(args[5]));
-                    try {
-                        setClusterActivityState(args[1],
-                                ClusterActivityState.create(true, unobscuredArea).toBundle());
-                    } catch (CarNotConnectedException e) {
-                        Log.i(TAG, "Failed to set activity state.", e);
-                    }
+                    setClusterActivityState(ClusterActivityState.create(true,
+                            new Rect(parseInt(args[2]), parseInt(args[3]),
+                                    parseInt(args[4]), parseInt(args[5]))));
                 } else {
                     Log.i(TAG, "wrong format, expected: category left top right bottom");
                 }
diff --git a/src/android/car/cluster/FakeFreeNavigationActivity.java b/src/android/car/cluster/FakeFreeNavigationActivity.java
index a836dc0..7b7a46b 100644
--- a/src/android/car/cluster/FakeFreeNavigationActivity.java
+++ b/src/android/car/cluster/FakeFreeNavigationActivity.java
@@ -17,6 +17,7 @@
 package android.car.cluster;
 
 import android.app.Activity;
+import android.car.Car;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -54,10 +55,9 @@
             Log.w(TAG, "Received a null intent");
             return;
         }
-        Bundle bundle = intent.getBundleExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE);
+        Bundle bundle = intent.getBundleExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE);
         if (bundle == null) {
-            Log.w(TAG, "Received an intent without " + CarInstrumentClusterManager
-                    .KEY_EXTRA_ACTIVITY_STATE);
+            Log.w(TAG, "Received an intent without " + Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE);
             return;
         }
         ClusterActivityState state = ClusterActivityState.fromBundle(bundle);
diff --git a/src/android/car/cluster/ImageResolver.java b/src/android/car/cluster/ImageResolver.java
index 2e1ada9..cb5c19b 100644
--- a/src/android/car/cluster/ImageResolver.java
+++ b/src/android/car/cluster/ImageResolver.java
@@ -44,12 +44,12 @@
         /**
          * Returns a {@link Bitmap} given a request Uri and dimensions
          */
-        Bitmap getBitmap(Uri uri, int width, int height);
+        Bitmap getBitmap(@NonNull Uri uri, int width, int height);
 
         /**
          * Returns a {@link Bitmap} given a request Uri, dimensions, and offLanesAlpha value
          */
-        Bitmap getBitmap(Uri uri, int width, int height, float offLanesAlpha);
+        Bitmap getBitmap(@NonNull Uri uri, int width, int height, float offLanesAlpha);
     }
 
     /**
diff --git a/src/android/car/cluster/LaneView.java b/src/android/car/cluster/LaneView.java
index 2a5ca58..7d48e28 100644
--- a/src/android/car/cluster/LaneView.java
+++ b/src/android/car/cluster/LaneView.java
@@ -70,7 +70,7 @@
 
     public void setLanes(ImageReference imageReference, ImageResolver imageResolver) {
         imageResolver
-                .getBitmap(imageReference, 0, getHeight())
+                .getBitmap(imageReference, 0, getHeight(), 0.5f)
                 .thenAccept(bitmap -> {
                     mHandler.post(() -> {
                         removeAllViews();
diff --git a/src/android/car/cluster/LoggingClusterRenderingService.java b/src/android/car/cluster/LoggingClusterRenderingService.java
index 89990f9..dfed06c 100644
--- a/src/android/car/cluster/LoggingClusterRenderingService.java
+++ b/src/android/car/cluster/LoggingClusterRenderingService.java
@@ -15,6 +15,7 @@
  */
 package android.car.cluster;
 
+import android.car.cluster.navigation.NavigationState.NavigationStateProto;
 import android.car.cluster.renderer.InstrumentClusterRenderingService;
 import android.car.cluster.renderer.NavigationRenderer;
 import android.car.navigation.CarNavigationInstrumentCluster;
@@ -32,7 +33,6 @@
 public class LoggingClusterRenderingService extends InstrumentClusterRenderingService {
     private static final String TAG = LoggingClusterRenderingService.class.getSimpleName();
     private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
-    private static final int NAV_STATE_EVENT_ID = 1;
 
     @Override
     public NavigationRenderer getNavigationRenderer() {
@@ -48,39 +48,29 @@
             }
 
             @Override
-            public void onEvent(int eventType, Bundle bundle) {
+            public void onNavigationStateChanged(Bundle bundle) {
                 StringBuilder bundleSummary = new StringBuilder();
-                if (eventType == NAV_STATE_EVENT_ID) {
-                    // Attempt to read proto byte array
-                    byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
-                    if (protoBytes != null) {
-                        try {
-                            android.car.cluster.navigation.NavigationState.NavigationStateProto
-                                    navState =
-                                    android.car.cluster.navigation.NavigationState.NavigationStateProto.parseFrom(
-                                            protoBytes);
-                            bundleSummary.append(navState.toString());
 
-                            // Sending broadcast for testing.
-                            Intent intent = new Intent(
-                                    "android.car.cluster.NAVIGATION_STATE_UPDATE");
-                            intent.putExtra(NAV_STATE_PROTO_BUNDLE_KEY, bundle);
-                            sendBroadcastAsUser(intent, UserHandle.ALL);
-                        } catch (InvalidProtocolBufferException e) {
-                            Log.e(TAG, "Error parsing navigation state proto", e);
-                        }
-                    } else {
-                        Log.e(TAG, "Received nav state byte array is null");
+                // Attempt to read proto byte array
+                byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
+                if (protoBytes != null) {
+                    try {
+                        NavigationStateProto navState = NavigationStateProto.parseFrom(protoBytes);
+                        bundleSummary.append(navState.toString());
+
+                        // Sending broadcast for testing.
+                        Intent intent = new Intent(
+                                "android.car.cluster.NAVIGATION_STATE_UPDATE");
+                        intent.putExtra(NAV_STATE_PROTO_BUNDLE_KEY, bundle);
+                        sendBroadcastAsUser(intent, UserHandle.ALL);
+                    } catch (InvalidProtocolBufferException e) {
+                        Log.e(TAG, "Error parsing navigation state proto", e);
                     }
                 } else {
-                    for (String key : bundle.keySet()) {
-                        bundleSummary.append(key);
-                        bundleSummary.append("=");
-                        bundleSummary.append(bundle.get(key));
-                        bundleSummary.append(" ");
-                    }
+                    Log.e(TAG, "Received nav state byte array is null");
                 }
-                Log.i(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
+
+                Log.i(TAG, "onEvent(" + bundleSummary + ")");
             }
         };
 
diff --git a/src/android/car/cluster/MainClusterActivity.java b/src/android/car/cluster/MainClusterActivity.java
index 5e19a42..9c76d37 100644
--- a/src/android/car/cluster/MainClusterActivity.java
+++ b/src/android/car/cluster/MainClusterActivity.java
@@ -16,6 +16,8 @@
 package android.car.cluster;
 
 import static android.car.cluster.ClusterRenderingService.LOCAL_BINDING_ACTION;
+import static android.content.Intent.ACTION_SCREEN_OFF;
+import static android.content.Intent.ACTION_USER_PRESENT;
 import static android.content.Intent.ACTION_USER_SWITCHED;
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 import static android.content.PermissionChecker.PERMISSION_GRANTED;
@@ -54,6 +56,7 @@
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentPagerAdapter;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
 import androidx.lifecycle.ViewModelProviders;
 import androidx.viewpager.widget.ViewPager;
 
@@ -110,6 +113,7 @@
     private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
     private static final int NAVIGATION_ACTIVITY_RELAUNCH_DELAY_MS = 5000;
 
+    private final UserReceiver mUserReceiver = new UserReceiver();
     private ActivityMonitor mActivityMonitor = new ActivityMonitor();
     private final Handler mHandler = new Handler();
     private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
@@ -172,6 +176,28 @@
         mClusterViewModel.setCurrentNavigationActivity(activity);
     };
 
+    /**
+     * On user switch the navigation application must be re-launched on the new user. Otherwise
+     * the navigation fragment will keep showing the application on the previous user.
+     * {@link MainClusterActivity} is shared between all users (it is not restarted on user switch)
+     */
+    private class UserReceiver extends BroadcastReceiver {
+        void register(Context context) {
+            IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
+            context.registerReceiverForAllUsers(this, intentFilter, null, null);
+        }
+        void unregister(Context context) {
+            context.unregisterReceiver(this);
+        }
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Broadcast received: " + intent);
+            }
+            tryLaunchNavigationActivity();
+        }
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -202,7 +228,24 @@
         mOrderToFacet.get(NAV_FACET_ID).mButton.requestFocus();
         mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
 
-        mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_USER_PRESENT);
+        filter.addAction(ACTION_SCREEN_OFF);
+        registerReceiver(new BroadcastReceiver(){
+            @Override
+            public void onReceive(final Context context, final Intent intent) {
+                if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)){
+                    Log.d(TAG, "ACTION_SCREEN_OFF");
+                    mNavStateController.hideNavigationStateInfo();
+                }
+                else if (intent.getAction().equals(Intent.ACTION_USER_PRESENT)) {
+                    Log.d(TAG, "ACTION_USER_PRESENT");
+                    mNavStateController.showNavigationStateInfo();
+                }
+            }
+        }, filter);
+
+        mClusterViewModel = new ViewModelProvider(this).get(ClusterViewModel.class);
         mClusterViewModel.getNavigationFocus().observe(this, focus -> {
             // If focus is lost, we launch the default navigation activity again.
             if (!focus) {
@@ -230,9 +273,11 @@
 
         mActivityMonitor.start();
 
+        mUserReceiver.register(this);
+
         InMemoryPhoneBook.init(this);
 
-        PhoneFragmentViewModel phoneViewModel = ViewModelProviders.of(this).get(
+        PhoneFragmentViewModel phoneViewModel = new ViewModelProvider(this).get(
                 PhoneFragmentViewModel.class);
 
         phoneViewModel.setPhoneStateCallback(new PhoneFragmentViewModel.PhoneStateCallback() {
@@ -263,6 +308,7 @@
     protected void onDestroy() {
         super.onDestroy();
         Log.d(TAG, "onDestroy");
+        mUserReceiver.unregister(this);
         mActivityMonitor.stop();
         if (mService != null) {
             mService.unregisterClient(this);
@@ -394,7 +440,7 @@
             Intent intent = new Intent(Intent.ACTION_MAIN)
                     .setComponent(navigationActivity)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE,
+                    .putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
                             activityState.toBundle());
 
             Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplay.mDisplayId);
@@ -420,7 +466,7 @@
      * <ul>
      * <li>Dynamically detect what's the default navigation activity the user has selected on the
      * head unit, and obtain the activity marked with
-     * {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} from the same package.
+     * {@link Car#CAR_CATEGORY_NAVIGATION} from the same package.
      * <li>Let the user select one from settings.
      * </ul>
      */
@@ -442,16 +488,6 @@
             if (navigationApp == null) {
                 return null;
             }
-
-            // Check that it has the right permissions
-            if (pm.checkPermission(Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER, navigationApp.activityInfo
-                    .packageName) != PERMISSION_GRANTED) {
-                Log.i(TAG, String.format("Package '%s' doesn't have permission %s",
-                        navigationApp.activityInfo.packageName,
-                        Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER));
-                return null;
-            }
-
             return new ComponentName(navigationApp.activityInfo.packageName,
                     navigationApp.activityInfo.name);
         } catch (URISyntaxException ex) {
diff --git a/src/android/car/cluster/MusicFragment.java b/src/android/car/cluster/MusicFragment.java
index 34a7ded..540db4d 100644
--- a/src/android/car/cluster/MusicFragment.java
+++ b/src/android/car/cluster/MusicFragment.java
@@ -15,6 +15,8 @@
  */
 package android.car.cluster;
 
+import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
+
 import android.os.Bundle;
 import android.util.Size;
 import android.view.LayoutInflater;
@@ -51,9 +53,10 @@
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
             Bundle savedInstanceState) {
         FragmentActivity activity = requireActivity();
-        PlaybackViewModel playbackViewModel = PlaybackViewModel.get(activity.getApplication());
+        PlaybackViewModel playbackViewModel =
+                PlaybackViewModel.get(activity.getApplication(), MEDIA_SOURCE_MODE_PLAYBACK);
         MediaSourceViewModel mMediaSourceViewModel = MediaSourceViewModel.get(
-                activity.getApplication());
+                activity.getApplication(), MEDIA_SOURCE_MODE_PLAYBACK);
 
         MusicFragmentViewModel innerViewModel = ViewModelProviders.of(activity).get(
                 MusicFragmentViewModel.class);
diff --git a/src/android/car/cluster/MusicFragmentViewModel.java b/src/android/car/cluster/MusicFragmentViewModel.java
index 769cac8..9fb0fa4 100644
--- a/src/android/car/cluster/MusicFragmentViewModel.java
+++ b/src/android/car/cluster/MusicFragmentViewModel.java
@@ -48,7 +48,7 @@
         mMediaSourceViewModel = mediaSourceViewModel;
         mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();
         mAppName = mapNonNull(mMediaSource, MediaSource::getDisplayName);
-        mAppIcon = mapNonNull(mMediaSource, MediaSource::getRoundPackageIcon);
+        mAppIcon = mapNonNull(mMediaSource, MediaSource::getCroppedPackageIcon);
     }
 
     LiveData<CharSequence> getAppName() {
diff --git a/src/android/car/cluster/NavStateController.java b/src/android/car/cluster/NavStateController.java
index facab51..fe4d724 100644
--- a/src/android/car/cluster/NavStateController.java
+++ b/src/android/car/cluster/NavStateController.java
@@ -15,6 +15,10 @@
  */
 package android.car.cluster;
 
+import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.NORMAL;
+import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.REROUTING;
+import static android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.SERVICE_STATUS_UNSPECIFIED;
+
 import android.annotation.Nullable;
 import android.car.cluster.navigation.NavigationState.Destination;
 import android.car.cluster.navigation.NavigationState.Destination.Traffic;
@@ -44,6 +48,7 @@
 
     private Handler mHandler = new Handler();
 
+    private View mNavigationState;
     private LinearLayout mSectionManeuver;
     private LinearLayout mSectionNavigation;
     private LinearLayout mSectionServiceStatus;
@@ -66,6 +71,7 @@
      * @param container {@link View} containing the navigation state views
      */
     public NavStateController(View container) {
+        mNavigationState = container;
         mSectionManeuver = container.findViewById(R.id.section_maneuver);
         mSectionNavigation = container.findViewById(R.id.section_navigation);
         mSectionServiceStatus = container.findViewById(R.id.section_service_status);
@@ -82,6 +88,14 @@
         mContext = container.getContext();
     }
 
+    public void hideNavigationStateInfo() {
+        mNavigationState.setVisibility(View.INVISIBLE);
+    }
+
+    public void showNavigationStateInfo() {
+        mNavigationState.setVisibility(View.VISIBLE);
+    }
+
     public void setImageResolver(@Nullable ImageResolver imageResolver) {
         mImageResolver = imageResolver;
     }
@@ -98,7 +112,13 @@
             return;
         }
 
-        if (state.getServiceStatus() == NavigationStateProto.ServiceStatus.REROUTING) {
+        NavigationStateProto.ServiceStatus serviceStatus = state.getServiceStatus();
+        if (serviceStatus == SERVICE_STATUS_UNSPECIFIED) {
+            mSectionManeuver.setVisibility(View.INVISIBLE);
+            mSectionNavigation.setVisibility(View.INVISIBLE);
+            mSectionServiceStatus.setVisibility(View.INVISIBLE);
+            return;
+        } else if (serviceStatus == REROUTING) {
             mSectionManeuver.setVisibility(View.INVISIBLE);
             mSectionNavigation.setVisibility(View.INVISIBLE);
             mSectionServiceStatus.setVisibility(View.VISIBLE);
@@ -115,8 +135,8 @@
         Traffic traffic = destination != null ? destination.getTraffic() : null;
         String eta = destination != null
                 ? destination.getFormattedDurationUntilArrival().isEmpty()
-                    ? formatEta(destination.getEstimatedTimeAtArrival())
-                    : destination.getFormattedDurationUntilArrival()
+                ? formatEta(destination.getEstimatedTimeAtArrival())
+                : destination.getFormattedDurationUntilArrival()
                 : null;
         mEta.setText(eta);
         mEta.setTextColor(getTrafficColor(traffic));
diff --git a/src/android/car/cluster/NetworkedVirtualDisplay.java b/src/android/car/cluster/NetworkedVirtualDisplay.java
deleted file mode 100644
index 56121d0..0000000
--- a/src/android/car/cluster/NetworkedVirtualDisplay.java
+++ /dev/null
@@ -1,452 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.car.cluster;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
-import android.hardware.display.VirtualDisplay;
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.CodecException;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecProfileLevel;
-import android.media.MediaFormat;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.util.Log;
-import android.view.Display;
-import android.view.Surface;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.nio.ByteBuffer;
-import java.util.UUID;
-
-/**
- * This class encapsulates all work related to managing networked virtual display.
- * <p>
- * It opens a socket and listens on port {@code PORT} for connections, or the emulator pipe. Once
- * connection is established it creates virtual display and media encoder and starts streaming video
- * to that socket.  If the receiving part is disconnected, it will keep port open and virtual
- * display won't be destroyed.
- */
-public class NetworkedVirtualDisplay {
-    private static final String TAG = "Cluster." + NetworkedVirtualDisplay.class.getSimpleName();
-
-    private final String mUniqueId =  UUID.randomUUID().toString();
-
-    private final DisplayManager mDisplayManager;
-    private final int mWidth;
-    private final int mHeight;
-    private final int mDpi;
-
-    private static final int FPS = 25;
-    private static final int BITRATE = 6144000;
-    private static final String MEDIA_FORMAT_MIMETYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
-
-    public static final int MSG_START = 0;
-    public static final int MSG_STOP = 1;
-    public static final int MSG_SEND_FRAME = 2;
-
-    private static final String PIPE_NAME = "pipe:qemud:carCluster";
-    private static final String PIPE_DEVICE = "/dev/qemu_pipe";
-
-    // Constants shared with emulator in car-cluster-widget.cpp
-    public static final int PIPE_START = 1;
-    public static final int PIPE_STOP = 2;
-
-    private static final int PORT = 5151;
-
-    private SenderThread mActiveThread;
-    private HandlerThread mBroadcastThread = new HandlerThread("BroadcastThread");
-
-    private VirtualDisplay mVirtualDisplay;
-    private MediaCodec mVideoEncoder;
-    private Handler mHandler;
-    private byte[] mBuffer = null;
-    private int mLastFrameLength = 0;
-
-    private final DebugCounter mCounter = new DebugCounter();
-
-    NetworkedVirtualDisplay(Context context, int width, int height, int dpi) {
-        mDisplayManager = context.getSystemService(DisplayManager.class);
-        mWidth = width;
-        mHeight = height;
-        mDpi = dpi;
-
-        DisplayListener displayListener = new DisplayListener() {
-            @Override
-            public void onDisplayAdded(int i) {
-                final Display display = mDisplayManager.getDisplay(i);
-                if (display != null && getDisplayName().equals(display.getName())) {
-                    onVirtualDisplayReady(display);
-                }
-            }
-
-            @Override
-            public void onDisplayRemoved(int i) {}
-
-            @Override
-            public void onDisplayChanged(int i) {}
-        };
-
-        mDisplayManager.registerDisplayListener(displayListener, new Handler());
-    }
-
-    /**
-     * Opens socket and creates virtual display asynchronously once connection established.  Clients
-     * of this class may subscribe to
-     * {@link android.hardware.display.DisplayManager#registerDisplayListener(
-     * DisplayListener, Handler)} to be notified when virtual display is created.
-     * Note, that this method should be called only once.
-     *
-     * @return Unique display name associated with the instance of this class.
-     *
-     * @see {@link Display#getName()}
-     *
-     * @throws IllegalStateException thrown if networked display already started
-     */
-    public String start() {
-        if (mBroadcastThread.isAlive()) {
-            throw new IllegalStateException("Already started");
-        }
-
-        mBroadcastThread.start();
-        mHandler = new BroadcastThreadHandler(mBroadcastThread.getLooper());
-        mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
-        return getDisplayName();
-    }
-
-    public void release() {
-        mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP));
-        mBroadcastThread.quitSafely();
-
-        if (mVirtualDisplay != null) {
-            mVirtualDisplay.setSurface(null);
-            mVirtualDisplay.release();
-            mVirtualDisplay = null;
-        }
-    }
-
-    private String getDisplayName() {
-        return "Cluster-" + mUniqueId;
-    }
-
-    private VirtualDisplay createVirtualDisplay() {
-        Log.i(TAG, "createVirtualDisplay " + mWidth + "x" + mHeight +"@" + mDpi);
-        return mDisplayManager.createVirtualDisplay(getDisplayName(), mWidth, mHeight, mDpi,
-                null, 0 /* flags */, null, null );
-    }
-
-    private void onVirtualDisplayReady(Display display) {
-        Log.i(TAG, "onVirtualDisplayReady, display: " + display);
-    }
-
-    private void startCasting(Handler handler) {
-        Log.i(TAG, "Start casting...");
-        if (mVideoEncoder != null) {
-            Log.i(TAG, "Already started casting");
-            return;
-        }
-        mVideoEncoder = createVideoStream(handler);
-
-        if (mVirtualDisplay == null) {
-            mVirtualDisplay = createVirtualDisplay();
-        }
-
-        mVirtualDisplay.setSurface(mVideoEncoder.createInputSurface());
-        mVideoEncoder.start();
-        Log.i(TAG, "Video encoder started");
-    }
-
-    private MediaCodec createVideoStream(Handler handler) {
-        MediaCodec encoder;
-        try {
-            encoder = MediaCodec.createEncoderByType(MEDIA_FORMAT_MIMETYPE);
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to create video encoder for " + MEDIA_FORMAT_MIMETYPE, e);
-            return null;
-        }
-
-        encoder.setCallback(new MediaCodec.Callback() {
-            @Override
-            public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
-                // Nothing to do
-            }
-
-            @Override
-            public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index,
-                    @NonNull BufferInfo info) {
-                mCounter.outputBuffers++;
-                doOutputBufferAvailable(index, info);
-            }
-
-            @Override
-            public void onError(@NonNull MediaCodec codec, @NonNull CodecException e) {
-                Log.e(TAG, "onError, codec: " + codec, e);
-                mCounter.bufferErrors++;
-                stopCasting();
-                startCasting(handler);
-            }
-
-            @Override
-            public void onOutputFormatChanged(@NonNull MediaCodec codec,
-                    @NonNull MediaFormat format) {
-                Log.i(TAG, "onOutputFormatChanged, codec: " + codec + ", format: " + format);
-
-            }
-        }, handler);
-
-        configureVideoEncoder(encoder, mWidth, mHeight);
-        return encoder;
-    }
-
-    private void doOutputBufferAvailable(int index, @NonNull BufferInfo info) {
-        mHandler.removeMessages(MSG_SEND_FRAME);
-
-        ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index);
-        if (encodedData == null) {
-            throw new RuntimeException("couldn't fetch buffer at index " + index);
-        }
-
-        if (info.size != 0) {
-            encodedData.position(info.offset);
-            encodedData.limit(info.offset + info.size);
-            mLastFrameLength = encodedData.remaining();
-            if (mBuffer == null || mBuffer.length < mLastFrameLength) {
-                Log.i(TAG, "Allocating new buffer: " + mLastFrameLength);
-                mBuffer = new byte[mLastFrameLength];
-            }
-            encodedData.get(mBuffer, 0, mLastFrameLength);
-            mVideoEncoder.releaseOutputBuffer(index, false);
-
-            // Send this frame asynchronously (avoiding blocking on the socket). We might miss
-            // frames if the consumer is not fast enough, but this is acceptable.
-            sendFrameAsync(0);
-        } else {
-            Log.e(TAG, "Skipping empty buffer");
-            mVideoEncoder.releaseOutputBuffer(index, false);
-        }
-    }
-
-    private void sendFrameAsync(long delayMs) {
-        Message msg = mHandler.obtainMessage(MSG_SEND_FRAME);
-        mHandler.sendMessageDelayed(msg, delayMs);
-    }
-
-    private void sendFrame(byte[] buf, int len) {
-        if (mActiveThread != null) {
-            mActiveThread.send(buf, len);
-        }
-    }
-
-    private void stopCasting() {
-        Log.i(TAG, "Stopping casting...");
-
-        if (mVirtualDisplay != null) {
-            Surface surface = mVirtualDisplay.getSurface();
-            if (surface != null) surface.release();
-        }
-
-        if (mVideoEncoder != null) {
-            // Releasing encoder as stop/start didn't work well (couldn't create or reuse input
-            // surface).
-            try {
-                mVideoEncoder.stop();
-                mVideoEncoder.release();
-            } catch (IllegalStateException e) {
-                // do nothing, already released
-            }
-            mVideoEncoder = null;
-        }
-        Log.i(TAG, "Casting stopped");
-    }
-
-    private class BroadcastThreadHandler extends Handler {
-        private static final int MAX_FAIL_COUNT = 10;
-        private int mFailConnectCounter;
-
-        BroadcastThreadHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_START:
-                    Log.i(TAG, "Received start message");
-
-                    // Make sure mActiveThread cannot start multiple times
-                    if (mActiveThread != null) {
-                        Log.w(TAG, "Trying to start a running thread. Race condition may exist");
-                        break;
-                    }
-
-                    // Failure to connect to either pipe or network returns null
-                    if (mActiveThread == null) {
-                        mActiveThread = tryPipeConnect();
-                    }
-                    if (mActiveThread == null) {
-                        mActiveThread = tryNetworkConnect();
-                    }
-                    if (mActiveThread == null) {
-                        // When failed attempt limit is reached, clean up and quit this thread.
-                        mFailConnectCounter++;
-                        if (mFailConnectCounter >= MAX_FAIL_COUNT) {
-                            Log.e(TAG, "Too many failed connection attempts; aborting");
-                            release();
-                            throw new RuntimeException("Abort after failed connection attempts");
-                        }
-                        mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
-                        break;
-                    }
-
-                    try {
-                        mFailConnectCounter = 0;
-                        mCounter.clientsConnected++;
-                        mActiveThread.start();
-                        startCasting(this);
-                    } catch (Exception e) {
-                        Log.e(TAG, "Failed to start thread", e);
-                        Log.e(TAG, "DebugCounter: " + mCounter);
-                    }
-                    break;
-
-                case MSG_STOP:
-                    Log.i(TAG, "Received stop message");
-                    stopCasting();
-                    mCounter.clientsDisconnected++;
-                    if (mActiveThread != null) {
-                        mActiveThread.close();
-                        try {
-                            mActiveThread.join();
-                        } catch (InterruptedException e) {
-                            Log.e(TAG, "Waiting for active thread to close failed", e);
-                        }
-                        mActiveThread = null;
-                    }
-                    break;
-
-                case MSG_SEND_FRAME:
-                    if (mActiveThread == null) {
-                        // Stop the chaining signal if there's no client to send to
-                        break;
-                    }
-                    sendFrame(mBuffer, mLastFrameLength);
-                    // We will keep sending last frame every second as a heartbeat.
-                    sendFrameAsync(1000L);
-                    break;
-            }
-        }
-
-        // Returns null if can't establish pipe connection
-        // Otherwise returns the corresponding client thread
-        private PipeThread tryPipeConnect() {
-            try {
-                RandomAccessFile pipe = new RandomAccessFile(PIPE_DEVICE, "rw");
-                byte[] temp = new byte[PIPE_NAME.length() + 1];
-                temp[PIPE_NAME.length()] = 0;
-                System.arraycopy(PIPE_NAME.getBytes(), 0, temp, 0, PIPE_NAME.length());
-                pipe.write(temp);
-
-                // At this point, the pipe exists, so we will just wait for a start signal
-                // This is in case pipe still sends leftover stops from last instantiation
-                int signal = pipe.read();
-                while (signal != PIPE_START) {
-                    Log.i(TAG, "Received non-start signal: " + signal);
-                    signal = pipe.read();
-                }
-                return new PipeThread(mHandler, pipe);
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to establish pipe connection", e);
-                return null;
-            }
-        }
-
-        // Returns null if can't establish network connection
-        // Otherwise returns the corresponding client thread
-        private SocketThread tryNetworkConnect() {
-            try {
-                ServerSocket serverSocket = new ServerSocket(PORT);
-                Log.i(TAG, "Server socket opened");
-                Socket socket = serverSocket.accept();
-                socket.setTcpNoDelay(true);
-                socket.setKeepAlive(true);
-                socket.setSoLinger(true, 0);
-
-                InputStream inputStream = socket.getInputStream();
-                OutputStream outputStream = socket.getOutputStream();
-
-                return new SocketThread(mHandler, serverSocket, inputStream, outputStream);
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to establish network connection", e);
-                return null;
-            }
-        }
-    }
-
-    private static void configureVideoEncoder(MediaCodec codec, int width, int height) {
-        MediaFormat format = MediaFormat.createVideoFormat(MEDIA_FORMAT_MIMETYPE, width, height);
-
-        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
-                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
-        format.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE);
-        format.setInteger(MediaFormat.KEY_FRAME_RATE, FPS);
-        format.setInteger(MediaFormat.KEY_CAPTURE_RATE, FPS);
-        format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
-        format.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 second between I-frames
-        format.setInteger(MediaFormat.KEY_LEVEL, CodecProfileLevel.AVCLevel31);
-        format.setInteger(MediaFormat.KEY_PROFILE,
-                MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
-
-        codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-    }
-
-    @Override
-    public String toString() {
-        return getClass() + "{"
-                + ", receiver connected: " + (mActiveThread != null)
-                + ", encoder: " + mVideoEncoder
-                + ", virtualDisplay" + mVirtualDisplay
-                + "}";
-    }
-
-    private static class DebugCounter {
-        long outputBuffers;
-        long bufferErrors;
-        long clientsConnected;
-        long clientsDisconnected;
-
-        @Override
-        public String toString() {
-            return getClass().getSimpleName() + "{"
-                    + "outputBuffers=" + outputBuffers
-                    + ", bufferErrors=" + bufferErrors
-                    + ", clientsConnected=" + clientsConnected
-                    + ", clientsDisconnected= " + clientsDisconnected
-                    + "}";
-        }
-    }
-}
diff --git a/src/android/car/cluster/PipeThread.java b/src/android/car/cluster/PipeThread.java
deleted file mode 100644
index a8f6308..0000000
--- a/src/android/car/cluster/PipeThread.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.car.cluster;
-import android.os.Handler;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-/**
- * Thread that can send data to the emulator using a qemud service.
- */
-public class PipeThread extends SenderThread {
-    private static final String TAG = "Cluster." + PipeThread.class.getSimpleName();
-
-    private RandomAccessFile mPipe;
-
-    /**
-     * Creates instance of pipe thread that can write to given pipe file.
-     *
-     * @param handler {@link Handler} used to message broadcaster.
-     * @param pipe {@link RandomAccessFile} file already connected to pipe.
-     */
-    PipeThread(Handler handler, RandomAccessFile pipe) {
-        super(handler);
-        mPipe = pipe;
-    }
-
-    public void run() {
-        try {
-            int signal = mPipe.read();
-            while (signal != NetworkedVirtualDisplay.PIPE_STOP) {
-                Log.i(TAG, "Received non-stop signal: " + signal);
-                signal = mPipe.read();
-            }
-            restart();
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to read from pipe");
-            restart();
-        }
-    }
-
-    @Override
-    public void send(byte[] buf, int len) {
-        try {
-            // First sends the size prior to sending the data, since receiving side only sees
-            // the size of the buffer, which could be significant larger than the actual data.
-            mPipe.write(ByteBuffer.allocate(4)
-                          .order(ByteOrder.LITTLE_ENDIAN).putInt(len).array());
-            mPipe.write(buf);
-        } catch (IOException e) {
-            Log.e(TAG, "Write to pipe failed");
-            restart();
-        }
-    }
-
-    @Override
-    public void close() {
-        try {
-            mPipe.close();
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to close pipe", e);
-        }
-    }
-}
-
diff --git a/src/android/car/cluster/SenderThread.java b/src/android/car/cluster/SenderThread.java
deleted file mode 100644
index de461e2..0000000
--- a/src/android/car/cluster/SenderThread.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.car.cluster;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-
-/**
- * This class serves as a template for sending to specific clients of the broadcaster.
- */
-public abstract class SenderThread extends Thread {
-    private static final String TAG = "Cluster.SenderThread";
-
-    private Handler mHandler;
-
-    SenderThread(Handler handler) {
-        mHandler = handler;
-    }
-
-    abstract void send(byte[] buf, int len);
-    abstract void close();
-
-    /**
-     * Tells the broadcasting thread to stop and close everything in progress, and start over again.
-     * It will kill the current instance of this thread, and produce a new one.
-     */
-    synchronized void restart() {
-        if (mHandler.hasMessages(NetworkedVirtualDisplay.MSG_START)) return;
-        Log.i(TAG, "Sending STOP and START msgs to NetworkedVirtualDisplay");
-
-        mHandler.sendMessage(Message.obtain(mHandler, NetworkedVirtualDisplay.MSG_STOP));
-        mHandler.sendMessage(Message.obtain(mHandler, NetworkedVirtualDisplay.MSG_START));
-    }
-}
diff --git a/src/android/car/cluster/SocketThread.java b/src/android/car/cluster/SocketThread.java
deleted file mode 100644
index 7f1c61a..0000000
--- a/src/android/car/cluster/SocketThread.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.car.cluster;
-import android.os.Handler;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.ServerSocket;
-
-/**
- * The thread that will send data on an opened socket.
- */
-public class SocketThread extends SenderThread {
-    private static final String TAG = "Cluster." + SocketThread.class.getSimpleName();
-    private ServerSocket mServerSocket;
-    private OutputStream mOutputStream;
-    private InputStream mInputStream;
-
-    /**
-     * Create instance of thread that can write to given open socket.
-     *
-     * @param handler {@link Handler} used to message the broadcaster.
-     * @param serverSocket {@link ServerSocket} should be already opened.
-     * @param inputStream {@link InputStream} corresponding to opened socket.
-     * @param outputStream {@link OutputStream} corresponding to opened socket.
-     */
-    SocketThread(Handler handler, ServerSocket serverSocket, InputStream inputStream,
-                    OutputStream outputStream) {
-        super(handler);
-        mServerSocket = serverSocket;
-        mInputStream = inputStream;
-        mOutputStream = outputStream;
-    }
-
-    public void run() {
-        try {
-            // This read should block until something disconnects (or something
-            // similar) which should cause an exception, in which case we should
-            // try to setup again and reconnect
-            mInputStream.read();
-        } catch (IOException e) {
-            Log.e(TAG, "Socket thread disconnected.");
-        }
-        restart();
-    }
-
-    @Override
-    public void send(byte[] buf, int len) {
-        try {
-            mOutputStream.write(buf, 0, len);
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to write data to socket, retrying connection");
-            restart();
-        }
-    }
-
-    @Override
-    public void close() {
-        if (mServerSocket != null) {
-            try {
-                mServerSocket.close();
-            } catch (IOException e) {
-                Log.w(TAG, "Failed to close server socket, ignoring");
-            }
-            mServerSocket = null;
-        }
-        mInputStream = null;
-        mOutputStream = null;
-    }
-}
-
diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp
new file mode 100644
index 0000000..b5bc314
--- /dev/null
+++ b/tests/robotests/Android.bp
@@ -0,0 +1,18 @@
+//############################################
+// Messenger Robolectric test target. #
+//############################################
+
+android_robolectric_test {
+    name: "DirectRenderingClusterTests",
+
+    srcs: ["src/**/*.java"],
+
+    java_resource_dirs: ["config"],
+
+    // Include the testing libraries
+    libs: [
+        "android.car",
+    ],
+
+    instrumentation_for: "DirectRenderingCluster",
+}
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
deleted file mode 100644
index 631a403..0000000
--- a/tests/robotests/Android.mk
+++ /dev/null
@@ -1,46 +0,0 @@
-#############################################
-# Messenger Robolectric test target. #
-#############################################
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := DirectRenderingClusterTests
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-# Include the testing libraries
-LOCAL_JAVA_LIBRARIES := \
-    robolectric_android-all-stub \
-    Robolectric_all-target \
-    mockito-robolectric-prebuilt \
-    truth-prebuilt \
-    android.car
-
-LOCAL_INSTRUMENTATION_FOR := DirectRenderingCluster
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Messenger runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunDirectRenderingClusterTests
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_JAVA_LIBRARIES := \
-    DirectRenderingClusterTests \
-    robolectric_android-all-stub \
-    Robolectric_all-target \
-    mockito-robolectric-prebuilt \
-    truth-prebuilt \
-    android.car
-
-LOCAL_TEST_PACKAGE := DirectRenderingCluster
-
-LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
-
-include external/robolectric-shadows/run_robotests.mk
diff --git a/tests/Android.mk b/tests/robotests/config/robolectric.properties
similarity index 72%
rename from tests/Android.mk
rename to tests/robotests/config/robolectric.properties
index 9f0a4e8..cb4cb3e 100644
--- a/tests/Android.mk
+++ b/tests/robotests/config/robolectric.properties
@@ -1,4 +1,4 @@
-# Copyright (C) 2019 The Android Open Source Project
+# 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.
@@ -11,9 +11,5 @@
 # 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.
+sdk=NEWEST_SDK
 
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all makefiles in subdirectories
-include $(call all-makefiles-under,$(LOCAL_PATH))