Allow activities in instrument cluster

- Added new CarInstrumentClusterManager to start activity in the cluster
and subscribe to cluster specific events
- Cluster vendor implementation (InstrumentClusterRenderingService) was
extended, now it can notify Car Service with ActivityOptions that holds
info how to launch activity in the cluster for specific category, also it
can send additional info such as unobscured bounds
(ClusterActivityState)
- added DirectRenderingClusterSample which is an example of vendor
implementation that utilizes new features
- added FakeClusterNavigationActivity in Kitchensink which is a dummy nav
activity that can be run in the cluster, it has appropriate permissions
and activity category in manifest

Test: kitchensink

Bug: b/37500371
Change-Id: Ic4b3709a3b7e1310dbd1c499990eea64479b3333
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index edd4d7c..706f7aa 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.car.annotation.FutureFeature;
+import android.car.cluster.CarInstrumentClusterManager;
 import android.car.content.pm.CarPackageManager;
 import android.car.hardware.CarDiagnosticManager;
 import android.car.hardware.CarSensorManager;
@@ -44,7 +45,6 @@
 import android.util.Log;
 
 import com.android.car.internal.FeatureConfiguration;
-import com.android.car.internal.FeatureUtil;
 import com.android.internal.annotations.GuardedBy;
 
 import java.lang.annotation.Retention;
@@ -85,6 +85,11 @@
      * @hide
      */
     public static final String CAR_NAVIGATION_SERVICE = "car_navigation_service";
+    /**
+     * Service name for {@link CarInstrumentClusterManager}
+     * @hide
+     */
+    public static final String CAR_INSTRUMENT_CLUSTER_SERVICE = "cluster_service";
 
     /**
      * @hide
@@ -170,6 +175,24 @@
             "android.car.permission.CAR_NAVIGATION_MANAGER";
 
     /**
+     * Permission necessary to start activities in the instrument cluster through
+     * {@link CarInstrumentClusterManager}
+     *
+     * @hide
+     */
+    public static final String PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL =
+            "android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
+
+    /**
+     * Application must have this permission in order to be launched in the instrument cluster
+     * display.
+     *
+     * @hide
+     */
+    public static final String PERMISSION_CAR_DISPLAY_IN_CLUSTER =
+            "android.car.permission.CAR_DISPLAY_IN_CLUSTER";
+
+    /**
      * Permission necessary to access car specific communication channel.
      * @hide
      */
@@ -600,6 +623,9 @@
             case VENDOR_EXTENSION_SERVICE:
                 manager = new CarVendorExtensionManager(binder, mEventHandler);
                 break;
+            case CAR_INSTRUMENT_CLUSTER_SERVICE:
+                manager = new CarInstrumentClusterManager(binder, mEventHandler);
+                break;
             case TEST_SERVICE:
                 /* CarTestManager exist in static library. So instead of constructing it here,
                  * only pass binder wrapper so that CarTestManager can be constructed outside. */
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
new file mode 100644
index 0000000..758ce9e
--- /dev/null
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cluster;
+
+import android.car.CarManagerBase;
+import android.car.CarNotConnectedException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * API to work with instrument cluster.
+ *
+ * @hide
+ */
+public class CarInstrumentClusterManager implements CarManagerBase {
+    private static final String TAG = CarInstrumentClusterManager.class.getSimpleName();
+
+    /** @hide */
+    public static final String CATEGORY_NAVIGATION = "android.car.cluster.NAVIGATION";
+
+    /**
+     * When activity in the cluster is launched it will receive {@link ClusterActivityState} in the
+     * intent's extra thus activity will know information about unobscured area, etc. upon activity
+     * creation.
+     *
+     * @hide
+     */
+    public static final String KEY_EXTRA_ACTIVITY_STATE =
+            "android.car.cluster.ClusterActivityState";
+
+    private final EventHandler mHandler;
+    private final Map<String, Set<Callback>> mCallbacksByCategory = new HashMap<>(0);
+    private final Object mLock = new Object();
+    private final Map<String, Bundle> mActivityStatesByCategory = new HashMap<>(0);
+
+    private final IInstrumentClusterManagerService mService;
+
+    private ClusterManagerCallback mServiceToManagerCallback;
+
+    /**
+     * Starts activity in the instrument cluster.
+     *
+     * @hide
+     */
+    public void startActivity(Intent intent) throws CarNotConnectedException {
+        try {
+            mService.startClusterActivity(intent);
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Caller of this method will receive immediate callback with the most recent state if state
+     * exists for given category.
+     *
+     * @param category category of the activity in the cluster,
+     *                         see {@link #CATEGORY_NAVIGATION}
+     * @param callback instance of {@link Callback} class to receive events.
+     *
+     * @hide
+     */
+    public void registerCallback(String category, Callback callback)
+            throws CarNotConnectedException {
+        Log.i(TAG, "registerCallback, category: " + category + ", callback: " + callback);
+        ClusterManagerCallback callbackToCarService = null;
+        synchronized (mLock) {
+            Set<Callback> callbacks = mCallbacksByCategory.get(category);
+            if (callbacks == null) {
+                callbacks = new HashSet<>(1);
+                mCallbacksByCategory.put(category, callbacks);
+            }
+            if (!callbacks.add(callback)) {
+                Log.w(TAG, "registerCallback: already registered");
+                return;  // already registered
+            }
+
+            if (mActivityStatesByCategory.containsKey(category)) {
+                Log.i(TAG, "registerCallback: sending activity state...");
+                callback.onClusterActivityStateChanged(
+                        category, mActivityStatesByCategory.get(category));
+            }
+
+            if (mServiceToManagerCallback == null) {
+                Log.i(TAG, "registerCallback: registering callback with car service...");
+                mServiceToManagerCallback = new ClusterManagerCallback();
+                callbackToCarService = mServiceToManagerCallback;
+            }
+        }
+        try {
+            mService.registerCallback(callbackToCarService);
+            Log.i(TAG, "registerCallback: done");
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     * Unregisters given callback for all activity categories.
+     *
+     * @param callback previously registered callback
+     *
+     * @hide
+     */
+    public void unregisterCallback(Callback callback) throws CarNotConnectedException {
+        List<String> keysToRemove = new ArrayList<>(1);
+        synchronized (mLock) {
+            for (Map.Entry<String, Set<Callback>> entry : mCallbacksByCategory.entrySet()) {
+                Set<Callback> callbacks = entry.getValue();
+                if (callbacks.remove(callback) && callbacks.isEmpty()) {
+                    keysToRemove.add(entry.getKey());
+                }
+
+            }
+
+            for (String key: keysToRemove) {
+                mCallbacksByCategory.remove(key);
+            }
+
+            if (mCallbacksByCategory.isEmpty()) {
+                try {
+                    mService.unregisterCallback(mServiceToManagerCallback);
+                } catch (RemoteException e) {
+                    throw new CarNotConnectedException(e);
+                }
+                mServiceToManagerCallback = null;
+            }
+        }
+    }
+
+    /** @hide */
+    public CarInstrumentClusterManager(IBinder service, Handler handler) {
+        mService = IInstrumentClusterManagerService.Stub.asInterface(service);
+
+        mHandler = new EventHandler(handler.getLooper());
+    }
+
+    /** @hide */
+    public interface Callback {
+
+        /**
+         * Notify client that activity state was changed.
+         *
+         * @param category cluster activity category, see {@link #CATEGORY_NAVIGATION}
+         * @param clusterActivityState see {@link ClusterActivityState} how to read this bundle.
+         */
+        void onClusterActivityStateChanged(String category, Bundle clusterActivityState);
+    }
+
+    /** @hide */
+    @Override
+    public void onCarDisconnected() {
+    }
+
+    private class EventHandler extends Handler {
+
+        final static int MSG_ACTIVITY_STATE = 1;
+
+        EventHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.i(TAG, "handleMessage, message: " + msg);
+            switch (msg.what) {
+                case MSG_ACTIVITY_STATE:
+                    Pair<String, Bundle> info = (Pair<String, Bundle>) msg.obj;
+                    String category = info.first;
+                    Bundle state = info.second;
+                    List<CarInstrumentClusterManager.Callback> callbacks = null;
+                    synchronized (mLock) {
+                        if (mCallbacksByCategory.containsKey(category)) {
+                            callbacks = new ArrayList<>(mCallbacksByCategory.get(category));
+                        }
+                    }
+                    Log.i(TAG, "handleMessage, callbacks: " + callbacks);
+                    if (callbacks != null) {
+                        for (CarInstrumentClusterManager.Callback cb : callbacks) {
+                            cb.onClusterActivityStateChanged(category, state);
+                        }
+                    }
+                    break;
+                default:
+                    Log.e(TAG, "Unexpected message: " + msg.what);
+            }
+        }
+    }
+
+    private class ClusterManagerCallback extends IInstrumentClusterManagerCallback.Stub {
+
+        @Override
+        public void setClusterActivityState(String category, Bundle clusterActivityState)
+                throws RemoteException {
+            Log.i(TAG, "setClusterActivityState, category: " + category);
+            synchronized (mLock) {
+                mActivityStatesByCategory.put(category, clusterActivityState);
+            }
+
+            mHandler.sendMessage(mHandler.obtainMessage(EventHandler.MSG_ACTIVITY_STATE,
+                    new Pair<>(category, clusterActivityState)));
+        }
+    }
+}
\ No newline at end of file
diff --git a/car-lib/src/android/car/cluster/ClusterActivityState.java b/car-lib/src/android/car/cluster/ClusterActivityState.java
new file mode 100644
index 0000000..9a6223c
--- /dev/null
+++ b/car-lib/src/android/car/cluster/ClusterActivityState.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cluster;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Bundle;
+
+/**
+ * Helper class that represents activity state in the cluster and can be serialized / deserialized
+ * to/from bundle.
+ * @hide
+ */
+public class ClusterActivityState {
+    private static final String KEY_VISIBLE = "android.car:activityState.visible";
+    private static final String KEY_UNOBSCURED_BOUNDS = "android.car:activityState.unobscured";
+    private static final String KEY_EXTRAS = "android.car:activityState.extras";
+
+    private boolean mVisible = true;
+    private Rect mUnobscuredBounds;
+    private Bundle mExtras;
+
+    public boolean isVisible() {
+        return mVisible;
+    }
+
+    @Nullable public Rect getUnobscuredBounds() {
+        return mUnobscuredBounds;
+    }
+
+    public ClusterActivityState setVisible(boolean visible) {
+        mVisible = visible;
+        return this;
+    }
+
+    public ClusterActivityState setUnobscuredBounds(Rect unobscuredBounds) {
+        mUnobscuredBounds = unobscuredBounds;
+        return this;
+    }
+
+    public ClusterActivityState setExtras(Bundle bundle) {
+        mExtras = bundle;
+        return this;
+    }
+
+    /** Use factory methods instead. */
+    private ClusterActivityState() {}
+
+    public static ClusterActivityState create(boolean visible, Rect unobscuredBounds) {
+        return new ClusterActivityState()
+                .setVisible(visible)
+                .setUnobscuredBounds(unobscuredBounds);
+    }
+
+    public static ClusterActivityState fromBundle(Bundle bundle) {
+        return new ClusterActivityState()
+                .setVisible(bundle.getBoolean(KEY_VISIBLE, true))
+                .setUnobscuredBounds((Rect) bundle.getParcelable(KEY_UNOBSCURED_BOUNDS))
+                .setExtras(bundle.getBundle(KEY_EXTRAS));
+    }
+
+    public Bundle toBundle() {
+        Bundle b = new Bundle();
+        b.putBoolean(KEY_VISIBLE, mVisible);
+        b.putParcelable(KEY_UNOBSCURED_BOUNDS, mUnobscuredBounds);
+        b.putBundle(KEY_EXTRAS, mExtras);
+        return b;
+    }
+
+    @Override
+    public String toString() {
+        return this.getClass().getSimpleName() + " {"
+                + "visible: " + mVisible + ", "
+                + "unobscuredBounds: " + mUnobscuredBounds
+                + " }";
+    }
+}
diff --git a/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl b/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl
new file mode 100644
index 0000000..91a497d
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IInstrumentClusterManagerCallback.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.car.cluster;
+
+import android.os.Bundle;
+
+/**
+ * Interface from Car Service to {@link android.car.cluster.CarInstrumentClusterManager}
+ * @hide
+ */
+interface IInstrumentClusterManagerCallback {
+    /**
+     * Notifies manager about changes in the cluster activity state.
+     *
+     * @param category cluster activity category to which this state applies,
+     *        see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+     * @param clusterActivityState is a {@link Bundle} object,
+     *        see {@link android.car.cluster.ClusterActivityState} for how to construct the bundle.
+     * @hide
+     */
+    oneway void setClusterActivityState(String category, in Bundle clusterActivityState);
+}
diff --git a/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl b/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl
new file mode 100644
index 0000000..aaaeaee
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IInstrumentClusterManagerService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.car.cluster;
+
+import android.content.Intent;
+
+import android.car.cluster.IInstrumentClusterManagerCallback;
+
+/**
+ * API to communicate between {@link CarInstrumentClusterManager} and Car Service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterManagerService {
+    oneway void startClusterActivity(in Intent intent);
+
+    oneway void registerCallback(in IInstrumentClusterManagerCallback callback);
+    oneway void unregisterCallback(in IInstrumentClusterManagerCallback callback);
+}
diff --git a/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java b/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java
deleted file mode 100644
index 409c817..0000000
--- a/car-lib/src/android/car/cluster/renderer/DisplayConfiguration.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016 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.renderer;
-
-import android.annotation.Nullable;
-import android.annotation.SystemApi;
-import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * TODO: need to properly define this class and make it immutable. bug: 32060601
- *
- * @hide
- */
-@SystemApi
-public class DisplayConfiguration implements Parcelable {
-    private final Rect mPrimaryRegion;
-
-    @Nullable
-    private final Rect mSecondaryRegion;
-
-    public static final Parcelable.Creator<DisplayConfiguration> CREATOR =
-            new Parcelable.Creator<DisplayConfiguration>() {
-
-                public DisplayConfiguration createFromParcel(Parcel in) {
-                    return new DisplayConfiguration(in);
-                }
-
-                public DisplayConfiguration[] newArray(int size) {
-                    return new DisplayConfiguration[size];
-                }
-            };
-
-
-    public DisplayConfiguration(Parcel in) {
-        mPrimaryRegion = in.readTypedObject(Rect.CREATOR);
-        mSecondaryRegion = in.readTypedObject(Rect.CREATOR);
-    }
-
-    public DisplayConfiguration(Rect primaryRegion) {
-        this(primaryRegion, null);
-    }
-
-    public DisplayConfiguration(Rect primaryRegion, @Nullable Rect secondaryRegion) {
-        mPrimaryRegion = primaryRegion;
-        mSecondaryRegion = secondaryRegion;
-    }
-
-
-    /** Region that will be fully visible in instrument cluster */
-    public Rect getPrimaryRegion() {
-        return mPrimaryRegion;
-    }
-
-    /**
-     * The region that includes primary region + may include some additional space that might
-     * be partially visible in the instrument cluster. It is useful to fade-out primary
-     * rendering for example or adding background image.
-     */
-    @Nullable
-    public Rect getSecondaryRegion() {
-        return mSecondaryRegion;
-    }
-
-    /** Returns true if secondary region is strongly greater then primary region */
-    public boolean hasSecondaryRegion() {
-        return mSecondaryRegion != null
-                && mSecondaryRegion.width() > mPrimaryRegion.width()
-                && mSecondaryRegion.height() > mPrimaryRegion.height();
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeTypedObject(mPrimaryRegion, 0);
-        dest.writeTypedObject(mSecondaryRegion, 0);
-    }
-}
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
index 9fb4b56..3458975 100644
--- a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
@@ -15,6 +15,7 @@
  */
 package android.car.cluster.renderer;
 
+import android.car.cluster.renderer.IInstrumentClusterCallback;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.view.KeyEvent;
 
@@ -28,8 +29,8 @@
     IInstrumentClusterNavigation getNavigationService();
 
     /** Supplies Instrument Cluster Renderer with current owner of Navigation app context */
-    void setNavigationContextOwner(int uid, int pid);
+    oneway void setNavigationContextOwner(int uid, int pid);
 
     /** Called when key event that was addressed to instrument cluster display has been received. */
-    void onKeyEvent(in KeyEvent keyEvent);
+    oneway void onKeyEvent(in KeyEvent keyEvent);
 }
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl
new file mode 100644
index 0000000..996dc9e
--- /dev/null
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterCallback.aidl
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.car.cluster.renderer;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+
+/**
+ * This interface defines the communication channel between the cluster vendor implementation and
+ * Car Service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterCallback {
+    /**
+     * Notify Car Service how to launch an activity for particular category.
+     *
+     * @param category cluster activity category,
+     *        see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+     * @param activityOptions this bundle will be converted to {@link android.app.ActivityOptions}
+     *        and used when starting an activity. It may contain information such as virtual display
+     *        id or activity stack id where to start cluster activity.
+     *
+     * @hide
+     */
+    void setClusterActivityLaunchOptions(String category, in Bundle activityOptions);
+
+    /**
+     * Activities launched on virtual display will be in onPause state most of the time, so they
+     * can't really know whether they visible on the screen or not. We need to propagate this
+     * information along with unobscured bounds (and possible other info) from instrument cluster
+     * vendor implementation to activity.
+     *
+     * @param category cluster activity category to which this state applies,
+     *        see {@link android.car.cluster.CarInstrumentClusterManager} for details.
+     * @param clusterActivityState is a {@link Bundle} object,
+     *        see {@link android.car.cluster.ClusterActivityState} for how to construct the bundle.
+     * @hide
+     */
+    void setClusterActivityState(String category, in Bundle clusterActivityState);
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 068b1e5..6afe6c4 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -18,8 +18,10 @@
 import android.annotation.CallSuper;
 import android.annotation.MainThread;
 import android.annotation.SystemApi;
+import android.app.ActivityOptions;
 import android.app.Service;
 import android.car.CarLibLog;
+import android.car.CarNotConnectedException;
 import android.car.navigation.CarNavigationInstrumentCluster;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -33,6 +35,8 @@
 import android.util.Pair;
 import android.view.KeyEvent;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -60,6 +64,14 @@
 
     private RendererBinder mRendererBinder;
 
+    /** @hide */
+    public static final String EXTRA_KEY_CALLBACK_SERVICE =
+            "android.car.cluster.IInstrumentClusterCallback";
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private IInstrumentClusterCallback mCallback;
+
     @Override
     @CallSuper
     public IBinder onBind(Intent intent) {
@@ -67,6 +79,15 @@
             Log.d(TAG, "onBind, intent: " + intent);
         }
 
+        if (intent.getExtras().containsKey(EXTRA_KEY_CALLBACK_SERVICE)) {
+            IBinder callbackBinder = intent.getExtras().getBinder(EXTRA_KEY_CALLBACK_SERVICE);
+            synchronized (mLock) {
+                mCallback = IInstrumentClusterCallback.Stub.asInterface(callbackBinder);
+            }
+        } else {
+            Log.w(TAG, "onBind, no callback in extra!");
+        }
+
         if (mRendererBinder == null) {
             mRendererBinder = new RendererBinder(getNavigationRenderer());
         }
@@ -83,6 +104,55 @@
     protected void onKeyEvent(KeyEvent keyEvent) {
     }
 
+    /**
+     *
+     * Sets configuration for activities that should be launched directly in the instrument
+     * cluster.
+     *
+     * @param category category of cluster activity
+     * @param activityOptions contains information of how to start cluster activity (on what display
+     *                        or activity stack.
+     *
+     * @hide
+     */
+    public void setClusterActivityLaunchOptions(String category,
+            ActivityOptions activityOptions) throws CarNotConnectedException {
+        IInstrumentClusterCallback cb;
+        synchronized (mLock) {
+            cb = mCallback;
+        }
+        if (cb == null) throw new CarNotConnectedException();
+        try {
+            cb.setClusterActivityLaunchOptions(category, activityOptions.toBundle());
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
+     *
+     * @param category cluster activity category,
+     *        see {@link android.car.cluster.CarInstrumentClusterManager}
+     * @param state pass information about activity state,
+     *        see {@link android.car.cluster.ClusterActivityState}
+     * @return true if information was sent to Car Service
+     * @throws CarNotConnectedException
+     */
+    public void setClusterActivityState(String category, Bundle state)
+            throws CarNotConnectedException {
+        IInstrumentClusterCallback cb;
+        synchronized (mLock) {
+            cb = mCallback;
+        }
+        if (cb == null) throw new CarNotConnectedException();
+        try {
+            cb.setClusterActivityState(category, state);
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         writer.println("**" + getClass().getSimpleName() + "**");
@@ -90,12 +160,19 @@
         if (mRendererBinder != null) {
             writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
             String owner = "none";
-            if (mRendererBinder.mNavContextOwner != null) {
-                owner = "[uid: " + mRendererBinder.mNavContextOwner.first
-                        + ", pid: " + mRendererBinder.mNavContextOwner.second + "]";
+            synchronized (mLock) {
+                if (mRendererBinder.mNavContextOwner != null) {
+                    owner = "[uid: " + mRendererBinder.mNavContextOwner.first
+                            + ", pid: " + mRendererBinder.mNavContextOwner.second + "]";
+                }
             }
             writer.println("navigation focus owner: " + owner);
         }
+        IInstrumentClusterCallback cb;
+        synchronized (mLock) {
+            cb = mCallback;
+        }
+        writer.println("callback: " + cb);
     }
 
     private class RendererBinder extends IInstrumentCluster.Stub {
@@ -103,8 +180,10 @@
         private final NavigationRenderer mNavigationRenderer;
         private final UiHandler mUiHandler;
 
-        private volatile NavigationBinder mNavigationBinder;
-        private volatile Pair<Integer, Integer> mNavContextOwner;
+        @GuardedBy("mLock")
+        private NavigationBinder mNavigationBinder;
+        @GuardedBy("mLock")
+        private Pair<Integer, Integer> mNavContextOwner;
 
         RendererBinder(NavigationRenderer navigationRenderer) {
             mNavigationRenderer = navigationRenderer;
@@ -113,21 +192,25 @@
 
         @Override
         public IInstrumentClusterNavigation getNavigationService() throws RemoteException {
-            if (mNavigationBinder == null) {
-                mNavigationBinder = new NavigationBinder(mNavigationRenderer);
-                if (mNavContextOwner != null) {
-                    mNavigationBinder.setNavigationContextOwner(
-                            mNavContextOwner.first, mNavContextOwner.second);
+            synchronized (mLock) {
+                if (mNavigationBinder == null) {
+                    mNavigationBinder = new NavigationBinder(mNavigationRenderer);
+                    if (mNavContextOwner != null) {
+                        mNavigationBinder.setNavigationContextOwner(
+                                mNavContextOwner.first, mNavContextOwner.second);
+                    }
                 }
+                return mNavigationBinder;
             }
-            return mNavigationBinder;
         }
 
         @Override
         public void setNavigationContextOwner(int uid, int pid) throws RemoteException {
-            mNavContextOwner = new Pair<>(uid, pid);
-            if (mNavigationBinder != null) {
-                mNavigationBinder.setNavigationContextOwner(uid, pid);
+            synchronized (mLock) {
+                mNavContextOwner = new Pair<>(uid, pid);
+                if (mNavigationBinder != null) {
+                    mNavigationBinder.setNavigationContextOwner(uid, pid);
+                }
             }
         }