Merge "Add FixedActivityService" into qt-qpr1-dev
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
index 7deecc7..4f41796 100644
--- a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
@@ -28,6 +28,8 @@
     /**
      * Returns {@link IInstrumentClusterNavigation} that will be passed to the navigation
      * application.
+     *
+     * TODO(b/141992448) : remove blocking call
      */
     IInstrumentClusterNavigation getNavigationService();
 
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
new file mode 100644
index 0000000..680e241
--- /dev/null
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 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.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Helper binder API for InstrumentClusterRenderingService. This contains binder calls to car
+ * service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterHelper {
+    /**
+     * Start an activity to specified display / user. The activity is considered as
+     * in fixed mode for the display and will be re-launched if the activity crashes, the package
+     * is updated or goes to background for whatever reason.
+     * Only one activity can exist in fixed mode for the target display and calling this multiple
+     * times with different {@code Intent} will lead into making all previous activities into
+     * non-fixed normal state (= will not be re-launched.)
+     *
+     * Do not change binder transaction number.
+     */
+    boolean startFixedActivityModeForDisplayAndUser(in Intent intent,
+            in Bundle activityOptionsBundle, int userId) = 0;
+    /**
+     * The activity lauched on the display is no longer in fixed mode. Re-launching or finishing
+     * should not trigger re-launfhing any more. Note that Activity for non-current user will
+     * be auto-stopped and there is no need to call this for user swiching. Note that this does not
+     * stop the activity but it will not be re-launched any more.
+     *
+     * Do not change binder transaction number.
+     */
+    void stopFixedActivityMode(int displayId) = 1;
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index b692c02..29ae9b8 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
 import android.app.ActivityOptions;
 import android.app.Service;
 import android.car.Car;
@@ -82,6 +83,15 @@
  */
 @SystemApi
 public abstract class InstrumentClusterRenderingService extends Service {
+    /**
+     * Key to pass IInstrumentClusterHelper binder in onBind call {@link Intent} through extra
+     * {@link Bundle). Both extra bundle and binder itself use this key.
+     *
+     * @hide
+     */
+    public static final String EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER =
+            "android.car.cluster.renderer.IInstrumentClusterHelper";
+
     private static final String TAG = CarLibLog.TAG_CLUSTER;
 
     /**
@@ -94,15 +104,20 @@
     private static final String BITMAP_QUERY_WIDTH = "w";
     private static final String BITMAP_QUERY_HEIGHT = "h";
 
+    private final Handler mUiHandler = new Handler(Looper.getMainLooper());
+
     private final Object mLock = new Object();
+    // Main thread only
     private RendererBinder mRendererBinder;
-    private Handler mUiHandler = new Handler(Looper.getMainLooper());
     private ActivityOptions mActivityOptions;
     private ClusterActivityState mActivityState;
     private ComponentName mNavigationComponent;
     @GuardedBy("mLock")
     private ContextOwner mNavContextOwner;
 
+    @GuardedBy("mLock")
+    private IInstrumentClusterHelper mInstrumentClusterHelper;
+
     private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
     private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
             IMAGE_CACHE_SIZE_BYTES) {
@@ -164,6 +179,18 @@
             Log.d(TAG, "onBind, intent: " + intent);
         }
 
+        Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+        IBinder binder = null;
+        if (bundle != null) {
+            binder = bundle.getBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+        }
+        if (binder == null) {
+            Log.wtf(TAG, "IInstrumentClusterHelper not passed through binder");
+        } else {
+            synchronized (mLock) {
+                mInstrumentClusterHelper = IInstrumentClusterHelper.Stub.asInterface(binder);
+            }
+        }
         if (mRendererBinder == null) {
             mRendererBinder = new RendererBinder(getNavigationRenderer());
         }
@@ -203,6 +230,76 @@
     public void onNavigationComponentReleased() {
     }
 
+    @Nullable
+    private IInstrumentClusterHelper getClusterHelper() {
+        synchronized (mLock) {
+            if (mInstrumentClusterHelper == null) {
+                Log.w("mInstrumentClusterHelper still null, should wait until onBind",
+                        new RuntimeException());
+            }
+            return mInstrumentClusterHelper;
+        }
+    }
+
+    /**
+     * Start Activity in fixed mode.
+     *
+     * <p>Activity launched in this way will stay visible across crash, package updatge
+     * or other Activity launch. So this should be carefully used for case like apps running
+     * in instrument cluster.</p>
+     *
+     * <p> Only one Activity can stay in this mode for a display and launching other Activity
+     * with this call means old one get out of the mode. Alternatively
+     * {@link #stopFixedActivityMode(int)} can be called to get the top activitgy out of this
+     * mode.</p>
+     *
+     * @param intent Should include specific {@code ComponentName}.
+     * @param options Should include target display.
+     * @param userId Target user id
+     * @return {@code true} if succeeded. {@code false} may mean the target component is not ready
+     *         or available. Note that failure can happen during early boot-up stage even if the
+     *         target Activity is in normal state and client should retry when it fails. Once it is
+     *         successfully launched, car service will guarantee that it is running across crash or
+     *         other events.
+     *
+     * @hide
+     */
+    protected boolean startFixedActivityModeFoDisplayAndUser(@NonNull Intent intent,
+            @NonNull ActivityOptions options, @UserIdInt int userId) {
+        IInstrumentClusterHelper helper = getClusterHelper();
+        if (helper == null) {
+            return false;
+        }
+        try {
+            return helper.startFixedActivityModeForDisplayAndUser(intent, options.toBundle(),
+                    userId);
+        } catch (RemoteException e) {
+            Log.w("Remote exception from car service", e);
+            // Probably car service will restart and rebind. So do nothing.
+        }
+        return false;
+    }
+
+
+    /**
+     * Stop fixed mode for top Activity in the display. Crashing or launching other Activity
+     * will not re-launch the top Activity any more.
+     *
+     * @hide
+     */
+    protected void stopFixedActivityMode(int displayId) {
+        IInstrumentClusterHelper helper = getClusterHelper();
+        if (helper == null) {
+            return;
+        }
+        try {
+            helper.stopFixedActivityMode(displayId);
+        } catch (RemoteException e) {
+            Log.w("Remote exception from car service, displayId:" + displayId, e);
+            // Probably car service will restart and rebind. So do nothing.
+        }
+    }
+
     /**
      * Updates the cluster navigation activity by checking which activity to show (an activity of
      * the {@link #mNavContextOwner}). If not yet launched, it will do so.
@@ -379,16 +476,19 @@
     @CallSuper
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
-        writer.println("**" + getClass().getSimpleName() + "**");
-        writer.println("renderer binder: " + mRendererBinder);
-        if (mRendererBinder != null) {
-            writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+        synchronized (mLock) {
+            writer.println("**" + getClass().getSimpleName() + "**");
+            writer.println("renderer binder: " + mRendererBinder);
+            if (mRendererBinder != null) {
+                writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+            }
+            writer.println("navigation focus owner: " + getNavigationContextOwner());
+            writer.println("activity options: " + mActivityOptions);
+            writer.println("activity state: " + mActivityState);
+            writer.println("current nav component: " + mNavigationComponent);
+            writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
+            writer.println("mInstrumentClusterHelper" + mInstrumentClusterHelper);
         }
-        writer.println("navigation focus owner: " + getNavigationContextOwner());
-        writer.println("activity options: " + mActivityOptions);
-        writer.println("activity state: " + mActivityState);
-        writer.println("current nav component: " + mNavigationComponent);
-        writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
     }
 
     private class RendererBinder extends IInstrumentCluster.Stub {
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index fe7c4d6..3c6129e 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -37,6 +37,7 @@
 import android.util.Slog;
 import android.util.TimingsTraceLog;
 
+import com.android.car.am.FixedActivityService;
 import com.android.car.audio.CarAudioService;
 import com.android.car.cluster.InstrumentClusterService;
 import com.android.car.garagemode.GarageModeService;
@@ -80,6 +81,7 @@
     private final CarPropertyService mCarPropertyService;
     private final CarNightService mCarNightService;
     private final AppFocusService mAppFocusService;
+    private final FixedActivityService mFixedActivityService;
     private final GarageModeService mGarageModeService;
     private final InstrumentClusterService mInstrumentClusterService;
     private final CarLocationService mCarLocationService;
@@ -149,6 +151,7 @@
         mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
         mCarAudioService = new CarAudioService(serviceContext);
         mCarNightService = new CarNightService(serviceContext, mCarPropertyService);
+        mFixedActivityService = new FixedActivityService(serviceContext);
         mInstrumentClusterService = new InstrumentClusterService(serviceContext,
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
@@ -177,6 +180,7 @@
         CarLocalServices.addService(SystemInterface.class, mSystemInterface);
         CarLocalServices.addService(CarDrivingStateService.class, mCarDrivingStateService);
         CarLocalServices.addService(PerUserCarServiceHelper.class, mPerUserCarServiceHelper);
+        CarLocalServices.addService(FixedActivityService.class, mFixedActivityService);
 
         // Be careful with order. Service depending on other service should be inited later.
         List<CarServiceBase> allServices = new ArrayList<>();
@@ -193,6 +197,7 @@
         allServices.add(mAppFocusService);
         allServices.add(mCarAudioService);
         allServices.add(mCarNightService);
+        allServices.add(mFixedActivityService);
         allServices.add(mInstrumentClusterService);
         allServices.add(mSystemStateControllerService);
         allServices.add(mPerUserCarServiceHelper);
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
new file mode 100644
index 0000000..b1ca176
--- /dev/null
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.am;
+
+import static com.android.car.CarLog.TAG_AM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackInfo;
+import android.app.ActivityOptions;
+import android.app.IActivityManager;
+import android.app.IProcessObserver;
+import android.app.TaskStackListener;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+
+import com.android.car.CarLocalServices;
+import com.android.car.CarServiceBase;
+import com.android.car.user.CarUserService;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Monitors top activity for a display and guarantee activity in fixed mode is re-launched if it has
+ * crashed or gone to background for whatever reason.
+ *
+ * <p>This component also monitors the upddate of the target package and re-launch it once
+ * update is complete.</p>
+ */
+public final class FixedActivityService implements CarServiceBase {
+
+    private static final boolean DBG = false;
+
+    private static class RunningActivityInfo {
+        @NonNull
+        public final Intent intent;
+
+        @NonNull
+        public final ActivityOptions activityOptions;
+
+        @UserIdInt
+        public final int userId;
+
+        // Only used in a method for local book-keeping. So do not need a lock.
+        // This does not represent the current visibility.
+        public boolean isVisible;
+
+        RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
+                @UserIdInt int userId) {
+            this.intent = intent;
+            this.activityOptions = activityOptions;
+            this.userId = userId;
+        }
+
+        @Override
+        public String toString() {
+            return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
+                    + ",userId:" + userId + "}";
+        }
+    }
+
+    private final Context mContext;
+
+    private final IActivityManager mAm;
+
+    private final UserManager mUm;
+
+    private final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
+        @Override
+        public void onUserLockChanged(@UserIdInt int userId, boolean unlocked) {
+            // Nothing to do
+        }
+
+        @Override
+        public void onSwitchUser(@UserIdInt int userId) {
+            synchronized (mLock) {
+                mRunningActivities.clear();
+            }
+        }
+    };
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+                    || Intent.ACTION_PACKAGE_REPLACED.equals(
+                    action)) {
+                launchIfNecessary();
+            }
+        }
+    };
+
+    // It says listener but is actually callback.
+    private final TaskStackListener mTaskStackListener = new TaskStackListener() {
+        @Override
+        public void onTaskStackChanged() {
+            launchIfNecessary();
+        }
+    };
+
+    private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
+        @Override
+        public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
+            launchIfNecessary();
+        }
+
+        @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+          // ignore
+        }
+
+        @Override
+        public void onProcessDied(int pid, int uid) {
+            launchIfNecessary();
+        }
+    };
+
+    private final Object mLock = new Object();
+
+    // key: displayId
+    @GuardedBy("mLock")
+    private final SparseArray<RunningActivityInfo> mRunningActivities =
+            new SparseArray<>(/* capacity= */ 1); // default to one cluster only case
+
+    @GuardedBy("mLock")
+    private boolean mEventMonitoringActive;
+
+    public FixedActivityService(Context context) {
+        mContext = context;
+        mAm = ActivityManager.getService();
+        mUm = context.getSystemService(UserManager.class);
+    }
+
+
+    @Override
+    public void init() {
+        // nothing to do
+    }
+
+    @Override
+    public void release() {
+        stopMonitoringEvents();
+    }
+
+    @Override
+    public void dump(PrintWriter writer) {
+        writer.println("*FixedActivityService*");
+        synchronized (mLock) {
+            writer.println("mRunningActivities:" + mRunningActivities
+                    + " ,mEventMonitoringActive:" + mEventMonitoringActive);
+        }
+    }
+
+    private void startMonitoringEvents() {
+        synchronized (mLock) {
+            if (mEventMonitoringActive) {
+                return;
+            }
+            mEventMonitoringActive = true;
+        }
+        CarUserService userService = CarLocalServices.getService(CarUserService.class);
+        userService.addUserCallback(mUserCallback);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
+                /* broadcastPermission= */ null, /* scheduler= */ null);
+        try {
+            mAm.registerTaskStackListener(mTaskStackListener);
+            mAm.registerProcessObserver(mProcessObserver);
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+    }
+
+    private void stopMonitoringEvents() {
+        synchronized (mLock) {
+            if (!mEventMonitoringActive) {
+                return;
+            }
+            mEventMonitoringActive = false;
+        }
+        CarUserService userService = CarLocalServices.getService(CarUserService.class);
+        userService.removeUserCallback(mUserCallback);
+        try {
+            mAm.unregisterTaskStackListener(mTaskStackListener);
+            mAm.unregisterProcessObserver(mProcessObserver);
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+    }
+
+    @Nullable
+    private List<StackInfo> getStackInfos() {
+        try {
+            return mAm.getAllStackInfos();
+        } catch (RemoteException e) {
+            Log.e(TAG_AM, "remote exception from AM", e);
+        }
+        return null;
+    }
+
+    /**
+     * Launches all stored fixed mode activities if necessary.
+     * @param displayId Display id to check if it is visible. If check is not necessary, should pass
+     *        {@link Display#INVALID_DISPLAY}.
+     * @return true if fixed Activity for given {@code displayId} is visible / successfully
+     *         launched. It will return false for {@link Display#INVALID_DISPLAY} {@code displayId}.
+     */
+    private boolean launchIfNecessary(int displayId) {
+        List<StackInfo> infos = getStackInfos();
+        if (infos == null) {
+            Log.e(TAG_AM, "cannot get StackInfo from AM");
+            return false;
+        }
+        synchronized (mLock) {
+            if (mRunningActivities.size() == 0) {
+                // it must have been stopped.
+                if (DBG) {
+                    Log.i(TAG_AM, "empty activity list", new RuntimeException());
+                }
+                return false;
+            }
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                mRunningActivities.valueAt(i).isVisible = false;
+            }
+            for (StackInfo stackInfo : infos) {
+                RunningActivityInfo activityInfo = mRunningActivities.get(stackInfo.displayId);
+                if (activityInfo == null) {
+                    continue;
+                }
+                int topUserId = stackInfo.taskUserIds[stackInfo.taskUserIds.length - 1];
+                if (activityInfo.intent.getComponent().equals(stackInfo.topActivity)
+                        && activityInfo.userId == topUserId && stackInfo.visible) {
+                    // top one is matching.
+                    activityInfo.isVisible = true;
+                    continue;
+                }
+                if (DBG) {
+                    Log.i(TAG_AM, "Unmatched top activity:" + stackInfo.topActivity
+                            + " user:" + topUserId + " display:" + stackInfo.displayId);
+                }
+            }
+            for (int i = 0; i < mRunningActivities.size(); i++) {
+                RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
+                if (activityInfo.isVisible) {
+                    continue;
+                }
+                if (!isComponentAvailable(activityInfo.intent.getComponent(),
+                        activityInfo.userId) || !isUserAllowedToLaunchActivity(
+                        activityInfo.userId)) {
+                    continue;
+                }
+                Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
+                        + ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
+                        + mRunningActivities.keyAt(i));
+                try {
+                    mContext.startActivityAsUser(activityInfo.intent,
+                            activityInfo.activityOptions.toBundle(),
+                            UserHandle.of(activityInfo.userId));
+                    activityInfo.isVisible = true;
+                } catch (Exception e) { // Catch all for any app related issues.
+                    Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
+                }
+            }
+            RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+            if (activityInfo == null) {
+                return false;
+            }
+            return activityInfo.isVisible;
+        }
+    }
+
+    private void launchIfNecessary() {
+        launchIfNecessary(Display.INVALID_DISPLAY);
+    }
+
+    private void logComponentNotFound(ComponentName component, @UserIdInt  int userId,
+            Exception e) {
+        Log.e(TAG_AM, "Specified Component not found:" + component
+                + " for userid:" + userId, e);
+    }
+
+    private boolean isComponentAvailable(ComponentName component, @UserIdInt int userId) {
+        PackageInfo packageInfo;
+        try {
+            packageInfo = mContext.getPackageManager().getPackageInfoAsUser(
+                    component.getPackageName(), PackageManager.GET_ACTIVITIES, userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            logComponentNotFound(component, userId, e);
+            return false;
+        }
+        if (packageInfo == null || packageInfo.activities == null) {
+            // may not be necessary but additional safety check
+            logComponentNotFound(component, userId, new RuntimeException());
+            return false;
+        }
+        String fullName = component.getClassName();
+        String shortName = component.getShortClassName();
+        for (ActivityInfo info : packageInfo.activities) {
+            if (info.name.equals(fullName) || info.name.equals(shortName)) {
+                return true;
+            }
+        }
+        logComponentNotFound(component, userId, new RuntimeException());
+        return false;
+    }
+
+    private boolean isUserAllowedToLaunchActivity(@UserIdInt int userId) {
+        int currentUser = ActivityManager.getCurrentUser();
+        if (userId == currentUser) {
+            return true;
+        }
+        int[] profileIds = mUm.getEnabledProfileIds(currentUser);
+        for (int id : profileIds) {
+            if (id == userId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isDisplayAllowedForFixedMode(int displayId) {
+        if (displayId == Display.DEFAULT_DISPLAY || displayId == Display.INVALID_DISPLAY) {
+            Log.w(TAG_AM, "Target display cannot be used for fixed mode, displayId:" + displayId,
+                    new RuntimeException());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Checks {@link InstrumentClusterRenderingService#startFixedActivityModeFoDisplayAndUser(
+     * Intent, ActivityOptions, int)}
+     */
+    public boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
+            @NonNull ActivityOptions options, int displayId, @UserIdInt int userId) {
+        if (!isDisplayAllowedForFixedMode(displayId)) {
+            return false;
+        }
+        if (!isUserAllowedToLaunchActivity(userId)) {
+            Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
+                    + " cannot launch activity, Intent:" + intent);
+            return false;
+        }
+        ComponentName component = intent.getComponent();
+        if (component == null) {
+            Log.e(TAG_AM,
+                    "startFixedActivityModeForDisplayAndUser: No component specified for "
+                            + "requested Intent"
+                            + intent);
+            return false;
+        }
+        if (!isComponentAvailable(component, userId)) {
+            return false;
+        }
+        boolean startMonitoringEvents = false;
+        synchronized (mLock) {
+            if (mRunningActivities.size() == 0) {
+                startMonitoringEvents = true;
+            }
+            RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+            if (activityInfo == null) {
+                activityInfo = new RunningActivityInfo(intent, options, userId);
+                mRunningActivities.put(displayId, activityInfo);
+            }
+        }
+        boolean launched = launchIfNecessary(displayId);
+        if (!launched) {
+            synchronized (mLock) {
+                mRunningActivities.remove(displayId);
+            }
+        }
+        // If first trial fails, let client know and do not retry as it can be wrong setting.
+        if (startMonitoringEvents && launched) {
+            startMonitoringEvents();
+        }
+        return launched;
+    }
+
+    /** Check {@link InstrumentClusterRenderingService#stopFixedActivityMode(int)} */
+    public void stopFixedActivityMode(int displayId) {
+        if (!isDisplayAllowedForFixedMode(displayId)) {
+            return;
+        }
+        boolean stopMonitoringEvents = false;
+        synchronized (mLock) {
+            mRunningActivities.remove(displayId);
+            if (mRunningActivities.size() == 0) {
+                stopMonitoringEvents = true;
+            }
+        }
+        if (stopMonitoringEvents) {
+            stopMonitoringEvents();
+        }
+    }
+}
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index fd16da5..df6c476 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -15,17 +15,22 @@
  */
 package com.android.car.cluster;
 
+import static android.car.cluster.renderer.InstrumentClusterRenderingService.EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER;
+
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.ActivityOptions;
 import android.car.CarAppFocusManager;
 import android.car.cluster.IInstrumentClusterManagerCallback;
 import android.car.cluster.IInstrumentClusterManagerService;
 import android.car.cluster.renderer.IInstrumentCluster;
+import android.car.cluster.renderer.IInstrumentClusterHelper;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -43,6 +48,7 @@
 import com.android.car.CarLog;
 import com.android.car.CarServiceBase;
 import com.android.car.R;
+import com.android.car.am.FixedActivityService;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 
@@ -69,16 +75,18 @@
      */
     @Deprecated
     private final ClusterManagerService mClusterManagerService = new ClusterManagerService();
-    private final Object mSync = new Object();
-    @GuardedBy("mSync")
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
     private ContextOwner mNavContextOwner = NO_OWNER;
-    @GuardedBy("mSync")
+    @GuardedBy("mLock")
     private IInstrumentCluster mRendererService;
     // If renderer service crashed / stopped and this class fails to rebind with it immediately,
     // we should wait some time before next attempt. This may happen during APK update for example.
+    @GuardedBy("mLock")
     private DeferredRebinder mDeferredRebinder;
     // Whether {@link android.car.cluster.renderer.InstrumentClusterRendererService} is bound
     // (although not necessarily connected)
+    @GuardedBy("mLock")
     private boolean mRendererBound = false;
 
     /**
@@ -92,7 +100,7 @@
             }
             IInstrumentCluster service = IInstrumentCluster.Stub.asInterface(binder);
             ContextOwner navContextOwner;
-            synchronized (mSync) {
+            synchronized (mLock) {
                 mRendererService = service;
                 navContextOwner = mNavContextOwner;
             }
@@ -107,19 +115,39 @@
                 Log.d(TAG, "onServiceDisconnected, name: " + name);
             }
             mContext.unbindService(this);
-            mRendererBound = false;
-
-            synchronized (mSync) {
+            DeferredRebinder rebinder;
+            synchronized (mLock) {
+                mRendererBound = false;
                 mRendererService = null;
+                if (mDeferredRebinder == null) {
+                    mDeferredRebinder = new DeferredRebinder();
+                }
+                rebinder = mDeferredRebinder;
             }
-
-            if (mDeferredRebinder == null) {
-                mDeferredRebinder = new DeferredRebinder();
-            }
-            mDeferredRebinder.rebind();
+            rebinder.rebind();
         }
     };
 
+    private final IInstrumentClusterHelper mInstrumentClusterHelper =
+            new IInstrumentClusterHelper.Stub() {
+                @Override
+                public boolean startFixedActivityModeForDisplayAndUser(Intent intent,
+                        Bundle activityOptionsBundle, int userId) {
+                    ActivityOptions options = new ActivityOptions(activityOptionsBundle);
+                    FixedActivityService service = CarLocalServices.getService(
+                            FixedActivityService.class);
+                    return service.startFixedActivityModeForDisplayAndUser(intent, options,
+                            options.getLaunchDisplayId(), userId);
+                }
+
+                @Override
+                public void stopFixedActivityMode(int displayId) {
+                    FixedActivityService service = CarLocalServices.getService(
+                            FixedActivityService.class);
+                    service.stopFixedActivityMode(displayId);
+                }
+            };
+
     public InstrumentClusterService(Context context, AppFocusService appFocusService,
             CarInputService carInputService) {
         mContext = context;
@@ -181,7 +209,7 @@
         IInstrumentCluster service;
         ContextOwner requester = new ContextOwner(uid, pid);
         ContextOwner newOwner = acquire ? requester : NO_OWNER;
-        synchronized (mSync) {
+        synchronized (mLock) {
             if ((acquire && Objects.equals(mNavContextOwner, requester))
                     || (!acquire && !Objects.equals(mNavContextOwner, requester))) {
                 // Nothing to do here. Either the same owner is acquiring twice, or someone is
@@ -221,6 +249,11 @@
 
         Intent intent = new Intent();
         intent.setComponent(ComponentName.unflattenFromString(rendererService));
+        // Litle bit inefficiency here as Intent.getIBinderExtra() is a hidden API.
+        Bundle bundle = new Bundle();
+        bundle.putBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER,
+                mInstrumentClusterHelper.asBinder());
+        intent.putExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER, bundle);
         return mContext.bindServiceAsUser(intent, mRendererServiceConnection,
                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM);
     }
@@ -262,7 +295,7 @@
 
     private IInstrumentCluster getInstrumentClusterRendererService() {
         IInstrumentCluster service;
-        synchronized (mSync) {
+        synchronized (mLock) {
             service = mRendererService;
         }
         return service;