Merge "Making the cluster UI more realistic with speedometers."
diff --git a/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml b/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml
new file mode 100644
index 0000000..9879024
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="200dp"
+    android:height="200dp"
+    android:viewportHeight="64"
+    android:viewportWidth="64">
+
+    <path
+        android:pathData="M0,32
+        A32,32 0 1,1 64,32
+        A32,32 0 1,1 0,32 Z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:centerX="32"
+                android:centerY="32"
+                android:gradientRadius="32"
+                android:type="radial">
+                <item
+                    android:color="#FF000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#FF000000"
+                    android:offset="0.92" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+
+    <path
+        android:fillColor="#000000"
+        android:strokeColor="#FFFFFF"
+        android:strokeWidth="0.25"
+        android:pathData="M2,32
+        A30,30 0 1,1 62,32
+        A30,30 0 1,1 2,32 Z" />
+
+</vector>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
index 2cb9662..23f01b0 100644
--- a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
@@ -1,12 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/activity_main"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@android:color/background_dark"
-    tools:context=".MainClusterActivity"
-    android:windowIsFloating="true">
+    android:windowIsFloating="true"
+    tools:context=".MainClusterActivity">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/left_unobscured"
+        app:layout_constraintGuide_begin="@dimen/speedometer_overlap_width"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/right_unobscured"
+        app:layout_constraintGuide_end="@dimen/speedometer_overlap_width"
+        android:orientation="vertical"/>
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -32,27 +48,27 @@
                 android:layout_width="48dp"
                 android:layout_height="48dp"
                 android:background="@drawable/btn_nav"
-                android:layout_margin="10dp"
+                android:layout_margin="5dp"
                 android:focusableInTouchMode="true" />
             <Button
                 android:id="@+id/btn_phone"
                 android:layout_width="48dp"
                 android:layout_height="48dp"
-                android:layout_margin="10dp"
+                android:layout_margin="5dp"
                 android:background="@drawable/btn_phone"
                 android:focusableInTouchMode="true" />
             <Button
                 android:id="@+id/btn_music"
                 android:layout_width="48dp"
                 android:layout_height="48dp"
-                android:layout_margin="10dp"
+                android:layout_margin="5dp"
                 android:background="@drawable/btn_music"
                 android:focusableInTouchMode="true" />
             <Button
                 android:id="@+id/btn_car_info"
                 android:layout_width="48dp"
                 android:layout_height="48dp"
-                android:layout_margin="10dp"
+                android:layout_margin="5dp"
                 android:background="@drawable/btn_car_info"
                 android:focusableInTouchMode="true" />
 
@@ -69,12 +85,24 @@
         </LinearLayout>
     </LinearLayout>
 
-    <TextView
-            android:id="@+id/text_overlay"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_centerInParent="true"
-            android:background="@android:color/background_light"
-            android:textSize="30sp"
-    />
-</RelativeLayout>
+    <ImageView
+        android:id="@+id/maneuver"
+        android:layout_width="@dimen/speedometer_width"
+        android:layout_height="@dimen/speedometer_height"
+        android:src="@drawable/speedometer"
+        android:elevation="2dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintRight_toLeftOf="@+id/left_unobscured"/>
+
+    <ImageView
+        android:id="@+id/maneuver"
+        android:layout_width="@dimen/speedometer_width"
+        android:layout_height="@dimen/speedometer_height"
+        android:src="@drawable/speedometer"
+        android:elevation="2dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toRightOf="@+id/right_unobscured"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
index 865a3b5..54894c2 100644
--- a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
@@ -34,13 +34,13 @@
 
     <ImageView
         android:layout_width="match_parent"
-        android:layout_height="30dp"
+        android:layout_height="@dimen/navigation_gradient_height"
         android:src="@drawable/gradient_top"
         app:layout_constraintTop_toTopOf="parent"/>
 
     <ImageView
         android:layout_width="match_parent"
-        android:layout_height="30dp"
+        android:layout_height="@dimen/navigation_gradient_height"
         android:src="@drawable/gradient_bottom"
         app:layout_constraintBottom_toBottomOf="parent"/>
 
diff --git a/tests/DirectRenderingClusterSample/res/values/dimens.xml b/tests/DirectRenderingClusterSample/res/values/dimens.xml
index 47c8224..80f5206 100644
--- a/tests/DirectRenderingClusterSample/res/values/dimens.xml
+++ b/tests/DirectRenderingClusterSample/res/values/dimens.xml
@@ -2,4 +2,10 @@
     <!-- Default screen margins, per the Android Design guidelines. -->
     <dimen name="activity_horizontal_margin">16dp</dimen>
     <dimen name="activity_vertical_margin">16dp</dimen>
+    <!-- Size and position of speedometers -->
+    <dimen name="speedometer_height">500dp</dimen>
+    <dimen name="speedometer_width">500dp</dimen>
+    <dimen name="speedometer_overlap_width">80dp</dimen>
+    <!-- Navigation fragment gradients -->
+    <dimen name="navigation_gradient_height">30dp</dimen>
 </resources>
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
index 8b69e65..d102a49 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -22,6 +22,7 @@
 import android.app.IProcessObserver;
 import android.app.TaskStackListener;
 import android.content.ComponentName;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -51,6 +52,7 @@
     private IActivityManager mActivityManager;
     // Listeners of top activity changes, indexed by the displayId they are interested on.
     private final Map<Integer, Set<ActivityListener>> mListeners = new HashMap<>();
+    private final Handler mHandler = new Handler();
     private final IProcessObserver.Stub mProcessObserver = new IProcessObserver.Stub() {
         @Override
         public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
@@ -120,19 +122,25 @@
         mActivityManager = null;
     }
 
+    /**
+     * Notifies listeners on changes of top activities. {@link ActivityManager} might trigger
+     * updates on threads different than UI.
+     */
     private void notifyTopActivities() {
-        try {
-            List<StackInfo> infos = mActivityManager.getAllStackInfos();
-            for (StackInfo info : infos) {
-                Set<ActivityListener> listeners = mListeners.get(info.displayId);
-                if (listeners != null && !listeners.isEmpty()) {
-                    for (ActivityListener listener : listeners) {
-                        listener.onTopActivityChanged(info.displayId, info.topActivity);
+        mHandler.post(() -> {
+            try {
+                List<StackInfo> infos = mActivityManager.getAllStackInfos();
+                for (StackInfo info : infos) {
+                    Set<ActivityListener> listeners = mListeners.get(info.displayId);
+                    if (listeners != null && !listeners.isEmpty()) {
+                        for (ActivityListener listener : listeners) {
+                            listener.onTopActivityChanged(info.displayId, info.topActivity);
+                        }
                     }
                 }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Cannot getTasks", e);
             }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Cannot getTasks", e);
-        }
+        });
     }
 }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index 74125c3..8a383fd 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -15,29 +15,17 @@
  */
 package android.car.cluster.sample;
 
-import static android.car.cluster.CarInstrumentClusterManager.CATEGORY_NAVIGATION;
-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 java.lang.Integer.parseInt;
 
-import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.car.CarNotConnectedException;
-import android.car.cluster.CarInstrumentClusterManager;
 import android.car.cluster.ClusterActivityState;
 import android.car.cluster.renderer.InstrumentClusterRenderingService;
 import android.car.cluster.renderer.NavigationRenderer;
 import android.car.navigation.CarNavigationInstrumentCluster;
-import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Bundle;
@@ -64,7 +52,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
@@ -83,29 +70,15 @@
     static final int MSG_ON_KEY_EVENT = 3;
     static final int MSG_REGISTER_CLIENT = 4;
     static final int MSG_UNREGISTER_CLIENT = 5;
-    static final int MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED = 6;
     static final String MSG_KEY_CATEGORY = "category";
     static final String MSG_KEY_ACTIVITY_DISPLAY_ID = "activity_display_id";
     static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
     static final String MSG_KEY_KEY_EVENT = "key_event";
-    static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME = "free_navigation_activity_name";
-    static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE =
-            "free_navigation_activity_visible";
-
-    private static final int NAVIGATION_ACTIVITY_MAX_RETRIES = 10;
-    private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
 
     private List<Messenger> mClients = new ArrayList<>();
     private ClusterDisplayProvider mDisplayProvider;
     private int mDisplayId = NO_DISPLAY;
-    private UserReceiver mUserReceiver;
-    private final Handler mHandler = new Handler();
     private final IBinder mLocalBinder = new Messenger(new MessageHandler(this)).getBinder();
-    private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
-    private int mNavigationDisplayId = NO_DISPLAY;
-    private ComponentName mFreeNavigationActivity;
-    private ActivityMonitor mActivityMonitor = new ActivityMonitor();
-    private boolean mFreeNavigationActivityVisible;
 
     private final DisplayListener mDisplayListener = new DisplayListener() {
         @Override
@@ -126,31 +99,6 @@
         }
     };
 
-    private static class UserReceiver extends BroadcastReceiver {
-        private WeakReference<ClusterRenderingServiceImpl> mService;
-
-        UserReceiver(ClusterRenderingServiceImpl service) {
-            mService = new WeakReference<>(service);
-        }
-
-        public void register(Context context) {
-            IntentFilter intentFilter =  new IntentFilter(ACTION_USER_UNLOCKED);
-            intentFilter.addAction(ACTION_USER_SWITCHED);
-            context.registerReceiver(this, intentFilter);
-        }
-
-        public void unregister(Context context) {
-            context.unregisterReceiver(this);
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            ClusterRenderingServiceImpl service = mService.get();
-            Log.d(TAG, "Broadcast received: " + intent);
-            service.tryLaunchNavigationActivity();
-        }
-    }
-
     private static class MessageHandler extends Handler {
         private final WeakReference<ClusterRenderingServiceImpl> mService;
 
@@ -175,15 +123,10 @@
                                 category, options, options.getLaunchDisplayId()));
                         mService.get().setClusterActivityState(category, state);
                         Log.d(TAG, String.format("activity state set: %s = %s", category, state));
-
-                        // Starting a default navigation activity. This would take place until any
-                        // navigation app takes focus.
-                        mService.get().startNavigationActivity(displayId);
                         break;
                     }
                     case MSG_REGISTER_CLIENT:
                         mService.get().mClients.add(msg.replyTo);
-                        mService.get().notifyFreeNavigationActivityChange();
                         break;
                     case MSG_UNREGISTER_CLIENT:
                         mService.get().mClients.remove(msg.replyTo);
@@ -197,17 +140,6 @@
         }
     }
 
-    private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
-        if (displayId != mNavigationDisplayId) {
-            return;
-        }
-        boolean activityVisible = activity != null && activity.equals(mFreeNavigationActivity);
-        if (activityVisible != mFreeNavigationActivityVisible) {
-            mFreeNavigationActivityVisible = activityVisible;
-            notifyFreeNavigationActivityChange();
-        }
-    };
-
     @Override
     public IBinder onBind(Intent intent) {
         Log.d(TAG, "onBind, intent: " + intent);
@@ -221,9 +153,6 @@
         super.onCreate();
         Log.d(TAG, "onCreate");
         mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
-        mUserReceiver = new UserReceiver(this);
-        mUserReceiver.register(this);
-        mActivityMonitor.start();
     }
 
     private void launchMainActivity() {
@@ -265,14 +194,6 @@
     }
 
     @Override
-    public void onDestroy() {
-        super.onDestroy();
-        Log.w(TAG, "onDestroy");
-        mUserReceiver.unregister(this);
-        mActivityMonitor.stop();
-    }
-
-    @Override
     protected NavigationRenderer getNavigationRenderer() {
         NavigationRenderer navigationRenderer = new NavigationRenderer() {
             @Override
@@ -406,119 +327,4 @@
             }
         }
     }
-
-    private void startNavigationActivity(int displayId) {
-        mActivityMonitor.removeListener(mNavigationDisplayId, mNavigationActivityMonitor);
-        mActivityMonitor.addListener(displayId, mNavigationActivityMonitor);
-        mNavigationDisplayId = displayId;
-        tryLaunchNavigationActivity();
-    }
-
-    /**
-     * Tries to start a default navigation activity in the cluster. During system initialization
-     * launching user activities might fail due the system not being ready or {@link PackageManager}
-     * not being able to resolve the implicit intent. It is also possible that the system doesn't
-     * have a default navigation activity selected yet.
-     */
-    private void tryLaunchNavigationActivity() {
-        int userHandle = ActivityManager.getCurrentUser();
-        if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
-                        userHandle, mNavigationDisplayId));
-            }
-            // Not ready to launch yet.
-            return;
-        }
-        mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
-
-        ComponentName navigationActivity = getNavigationActivity();
-        if (!Objects.equals(navigationActivity, mFreeNavigationActivity)) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Navigation activity change detected: " + navigationActivity);
-            }
-            mFreeNavigationActivity = navigationActivity;
-            notifyFreeNavigationActivityChange();
-        }
-
-        try {
-            if (navigationActivity == null) {
-                throw new ActivityNotFoundException();
-            }
-            Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION)
-                    .setPackage(navigationActivity.getPackageName())
-                    .setComponent(navigationActivity)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplayId);
-            Bundle activityOptions = ActivityOptions.makeBasic()
-                    .setLaunchDisplayId(mNavigationDisplayId)
-                    .toBundle();
-
-            startActivityAsUser(intent, activityOptions, UserHandle.CURRENT);
-        } catch (ActivityNotFoundException ex) {
-            // Some activities might not be available right on startup. We will retry.
-            mHandler.postDelayed(mRetryLaunchNavigationActivity,
-                    NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
-        } catch (Exception ex) {
-            Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
-        }
-    }
-
-    private void notifyFreeNavigationActivityChange() {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME, mFreeNavigationActivity);
-        bundle.putBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE, mFreeNavigationActivityVisible);
-        broadcastClientMessage(MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED, bundle);
-    }
-
-    /**
-     * Returns a default navigation activity to show in the cluster.
-     * In the current implementation we search for an activity with the
-     * {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} category from the same navigation app
-     * selected from CarLauncher (see CarLauncher#getMapsIntent()).
-     * Alternatively, other implementations could:
-     * <ul>
-     * <li>Read this package from a resource (having a OEM default activity to show)
-     * <li>Let the user select one from settings.
-     * </ul>
-     */
-    private ComponentName getNavigationActivity() {
-        PackageManager pm = getPackageManager();
-        int userId = ActivityManager.getCurrentUser();
-
-        // Get currently selected navigation app.
-        Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
-                Intent.CATEGORY_APP_MAPS);
-        ResolveInfo navigationApp = pm.resolveActivityAsUser(intent,
-                PackageManager.MATCH_DEFAULT_ONLY, userId);
-
-        // Get all possible cluster activities
-        intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION);
-        List<ResolveInfo> candidates = pm.queryIntentActivitiesAsUser(intent, 0, userId);
-
-        // If there is a select navigation app, try finding a matching auxiliary navigation activity
-        if (navigationApp != null) {
-            Log.d(TAG, "Current navigation app: " + navigationApp);
-            for (ResolveInfo candidate : candidates) {
-                Log.d(TAG, "Candidate: " + candidate);
-                if (candidate.activityInfo.packageName.equals(navigationApp.activityInfo
-                        .packageName)) {
-                    Log.d(TAG, "Found activity: " + candidate);
-                    return new ComponentName(candidate.activityInfo.packageName,
-                            candidate.activityInfo.name);
-                }
-            }
-        } else {
-            Log.d(TAG, "NO CURRENT ACTIVITY");
-            for (ResolveInfo candidate : candidates) {
-                Log.d(TAG, "Candidate: " + candidate);
-            }
-        }
-
-        // During initialization implicit intents might not provided a result. We will just
-        // retry until we find one, or we exhaust the retries.
-        Log.d(TAG, "No default activity found (it might not be available yet).");
-        return null;
-    }
-
 }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
index 55c9e54..61c8dd0 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
@@ -17,6 +17,7 @@
 
 import android.app.Application;
 import android.content.ComponentName;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.AndroidViewModel;
@@ -29,44 +30,21 @@
  * {@link AndroidViewModel} for cluster information.
  */
 public class ClusterViewModel extends AndroidViewModel {
-    /**
-     * Reference to a component (e.g.: an activity) and whether such component is visible or not.
-     */
-    public static class ComponentVisibility {
-        /**
-         * Application component name
-         */
-        public final ComponentName mComponent;
-        /**
-         * Whether the component is currently visible to the user or not.
-         */
-        public final boolean mIsVisible;
+    private static final String TAG = "Cluster.ViewModel";
 
-        /**
-         * Creates a new component visibility reference
-         */
-        private ComponentVisibility(ComponentName component, boolean isVisible) {
-            mComponent = component;
-            mIsVisible = isVisible;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            ComponentVisibility that = (ComponentVisibility) o;
-            return mIsVisible == that.mIsVisible
-                    && Objects.equals(mComponent, that.mComponent);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mComponent, mIsVisible);
-        }
+    public enum NavigationActivityState {
+        /** No activity has been selected to be displayed on the navigation fragment yet */
+        NOT_SELECTED,
+        /** An activity has been selected, but it is not yet visible to the user */
+        LOADING,
+        /** Navigation activity is visible to the user */
+        VISIBLE,
     }
 
-    private final MutableLiveData<ComponentVisibility> mFreeNavigationActivity =
-            new MutableLiveData<>(new ComponentVisibility(null, false));
+    private ComponentName mFreeNavigationActivity;
+    private ComponentName mCurrentNavigationActivity;
+    private final MutableLiveData<NavigationActivityState> mNavigationActivityStateLiveData =
+            new MutableLiveData<>();
     private final MutableLiveData<Boolean> mNavigationFocus = new MutableLiveData<>(false);
 
     /**
@@ -77,20 +55,17 @@
     }
 
     /**
-     * Returns a {@link LiveData} providing the activity selected to be displayed on the cluster
-     * when navigation focus is not granted (a.k.a.: free navigation). It also indicates whether
-     * such activity is currently visible to the user or not.
+     * Returns a {@link LiveData} providing the current state of the activity displayed on the
+     * navigation fragment.
      */
-    public LiveData<ComponentVisibility> getFreeNavigationActivity() {
-        return mFreeNavigationActivity;
+    public LiveData<NavigationActivityState> getNavigationActivityState() {
+        return mNavigationActivityStateLiveData;
     }
 
     /**
      * Returns a {@link LiveData} indicating whether navigation focus is currently being granted
      * or not. This indicates whether a navigation application is currently providing driving
-     * directions. Instrument cluster can use this signal to show/hide turn-by-turn
-     * directions UI, and hide/show the free navigation activity
-     * (see {@link #getFreeNavigationActivity()}).
+     * directions.
      */
     public LiveData<Boolean> getNavigationFocus() {
         return mNavigationFocus;
@@ -98,12 +73,22 @@
 
     /**
      * Sets the activity selected to be displayed on the cluster when no driving directions are
-     * being provided, and whether such activity is currently visible to the user or not
+     * being provided.
      */
-    public void setFreeNavigationActivity(ComponentName application, boolean isVisible) {
-        ComponentVisibility newValue = new ComponentVisibility(application, isVisible);
-        if (!Objects.equals(mFreeNavigationActivity.getValue(), newValue)) {
-            mFreeNavigationActivity.setValue(new ComponentVisibility(application, isVisible));
+    public void setFreeNavigationActivity(ComponentName activity) {
+        if (!Objects.equals(activity, mFreeNavigationActivity)) {
+            mFreeNavigationActivity = activity;
+            updateNavigationActivityLiveData();
+        }
+    }
+
+    /**
+     * Sets the activity currently being displayed on the cluster.
+     */
+    public void setCurrentNavigationActivity(ComponentName activity) {
+        if (!Objects.equals(activity, mCurrentNavigationActivity)) {
+            mCurrentNavigationActivity = activity;
+            updateNavigationActivityLiveData();
         }
     }
 
@@ -113,6 +98,34 @@
     public void setNavigationFocus(boolean navigationFocus) {
         if (mNavigationFocus.getValue() == null || mNavigationFocus.getValue() != navigationFocus) {
             mNavigationFocus.setValue(navigationFocus);
+            updateNavigationActivityLiveData();
+        }
+    }
+
+    private void updateNavigationActivityLiveData() {
+        NavigationActivityState newState = calculateNavigationActivityState();
+        if (newState != mNavigationActivityStateLiveData.getValue()) {
+            mNavigationActivityStateLiveData.setValue(newState);
+        }
+    }
+
+    private NavigationActivityState calculateNavigationActivityState() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("Current state: current activity = '%s', free nav activity = "
+                            + "'%s', focus = %s", mCurrentNavigationActivity,
+                    mFreeNavigationActivity,
+                    mNavigationFocus.getValue()));
+        }
+        if (mNavigationFocus.getValue() != null && mNavigationFocus.getValue()) {
+            // Car service controls which activity is displayed while driving, so we assume this
+            // has already been taken care of.
+            return NavigationActivityState.VISIBLE;
+        } else if (mFreeNavigationActivity == null) {
+            return NavigationActivityState.NOT_SELECTED;
+        } else if (Objects.equals(mFreeNavigationActivity, mCurrentNavigationActivity)) {
+            return NavigationActivityState.VISIBLE;
+        } else {
+            return NavigationActivityState.LOADING;
         }
     }
 }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 2a2ac73..7f32a3b 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -20,12 +20,7 @@
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
-        .MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
-        .MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl
         .MSG_ON_NAVIGATION_STATE_CHANGED;
@@ -33,14 +28,25 @@
 import static android.car.cluster.sample.ClusterRenderingServiceImpl
         .MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
 import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
+import static android.content.Intent.ACTION_USER_SWITCHED;
+import static android.content.Intent.ACTION_USER_UNLOCKED;
 
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.car.Car;
 import android.car.CarAppFocusManager;
 import android.car.CarNotConnectedException;
+import android.car.cluster.CarInstrumentClusterManager;
 import android.car.cluster.ClusterActivityState;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
@@ -48,6 +54,7 @@
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Display;
@@ -69,11 +76,30 @@
 import java.lang.ref.WeakReference;
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
+import java.util.List;
 
+/**
+ * Main activity displayed on the instrument cluster. This activity contains fragments for each of
+ * the cluster "facets" (e.g.: navigation, communication, media and car state). Users can navigate
+ * to each facet by using the steering wheel buttons.
+ * <p>
+ * This activity runs on "system user" (see {@link UserHandle#USER_SYSTEM}) but it is visible on
+ * all users (the same activity remains active even during user switch).
+ * <p>
+ * This activity also launches a default navigation app inside a virtual display (which is located
+ * inside {@link NavigationFragment}). This navigation app is launched when:
+ * <ul>
+ * <li>Virtual display for navigation apps is ready.
+ * <li>After every user switch.
+ * </ul>
+ * This is necessary because the navigation app runs under a normal user, and different users will
+ * see different instances of the same application, with their own personalized data.
+ */
 public class MainClusterActivity extends FragmentActivity {
     private static final String TAG = "Cluster.MainActivity";
 
     private static final NavigationState NULL_NAV_STATE = new NavigationState.Builder().build();
+    private static final int NO_DISPLAY = -1;
 
     private ViewPager mPager;
     private NavStateController mNavStateController;
@@ -89,13 +115,26 @@
     private Car mCar;
     private CarAppFocusManager mCarAppFocusManager;
 
-    public static class VirtualDisplay {
-        public final int mDisplayId;
-        public final Rect mBounds;
+    private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
 
-        public VirtualDisplay(int displayId, Rect bounds) {
+    private UserReceiver mUserReceiver;
+    private ActivityMonitor mActivityMonitor = new ActivityMonitor();
+    private final Handler mHandler = new Handler();
+    private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
+    private int mNavigationDisplayId = NO_DISPLAY;
+
+    /**
+     * Description of a virtual display
+     */
+    public static class VirtualDisplay {
+        /** Identifier of the display */
+        public final int mDisplayId;
+        /** Rectangular area inside this display that can be viewed without obstructions */
+        public final Rect mUnobscuredBounds;
+
+        public VirtualDisplay(int displayId, Rect unobscuredBounds) {
             mDisplayId = displayId;
-            mBounds = bounds;
+            mUnobscuredBounds = unobscuredBounds;
         }
     }
 
@@ -184,19 +223,46 @@
                         mActivity.get().onNavigationStateChange(navState);
                     }
                     break;
-                case MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED:
-                    ComponentName activity = data.getParcelable(
-                            MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME);
-                    boolean isVisible = data.getBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE);
-                    mActivity.get().mClusterViewModel.setFreeNavigationActivity(activity,
-                            isVisible);
-                    break;
                 default:
                     super.handleMessage(msg);
             }
         }
     }
 
+    private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
+        if (displayId != mNavigationDisplayId) {
+            return;
+        }
+        mClusterViewModel.setCurrentNavigationActivity(activity);
+    };
+
+    private static class UserReceiver extends BroadcastReceiver {
+        private WeakReference<MainClusterActivity> mActivity;
+
+        UserReceiver(MainClusterActivity activity) {
+            mActivity = new WeakReference<>(activity);
+        }
+
+        public void register(Context context) {
+            IntentFilter intentFilter =  new IntentFilter(ACTION_USER_UNLOCKED);
+            intentFilter.addAction(ACTION_USER_SWITCHED);
+            context.registerReceiver(this, intentFilter);
+        }
+
+        public void unregister(Context context) {
+            context.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            MainClusterActivity activity = mActivity.get();
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Broadcast received: " + intent);
+            }
+            activity.tryLaunchNavigationActivity();
+        }
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -221,17 +287,29 @@
         mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
 
         mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
-        mClusterViewModel.getNavigationFocus().observe(this, active ->
-                mNavStateController.setActive(active));
+        mClusterViewModel.getNavigationFocus().observe(this, focus -> {
+            mNavStateController.setActive(focus);
+            // If focus is lost, we launch the default navigation activity again.
+            if (!focus) {
+                tryLaunchNavigationActivity();
+            }
+        });
 
         mCar = Car.createCar(this, mCarServiceConnection);
         mCar.connect();
+
+        mActivityMonitor.start();
+
+        mUserReceiver = new UserReceiver(this);
+        mUserReceiver.register(this);
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
         Log.d(TAG, "onDestroy");
+        mUserReceiver.unregister(this);
+        mActivityMonitor.stop();
         mCar.disconnect();
         mCarAppFocusManager = null;
         if (mService != null) {
@@ -259,6 +337,10 @@
     }
 
     public void updateNavDisplay(VirtualDisplay virtualDisplay) {
+        // Starting the default navigation activity. This activity will be shown when navigation
+        // focus is not taken.
+        startNavigationActivity(virtualDisplay.mDisplayId);
+        // Notify the service (so it updates display properties on car service)
         if (mService == null) {
             // Service is not bound yet. Hold the information and notify when the service is bound.
             mPendingVirtualDisplay = virtualDisplay;
@@ -274,7 +356,7 @@
         data.putInt(MSG_KEY_ACTIVITY_DISPLAY_ID, virtualDisplay.mDisplayId);
         data.putBundle(MSG_KEY_ACTIVITY_STATE, ClusterActivityState
                 .create(virtualDisplay.mDisplayId != Display.INVALID_DISPLAY,
-                        virtualDisplay.mBounds)
+                        virtualDisplay.mUnobscuredBounds)
                 .toBundle());
         sendServiceMessage(MSG_SET_ACTIVITY_LAUNCH_OPTIONS, data, null);
     }
@@ -350,4 +432,98 @@
             return mFragment;
         }
     }
+
+    private void startNavigationActivity(int displayId) {
+        mActivityMonitor.removeListener(mNavigationDisplayId, mNavigationActivityMonitor);
+        mActivityMonitor.addListener(displayId, mNavigationActivityMonitor);
+        mNavigationDisplayId = displayId;
+        tryLaunchNavigationActivity();
+    }
+
+    /**
+     * Tries to start a default navigation activity in the cluster. During system initialization
+     * launching user activities might fail due the system not being ready or {@link PackageManager}
+     * not being able to resolve the implicit intent. It is also possible that the system doesn't
+     * have a default navigation activity selected yet.
+     */
+    private void tryLaunchNavigationActivity() {
+        int userHandle = ActivityManager.getCurrentUser();
+        if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
+                        userHandle, mNavigationDisplayId));
+            }
+            // Not ready to launch yet.
+            return;
+        }
+        mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
+
+        ComponentName navigationActivity = getNavigationActivity();
+        mClusterViewModel.setFreeNavigationActivity(navigationActivity);
+
+        try {
+            if (navigationActivity == null) {
+                throw new ActivityNotFoundException();
+            }
+            Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION)
+                    .setPackage(navigationActivity.getPackageName())
+                    .setComponent(navigationActivity)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplayId);
+            Bundle activityOptions = ActivityOptions.makeBasic()
+                    .setLaunchDisplayId(mNavigationDisplayId)
+                    .toBundle();
+
+            startActivityAsUser(intent, activityOptions, UserHandle.CURRENT);
+        } catch (ActivityNotFoundException ex) {
+            // Some activities might not be available right on startup. We will retry.
+            mHandler.postDelayed(mRetryLaunchNavigationActivity,
+                    NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
+        } catch (Exception ex) {
+            Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
+        }
+    }
+
+    /**
+     * Returns a default navigation activity to show in the cluster.
+     * In the current implementation we search for an activity with the
+     * {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} category from the same navigation app
+     * selected from CarLauncher (see CarLauncher#getMapsIntent()).
+     * Alternatively, other implementations could:
+     * <ul>
+     * <li>Read this package from a resource (having a OEM default activity to show)
+     * <li>Let the user select one from settings.
+     * </ul>
+     */
+    private ComponentName getNavigationActivity() {
+        PackageManager pm = getPackageManager();
+        int userId = ActivityManager.getCurrentUser();
+
+        // Get currently selected navigation app.
+        Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
+                Intent.CATEGORY_APP_MAPS);
+        ResolveInfo navigationApp = pm.resolveActivityAsUser(intent,
+                PackageManager.MATCH_DEFAULT_ONLY, userId);
+
+        // Get all possible cluster activities
+        intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION);
+        List<ResolveInfo> candidates = pm.queryIntentActivitiesAsUser(intent, 0, userId);
+
+        // If there is a select navigation app, try finding a matching auxiliary navigation activity
+        if (navigationApp != null) {
+            for (ResolveInfo candidate : candidates) {
+                if (candidate.activityInfo.packageName.equals(navigationApp.activityInfo
+                        .packageName)) {
+                    Log.d(TAG, "Found activity: " + candidate);
+                    return new ComponentName(candidate.activityInfo.packageName,
+                            candidate.activityInfo.name);
+                }
+            }
+        }
+
+        // During initialization implicit intents might not provided a result. We will just
+        // retry until we find one, or we exhaust the retries.
+        Log.d(TAG, "No default activity found (it might not be available yet).");
+        return null;
+    }
 }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
index 407e13a..d785c69 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
@@ -138,8 +138,13 @@
                 Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height
                         + ", format:" + format);
 
-                //Create dummy unobscured area to report to navigation activity.
-                mUnobscuredBounds = new Rect(40, 0, width - 80, height - 40);
+                // Create dummy unobscured area to report to navigation activity.
+                int obscuredWidth = (int) getResources()
+                        .getDimension(R.dimen.speedometer_overlap_width);
+                int obscuredHeight = (int) getResources()
+                        .getDimension(R.dimen.navigation_gradient_height);
+                mUnobscuredBounds = new Rect(obscuredWidth, obscuredHeight, width - obscuredWidth,
+                        height - obscuredHeight);
 
                 if (mVirtualDisplay == null) {
                     mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
@@ -159,10 +164,14 @@
         mProgressBar = root.findViewById(R.id.progress_bar);
         mMessage = root.findViewById(R.id.message);
 
-        mViewModel.getFreeNavigationActivity().observe(this, app -> {
-            mProgressBar.setVisibility(app.mComponent != null && !app.mIsVisible ? View.VISIBLE
-                    : View.GONE);
-            mMessage.setVisibility(app.mComponent == null ? View.VISIBLE : View.GONE);
+        mViewModel.getNavigationActivityState().observe(this, state -> {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "State: " + state);
+            }
+            mProgressBar.setVisibility(state == ClusterViewModel.NavigationActivityState.LOADING
+                    ? View.VISIBLE : View.INVISIBLE);
+            mMessage.setVisibility(state == ClusterViewModel.NavigationActivityState.NOT_SELECTED
+                    ? View.VISIBLE : View.INVISIBLE);
         });
 
         return root;
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index c400bd8..798e6b4 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -33,6 +33,7 @@
     <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
     <uses-permission android:name="android.car.permission.VEHICLE_DYNAMICS_STATE"/>
     <uses-permission android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"/>
+    <uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/>
     <uses-permission android:name="android.car.permission.STORAGE_MONITORING" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.MANAGE_USB" />
@@ -97,10 +98,9 @@
                   android:launchMode="singleInstance"
                   android:resizeableActivity="true"
                   android:allowEmbedded="true"
-                  android:permission="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL">
+                  android:permission="android.car.permission.CAR_DISPLAY_IN_CLUSTER">
             <intent-filter android:priority="-1">
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.car.cluster.NAVIGATION"/>
             </intent-filter>
         </activity>
@@ -108,7 +108,7 @@
         <activity android:name=".activityview.ActivityViewTestFragment"/>
 
         <!-- temporary solution until b/68882625 is fixed. -->
-        <receiver android:name=".touchsound.DisableTouchSoundOnBoot"  android:exported="true">
+        <receiver android:name=".touchsound.DisableTouchSoundOnBoot" android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED"/>
             </intent-filter>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index f58af8f..d244904 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -13,40 +13,47 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:orientation="vertical"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:layout_marginTop="40dp"
-              android:layout_marginStart="40dp">
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:layout_marginTop="40dp"
+    android:layout_marginStart="40dp"
+    android:layout_marginEnd="40dp">
 
     <LinearLayout
-            android:orientation="vertical"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
+        android:orientation="vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
             <Button
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    android:layout_weight="1"
-                    android:text="@string/cluster_start"
-                    android:id="@+id/cluster_start_button"/>
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:padding="20dp"
+                android:text="@string/cluster_start"
+                android:id="@+id/cluster_start_button"/>
             <Button
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    android:layout_weight="1"
-                    android:text="@string/cluster_turn_left"
-                    android:id="@+id/cluster_turn_left_button"/>
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:padding="20dp"
+                android:text="@string/cluster_start_guidance"
+                android:id="@+id/cluster_turn_left_button"/>
             <Button
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    android:layout_weight="1"
-                    android:text="@string/cluster_stop"
-                    android:id="@+id/cluster_stop_button"/>
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:padding="20dp"
+                android:text="@string/cluster_stop"
+                android:id="@+id/cluster_stop_button"/>
             <Button
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    android:layout_weight="1"
-                    android:text="@string/cluster_start_activity"
-                    android:id="@+id/cluster_start_activity"/>
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:padding="20dp"
+                android:text="@string/cluster_start_activity"
+                android:id="@+id/cluster_start_activity"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 35d3ec2..15f2afc 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -126,11 +126,14 @@
     <string name="open_kb_button">Hide/Show Input</string>
 
     <!-- instrument cluster -->
-    <string name="cluster_start">Start metadata</string>
-    <string name="cluster_turn_left">Send turn-by-turn</string>
-    <string name="cluster_stop">Stop metadata</string>
+    <string name="cluster_start">Request focus</string>
+    <string name="cluster_start_guidance">Start turn-by-turn</string>
+    <string name="cluster_stop">Abandon focus</string>
+    <string name="cluster_stop_guidance">Stop turn-by-turn</string>
     <string name="cluster_nav_app_context_loss">Navigation app context lost!</string>
     <string name="cluster_start_activity">Start Nav Activity</string>
+    <string name="cluster_start_activity_failed">Failed to start activity in cluster</string>
+    <string name="cluster_not_started">Missing navigation focus</string>
 
     <!--  input test -->
     <string name="volume_up">Volume +</string>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index ad4b93a..7e40b5b 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -15,7 +15,7 @@
  */
 package com.google.android.car.kitchensink.cluster;
 
-import android.app.AlertDialog;
+import android.annotation.Nullable;
 import android.car.Car;
 import android.car.CarAppFocusManager;
 import android.car.CarNotConnectedException;
@@ -31,14 +31,15 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.Button;
 import android.widget.Toast;
 
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.car.cluster.navigation.NavigationState;
 import androidx.fragment.app.Fragment;
 
+import com.google.android.car.kitchensink.KitchenSinkActivity;
 import com.google.android.car.kitchensink.R;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
@@ -47,10 +48,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.util.Arrays;
 import java.util.Timer;
 import java.util.TimerTask;
-import java.util.stream.Collectors;
 
 /**
  * Contains functions to test instrument cluster API.
@@ -65,35 +64,61 @@
     private Car mCarApi;
     private Timer mTimer;
     private NavigationState[] mNavStateData;
+    private Button mTurnByTurnButton;
 
-    private final ServiceConnection mServiceConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                Log.d(TAG, "Connected to Car Service");
-                try {
-                    mCarNavigationStatusManager =
-                            (CarNavigationStatusManager) mCarApi.getCarManager(
-                                    Car.CAR_NAVIGATION_SERVICE);
-                    mCarAppFocusManager =
-                        (CarAppFocusManager) mCarApi.getCarManager(Car.APP_FOCUS_SERVICE);
-                } catch (CarNotConnectedException e) {
-                    Log.e(TAG, "Car is not connected!", e);
-                }
+    private ServiceConnection mCarServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.d(TAG, "Connected to Car Service");
+            try {
+                mCarNavigationStatusManager = (CarNavigationStatusManager) mCarApi
+                        .getCarManager(Car.CAR_NAVIGATION_SERVICE);
+                mCarAppFocusManager = (CarAppFocusManager) mCarApi
+                        .getCarManager(Car.APP_FOCUS_SERVICE);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Car is not connected!", e);
             }
+        }
 
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-                Log.d(TAG, "Disconnect from Car Service");
-            }
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.d(TAG, "Disconnect from Car Service");
+        }
     };
 
+    private final CarAppFocusManager.OnAppFocusOwnershipCallback mFocusCallback =
+            new CarAppFocusManager.OnAppFocusOwnershipCallback() {
+        @Override
+        public void onAppFocusOwnershipLost(@CarAppFocusManager.AppFocusType int appType) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onAppFocusOwnershipLost, appType: " + appType);
+            }
+            Toast.makeText(getContext(), getText(R.string.cluster_nav_app_context_loss),
+                    Toast.LENGTH_LONG).show();
+        }
+
+        @Override
+        public void onAppFocusOwnershipGranted(@CarAppFocusManager.AppFocusType int appType) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onAppFocusOwnershipGranted, appType: " + appType);
+            }
+        }
+    };
+    private CarAppFocusManager.OnAppFocusChangedListener mOnAppFocusChangedListener =
+            (appType, active) -> {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: " + active);
+                }
+            };
+
+
     private void initCarApi() {
         if (mCarApi != null && mCarApi.isConnected()) {
             mCarApi.disconnect();
             mCarApi = null;
         }
 
-        mCarApi = Car.createCar(getContext(), mServiceConnection);
+        mCarApi = Car.createCar(getContext(), mCarServiceConnection);
         mCarApi.connect();
     }
 
@@ -122,7 +147,7 @@
         InputStream inputStream = getResources().openRawResource(resId);
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
         StringBuilder builder = new StringBuilder();
-        for (String line = null; (line = reader.readLine()) != null; ) {
+        for (String line; (line = reader.readLine()) != null; ) {
             builder.append(line).append("\n");
         }
         return builder.toString();
@@ -135,9 +160,12 @@
         View view = inflater.inflate(R.layout.instrument_cluster, container, false);
 
         view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
-        view.findViewById(R.id.cluster_turn_left_button).setOnClickListener(v -> toogleSendTurn());
+        view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster());
         view.findViewById(R.id.cluster_start_activity).setOnClickListener(v -> startNavActivity());
 
+        mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button);
+        mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn());
+
         return view;
     }
 
@@ -159,14 +187,15 @@
             return;
         }
 
-        // Implicit intent
+        // Implicit intent ("startActivity" method doesn't work with explicit intents)
         Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.addCategory(CarInstrumentClusterManager.CATEGORY_NAVIGATION);
+        intent.setPackage(KitchenSinkActivity.class.getPackage().getName());
         try {
             clusterManager.startActivity(intent);
         } catch (android.car.CarNotConnectedException e) {
             Log.e(TAG, "Failed to startActivity in cluster", e);
-            Toast.makeText(getContext(), "Failed to start activity in cluster",
+            Toast.makeText(getContext(), getText(R.string.cluster_start_activity_failed),
                     Toast.LENGTH_LONG).show();
             return;
         }
@@ -175,32 +204,48 @@
     /**
      * Enables/disables sending turn-by-turn data through the {@link CarNavigationStatusManager}
      */
-    private void toogleSendTurn() {
+    private void toggleSendTurn() {
         // If we haven't yet load the sample navigation state data, do so.
         if (mNavStateData == null) {
             mNavStateData = getNavStateData();
-            Log.i(TAG, "Loaded: " + Arrays.asList(mNavStateData)
-                    .stream()
-                    .map(n -> n.toString())
-                    .collect(Collectors.joining(", ")));
         }
 
         // Toggle a timer to send update periodically.
         if (mTimer == null) {
-            mTimer = new Timer();
-            mTimer.schedule(new TimerTask() {
-                private int mPos;
-
-                @Override
-                public void run() {
-                    sendTurn(mNavStateData[mPos]);
-                    mPos = (mPos + 1) % mNavStateData.length;
-                }
-            }, 0, 1000);
+            startSendTurn();
         } else {
+            stopSendTurn();
+        }
+    }
+
+    private void startSendTurn() {
+        if (mTimer != null) {
+            stopSendTurn();
+        }
+        if (!hasFocus()) {
+            Toast.makeText(getContext(), getText(R.string.cluster_not_started), Toast.LENGTH_LONG)
+                    .show();
+            return;
+        }
+        mTimer = new Timer();
+        mTimer.schedule(new TimerTask() {
+            private int mPos;
+
+            @Override
+            public void run() {
+                sendTurn(mNavStateData[mPos]);
+                mPos = (mPos + 1) % mNavStateData.length;
+            }
+        }, 0, 1000);
+        mTurnByTurnButton.setText(R.string.cluster_stop_guidance);
+    }
+
+    private void stopSendTurn() {
+        if (mTimer != null) {
             mTimer.cancel();
             mTimer = null;
         }
+        mTurnByTurnButton.setText(R.string.cluster_start_guidance);
     }
 
     /**
@@ -218,55 +263,44 @@
     }
 
     private void initCluster() {
-        try {
-            mCarAppFocusManager
-                    .addFocusListener(new CarAppFocusManager.OnAppFocusChangedListener() {
-                        @Override
-                        public void onAppFocusChanged(int appType, boolean active) {
-                            Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: "
-                                    + active);
-                        }
-                    }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
-        } catch (CarNotConnectedException e) {
-            Log.e(TAG, "Failed to register focus listener", e);
+        if (hasFocus()) {
+            return;
         }
-
-        CarAppFocusManager.OnAppFocusOwnershipCallback
-                focusCallback = new CarAppFocusManager.OnAppFocusOwnershipCallback() {
-            @Override
-            public void onAppFocusOwnershipLost(int focus) {
-                Log.w(TAG, "onAppFocusOwnershipLost, focus: " + focus);
-                new AlertDialog.Builder(getContext())
-                        .setTitle(getContext().getApplicationInfo().name)
-                        .setMessage(R.string.cluster_nav_app_context_loss)
-                        .show();
-            }
-
-            @Override
-            public void onAppFocusOwnershipGranted(int focus) {
-                Log.w(TAG, "onAppFocusOwnershipGranted, focus: " + focus);
-            }
-
-        };
         try {
+            mCarAppFocusManager.addFocusListener(mOnAppFocusChangedListener,
+                    CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
             mCarAppFocusManager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION,
-                    focusCallback);
-        } catch (CarNotConnectedException e) {
-            Log.e(TAG, "Failed to set active focus", e);
-        }
-
-        try {
-            boolean ownsFocus = mCarAppFocusManager.isOwningFocus(
-                    focusCallback, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
-            Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus);
-            if (!ownsFocus) {
+                    mFocusCallback);
+            if (!hasFocus()) {
                 throw new RuntimeException("Focus was not acquired.");
             }
         } catch (CarNotConnectedException e) {
-            Log.e(TAG, "Failed to get owned focus", e);
+            Log.e(TAG, "Failed to set active focus", e);
         }
     }
 
+    private boolean hasFocus() {
+        try {
+            boolean ownsFocus = mCarAppFocusManager.isOwningFocus(mFocusCallback,
+                    CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus);
+            }
+            return ownsFocus;
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Failed to get owned focus", e);
+            return false;
+        }
+    }
+
+    private void stopCluster() {
+        stopSendTurn();
+        mCarAppFocusManager.removeFocusListener(mOnAppFocusChangedListener,
+                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+        mCarAppFocusManager.abandonAppFocus(mFocusCallback,
+                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+    }
+
     @Override
     public void onResume() {
         super.onResume();