Merge "Format sensor values and imperial unit conversions"
diff --git a/car-lib/Android.bp b/car-lib/Android.bp
index 1d887fa..4e03d8c 100644
--- a/car-lib/Android.bp
+++ b/car-lib/Android.bp
@@ -109,17 +109,6 @@
     ],
 }
 
-genrule {
-    name: "android-car-last-released-test-api",
-    srcs: [
-        "api/test-released/*.txt",
-    ],
-    cmd: "cp -f $$(echo $(in) | tr \" \" \"\\n\" | sort -n | tail -1) $(genDir)/last-released-test-api.txt",
-    out: [
-        "last-released-test-api.txt",
-    ],
-}
-
 droidstubs {
     name: "android.car-stubs-docs",
     defaults: ["android.car-docs-default"],
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index f520acc..c59a5e3 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -144,6 +144,7 @@
     field public static final int EV_CHARGE_PORT_OPEN = 287310602; // 0x1120030a
     field public static final int FOG_LIGHTS_STATE = 289410562; // 0x11400e02
     field public static final int FOG_LIGHTS_SWITCH = 289410578; // 0x11400e12
+    field public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364; // 0x11200604
     field public static final int FUEL_DOOR_OPEN = 287310600; // 0x11200308
     field public static final int FUEL_LEVEL = 291504903; // 0x11600307
     field public static final int FUEL_LEVEL_LOW = 287310853; // 0x11200405
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index ff4f8d1..4462f6c 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -17,6 +17,7 @@
     field public static final String PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL = "android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
     field public static final String PERMISSION_CAR_POWER = "android.car.permission.CAR_POWER";
     field public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
+    field public static final String PERMISSION_CAR_PROJECTION_STATUS = "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
     field public static final String PERMISSION_CAR_TEST_SERVICE = "android.car.permission.CAR_TEST_SERVICE";
     field public static final String PERMISSION_CONTROL_APP_BLOCKING = "android.car.permission.CONTROL_APP_BLOCKING";
     field public static final String PERMISSION_CONTROL_CAR_CLIMATE = "android.car.permission.CONTROL_CAR_CLIMATE";
@@ -43,15 +44,19 @@
   }
 
   public final class CarProjectionManager {
-    method public void onCarDisconnected();
-    method public void registerProjectionListener(android.car.CarProjectionManager.CarProjectionListener, int);
-    method public void registerProjectionRunner(android.content.Intent);
-    method public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
-    method public boolean requestBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
-    method public void startProjectionAccessPoint(android.car.CarProjectionManager.ProjectionAccessPointCallback);
-    method public void stopProjectionAccessPoint();
-    method public void unregisterProjectionListener();
-    method public void unregisterProjectionRunner(android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public java.util.List<java.lang.Integer> getAvailableWifiChannels(int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public android.os.Bundle getProjectionOptions();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionListener(@NonNull android.car.CarProjectionManager.CarProjectionListener, int);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionRunner(@NonNull android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void registerProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean requestBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int, @NonNull android.os.IBinder);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void startProjectionAccessPoint(@NonNull android.car.CarProjectionManager.ProjectionAccessPointCallback);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void stopProjectionAccessPoint();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionListener();
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionRunner(@NonNull android.content.Intent);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void unregisterProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+    method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void updateProjectionStatus(@NonNull android.car.projection.ProjectionStatus);
     field public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 2; // 0x2
     field public static final int PROJECTION_VOICE_SEARCH = 1; // 0x1
   }
@@ -71,6 +76,10 @@
     field public static final int ERROR_TETHERING_DISALLOWED = 4; // 0x4
   }
 
+  public static interface CarProjectionManager.ProjectionStatusListener {
+    method public void onProjectionStatusChanged(int, @Nullable String, @NonNull java.util.List<android.car.projection.ProjectionStatus>);
+  }
+
   public final class VehicleAreaDoor {
     field public static final int DOOR_HOOD = 268435456; // 0x10000000
     field public static final int DOOR_REAR = 536870912; // 0x20000000
@@ -791,6 +800,66 @@
 
 }
 
+package android.car.projection {
+
+  public class ProjectionOptions {
+    ctor public ProjectionOptions(android.os.Bundle);
+    method @Nullable public android.app.ActivityOptions getActivityOptions();
+    method @Nullable public android.content.ComponentName getConsentActivity();
+    method public int getUiMode();
+    method @NonNull public android.os.Bundle toBundle();
+    field public static final int UI_MODE_BLENDED = 1; // 0x1
+    field public static final int UI_MODE_FULL_SCREEN = 0; // 0x0
+  }
+
+  public final class ProjectionStatus implements android.os.Parcelable {
+    method @NonNull public static android.car.projection.ProjectionStatus.Builder builder(String, int);
+    method public int describeContents();
+    method @NonNull public java.util.List<android.car.projection.ProjectionStatus.MobileDevice> getConnectedMobileDevices();
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getPackageName();
+    method public int getState();
+    method public int getTransport();
+    method public boolean isActive();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus> CREATOR;
+    field public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3; // 0x3
+    field public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2; // 0x2
+    field public static final int PROJECTION_STATE_INACTIVE = 0; // 0x0
+    field public static final int PROJECTION_STATE_READY_TO_PROJECT = 1; // 0x1
+    field public static final int PROJECTION_TRANSPORT_NONE = 0; // 0x0
+    field public static final int PROJECTION_TRANSPORT_USB = 1; // 0x1
+    field public static final int PROJECTION_TRANSPORT_WIFI = 2; // 0x2
+  }
+
+  public static final class ProjectionStatus.Builder {
+    method @NonNull public android.car.projection.ProjectionStatus.Builder addMobileDevice(android.car.projection.ProjectionStatus.MobileDevice);
+    method public android.car.projection.ProjectionStatus build();
+    method @NonNull public android.car.projection.ProjectionStatus.Builder setExtras(android.os.Bundle);
+    method @NonNull public android.car.projection.ProjectionStatus.Builder setProjectionTransport(int);
+  }
+
+  public static final class ProjectionStatus.MobileDevice implements android.os.Parcelable {
+    method @NonNull public static android.car.projection.ProjectionStatus.MobileDevice.Builder builder(int, String);
+    method public int describeContents();
+    method @NonNull public java.util.List<java.lang.Integer> getAvailableTransports();
+    method @NonNull public android.os.Bundle getExtras();
+    method public int getId();
+    method @NonNull public String getName();
+    method public boolean isProjecting();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus.MobileDevice> CREATOR;
+  }
+
+  public static final class ProjectionStatus.MobileDevice.Builder {
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder addTransport(int);
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice build();
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setExtras(android.os.Bundle);
+    method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setProjecting(boolean);
+  }
+
+}
+
 package android.car.storagemonitoring {
 
   public final class CarStorageMonitoringManager {
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index ab6aa9f..0ab23b7 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -393,6 +393,14 @@
     public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
 
     /**
+     * Permission necessary to access projection status.
+     * @hide
+     */
+    @SystemApi
+    public static final String PERMISSION_CAR_PROJECTION_STATUS =
+            "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
+
+    /**
      * Permission necessary to mock vehicle hal for testing.
      * @hide
      * @deprecated mocking vehicle HAL in car service is no longer supported.
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 50606de..7706bf7 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -16,11 +16,18 @@
 
 package android.car;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
 import android.content.Intent;
 import android.net.wifi.WifiConfiguration;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -29,7 +36,14 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.util.Preconditions;
+
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * CarProjectionManager allows applications implementing projection to register/unregister itself
@@ -44,6 +58,9 @@
 public final class CarProjectionManager implements CarManagerBase {
     private static final String TAG = CarProjectionManager.class.getSimpleName();
 
+    private final Binder mToken = new Binder();
+    private final Object mLock = new Object();
+
     /**
      * Listener to get projected notifications.
      *
@@ -81,10 +98,30 @@
 
     private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy;
 
+    private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>();
+    private CarProjectionStatusListenerImpl mCarProjectionStatusListener;
+
     // Only one access point proxy object per process.
     private static final IBinder mAccessPointProxyToken = new Binder();
 
     /**
+     * Interface to receive for projection status updates.
+     */
+    public interface ProjectionStatusListener {
+        /**
+         * This method gets invoked if projection status has been changed.
+         *
+         * @param state - current projection state
+         * @param packageName - if projection is currently running either in the foreground or
+         *                      in the background this argument will contain its package name
+         * @param details - contains detailed information about all currently registered projection
+         *                  receivers.
+         */
+        void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName,
+                @NonNull List<ProjectionStatus> details);
+    }
+
+    /**
      * @hide
      */
     public CarProjectionManager(IBinder service, Handler handler) {
@@ -107,11 +144,11 @@
      * @param listener
      * @param voiceSearchFilter Flags of voice search requests to get notification.
      */
-    public void registerProjectionListener(CarProjectionListener listener, int voiceSearchFilter) {
-        if (listener == null) {
-            throw new IllegalArgumentException("null listener");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void registerProjectionListener(@NonNull CarProjectionListener listener,
+            int voiceSearchFilter) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
             if (mListener == null || mVoiceSearchFilter != voiceSearchFilter) {
                 try {
                     mService.registerProjectionListener(mBinderListener, voiceSearchFilter);
@@ -135,8 +172,9 @@
     /**
      * Unregister listener and stop listening projection events.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public void unregisterProjectionListener() {
-        synchronized (this) {
+        synchronized (mLock) {
             try {
                 mService.unregisterProjectionListener(mBinderListener);
             } catch (RemoteException e) {
@@ -152,11 +190,10 @@
      * to create reverse binding.
      * @param serviceIntent
      */
-    public void registerProjectionRunner(Intent serviceIntent) {
-        if (serviceIntent == null) {
-            throw new IllegalArgumentException("null serviceIntent");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void registerProjectionRunner(@NonNull Intent serviceIntent) {
+        Preconditions.checkNotNull("serviceIntent cannot be null");
+        synchronized (mLock) {
             try {
                 mService.registerProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
@@ -170,11 +207,10 @@
      * reverse binding.
      * @param serviceIntent
      */
-    public void unregisterProjectionRunner(Intent serviceIntent) {
-        if (serviceIntent == null) {
-            throw new IllegalArgumentException("null serviceIntent");
-        }
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void unregisterProjectionRunner(@NonNull Intent serviceIntent) {
+        Preconditions.checkNotNull("serviceIntent cannot be null");
+        synchronized (mLock) {
             try {
                 mService.unregisterProjectionRunner(serviceIntent);
             } catch (RemoteException e) {
@@ -183,6 +219,7 @@
         }
     }
 
+    /** @hide */
     @Override
     public void onCarDisconnected() {
         // nothing to do
@@ -194,9 +231,13 @@
      *
      * <p>A process can have only one request to start an access point, subsequent call of this
      * method will invalidate previous calls.
+     *
+     * @param callback to receive notifications when access point status changed for the request
      */
-    public void startProjectionAccessPoint(ProjectionAccessPointCallback callback) {
-        synchronized (this) {
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
+        Preconditions.checkNotNull(callback, "callback cannot be null");
+        synchronized (mLock) {
             Looper looper = mHandler.getLooper();
             ProjectionAccessPointCallbackProxy proxy =
                     new ProjectionAccessPointCallbackProxy(this, looper, callback);
@@ -210,11 +251,32 @@
     }
 
     /**
+     * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz,
+     * e.g. channel 1 will be represented as 2412 in the list.
+     *
+     * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*}
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public @NonNull List<Integer> getAvailableWifiChannels(int band) {
+        try {
+            int[] channels = mService.getAvailableWifiChannels(band);
+            List<Integer> channelList = new ArrayList<>(channels.length);
+            for (int v : channels) {
+                channelList.add(v);
+            }
+            return channelList;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Stop Wi-Fi Access Point for wireless projection receiver app.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public void stopProjectionAccessPoint() {
         ProjectionAccessPointCallbackProxy proxy;
-        synchronized (this) {
+        synchronized (mLock) {
             proxy = mProjectionAccessPointCallbackProxy;
             mProjectionAccessPointCallbackProxy = null;
         }
@@ -239,8 +301,11 @@
      *                owning the token dies, the request will automatically be released.
      * @return True if the profile was successfully inhibited, false if an error occurred.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public boolean requestBluetoothProfileInhibit(
-            BluetoothDevice device, int profile, IBinder token) {
+            @NonNull BluetoothDevice device, int profile, @NonNull IBinder token) {
+        Preconditions.checkNotNull(device, "device cannot be null");
+        Preconditions.checkNotNull(token, "token cannot be null");
         try {
             return mService.requestBluetoothProfileInhibit(device, profile, token);
         } catch (RemoteException e) {
@@ -258,8 +323,11 @@
      *                {@link #requestBluetoothProfileInhibit}.
      * @return True if the request was released, false if an error occurred.
      */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
     public boolean releaseBluetoothProfileInhibit(
             BluetoothDevice device, int profile, IBinder token) {
+        Preconditions.checkNotNull(device, "device cannot be null");
+        Preconditions.checkNotNull(token, "token cannot be null");
         try {
             return mService.releaseBluetoothProfileInhibit(device, profile, token);
         } catch (RemoteException e) {
@@ -268,6 +336,108 @@
     }
 
     /**
+     * Call this method to report projection status of your app. The aggregated status (from other
+     * projection apps if available) will be broadcasted to interested parties.
+     *
+     * @param status the reported status that will be distributed to the interested listeners
+     *
+     * @see #registerProjectionListener(CarProjectionListener, int)
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public void updateProjectionStatus(@NonNull ProjectionStatus status) {
+        Preconditions.checkNotNull(status, "status cannot be null");
+        try {
+            mService.updateProjectionStatus(status, mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register projection status listener. See {@link ProjectionStatusListener} for details. It is
+     * allowed to register multiple listeners.
+     *
+     * <p>Note: provided listener will be called immediately with the most recent status.
+     *
+     * @param listener the listener to receive notification for any projection status changes
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+    public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
+            mProjectionStatusListeners.add(listener);
+
+            if (mCarProjectionStatusListener == null) {
+                mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this);
+                try {
+                    mService.registerProjectionStatusListener(mCarProjectionStatusListener);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            } else {
+                // Already subscribed to Car Service, immediately notify listener with the current
+                // projection status in the event handler thread.
+                mHandler.post(() ->
+                        listener.onProjectionStatusChanged(
+                                mCarProjectionStatusListener.mCurrentState,
+                                mCarProjectionStatusListener.mCurrentPackageName,
+                                mCarProjectionStatusListener.mDetails));
+            }
+        }
+    }
+
+    /**
+     * Unregister provided listener from projection status notifications
+     *
+     * @param listener the listener for projection status notifications that was previously
+     * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)}
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+    public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mLock) {
+            if (!mProjectionStatusListeners.remove(listener)
+                    || !mProjectionStatusListeners.isEmpty()) {
+                return;
+            }
+            unregisterProjectionStatusListenerFromCarServiceLocked();
+        }
+    }
+
+    private void unregisterProjectionStatusListenerFromCarServiceLocked() {
+        try {
+            mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
+            mCarProjectionStatusListener = null;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void handleProjectionStatusChanged(@ProjectionState int state,
+            String packageName, List<ProjectionStatus> details) {
+        List<ProjectionStatusListener> listeners;
+        synchronized (mLock) {
+            listeners = new ArrayList<>(mProjectionStatusListeners);
+        }
+        for (ProjectionStatusListener listener : listeners) {
+            listener.onProjectionStatusChanged(state, packageName, details);
+        }
+    }
+
+    /**
+     * Returns {@link Bundle} object that contains customization for projection app. This bundle
+     * can be parsed using {@link ProjectionOptions}.
+     */
+    @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+    public @NonNull Bundle getProjectionOptions() {
+        try {
+            return mService.getProjectionOptions();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Callback class for applications to receive updates about the LocalOnlyHotspot status.
      */
     public abstract static class ProjectionAccessPointCallback {
@@ -345,7 +515,7 @@
 
     private void handleVoiceAssistantRequest(boolean fromLongPress) {
         CarProjectionListener listener;
-        synchronized (this) {
+        synchronized (mLock) {
             if (mListener == null) {
                 return;
             }
@@ -371,4 +541,34 @@
             manager.mHandler.post(() -> manager.handleVoiceAssistantRequest(fromLongPress));
         }
     }
+
+    private static class CarProjectionStatusListenerImpl
+            extends ICarProjectionStatusListener.Stub {
+
+        private @ProjectionState int mCurrentState;
+        private @Nullable String mCurrentPackageName;
+        private List<ProjectionStatus> mDetails = new ArrayList<>(0);
+
+        private final WeakReference<CarProjectionManager> mManagerRef;
+
+        private CarProjectionStatusListenerImpl(CarProjectionManager mgr) {
+            mManagerRef = new WeakReference<>(mgr);
+        }
+
+        @Override
+        public void onProjectionStatusChanged(int projectionState,
+                String packageName,
+                List<ProjectionStatus> details) {
+            CarProjectionManager mgr = mManagerRef.get();
+            if (mgr != null) {
+                mgr.mHandler.post(() -> {
+                    mCurrentState = projectionState;
+                    mCurrentPackageName = packageName;
+                    mDetails = Collections.unmodifiableList(details);
+
+                    mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails);
+                });
+            }
+        }
+    }
 }
diff --git a/car-lib/src/android/car/ICarProjection.aidl b/car-lib/src/android/car/ICarProjection.aidl
index efaac0d..6e85c45 100644
--- a/car-lib/src/android/car/ICarProjection.aidl
+++ b/car-lib/src/android/car/ICarProjection.aidl
@@ -17,8 +17,11 @@
 package android.car;
 
 import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionStatus;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.Messenger;
 
 /**
@@ -69,4 +72,22 @@
     /** Undo the effects of requestBluetoothProfileInhibit. */
     boolean releaseBluetoothProfileInhibit(
             in BluetoothDevice device, in int profile, in IBinder token) = 7;
+
+    /** Reports projection status for a given projection receiver app. */
+    void updateProjectionStatus(in ProjectionStatus status, in IBinder token) = 8;
+
+    /** Registers projection status listener */
+    void registerProjectionStatusListener(in ICarProjectionStatusListener listener) = 9;
+
+    /** Unregister projection status listener */
+    void unregisterProjectionStatusListener(in ICarProjectionStatusListener listener) = 10;
+
+    /**
+     * Returns options for projection receiver app that can be un-packed using
+     * {@link android.car.projection.ProjectionOptions} class.
+     */
+    Bundle getProjectionOptions() = 11;
+
+    /** Returns a list of available Wi-Fi channels */
+    int[] getAvailableWifiChannels(int band) = 12;
 }
diff --git a/car-lib/src/android/car/ICarProjectionStatusListener.aidl b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
new file mode 100644
index 0000000..cac48a0
--- /dev/null
+++ b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+import android.car.projection.ProjectionStatus;
+
+/**
+ * Listener interface to notify interested parties of projection status change.
+ *
+ * @hide
+ */
+oneway interface ICarProjectionStatusListener {
+    void onProjectionStatusChanged(int projectionState,
+             in String activeProjectionPackageName,
+             in List<ProjectionStatus> details) = 0;
+}
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index ce10d2a..cfc6e34 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -295,6 +295,10 @@
      */
     public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
     /**
+     * Fuel consumption units for display
+     */
+    public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
+    /**
      * Outside temperature
      */
     public static final int ENV_OUTSIDE_TEMPERATURE = 291505923;
@@ -776,6 +780,9 @@
         if (o == EV_BATTERY_DISPLAY_UNITS) {
             return "EV_BATTERY_DISPLAY_UNITS";
         }
+        if (o == FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME) {
+            return "FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME";
+        }
         if (o == ENV_OUTSIDE_TEMPERATURE) {
             return "ENV_OUTSIDE_TEMPERATURE";
         }
diff --git a/car-lib/src/android/car/VehicleUnit.java b/car-lib/src/android/car/VehicleUnit.java
index c9859dc..463d53b 100644
--- a/car-lib/src/android/car/VehicleUnit.java
+++ b/car-lib/src/android/car/VehicleUnit.java
@@ -43,7 +43,8 @@
     public static final int KELVIN = 0x32;
     public static final int MILLILITER = 0x40;
     public static final int LITER = 0x41;
-    public static final int GALLON  = 0x42;
+    public static final int US_GALLON  = 0x42;
+    public static final int IMPERIAL_GALLON = 0x43;
     public static final int NANO_SECS = 0x50;
     public static final int SECS = 0x53;
     public static final int YEAR = 0x59;
@@ -74,7 +75,8 @@
             KELVIN,
             MILLILITER,
             LITER,
-            GALLON,
+            US_GALLON,
+            IMPERIAL_GALLON,
             NANO_SECS,
             SECS,
             YEAR,
diff --git a/car-lib/src/android/car/projection/ProjectionOptions.java b/car-lib/src/android/car/projection/ProjectionOptions.java
new file mode 100644
index 0000000..26b8140
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionOptions.java
@@ -0,0 +1,157 @@
+/*
+ * 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.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class holds OEM customization for projection receiver app.  It is created by Car Service.
+ *
+ * @hide
+ */
+@SystemApi
+public class ProjectionOptions {
+    private static final String KEY_PREFIX = "android.car.projection.";
+
+    /** Immersive full screen mode (all system bars are hidden) */
+    public static final int UI_MODE_FULL_SCREEN = 0;
+
+    /** Show status and navigation bars. */
+    public static final int UI_MODE_BLENDED = 1;
+
+    private static final int UI_MODE_DEFAULT = UI_MODE_FULL_SCREEN;
+
+    /** @hide */
+    @IntDef({UI_MODE_FULL_SCREEN, UI_MODE_BLENDED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionUiMode {}
+
+    private static final String KEY_ACTIVITY_OPTIONS = KEY_PREFIX + "activityOptions";
+    private static final String KEY_UI_MODE = KEY_PREFIX + "systemUiFlags";
+    private static final String KEY_CONSENT_ACTIVITY = KEY_PREFIX + "consentActivity";
+
+    private final ActivityOptions mActivityOptions;
+    private final int mUiMode;
+    private final ComponentName mConsentActivity;
+
+    /**
+     * Creates new instance for given {@code Bundle}
+     *
+     * @param bundle contains OEM specific information
+     */
+    public ProjectionOptions(Bundle bundle) {
+        Bundle activityOptionsBundle = bundle.getBundle(KEY_ACTIVITY_OPTIONS);
+        mActivityOptions = activityOptionsBundle != null
+                ? new ActivityOptions(activityOptionsBundle) : null;
+        mUiMode = bundle.getInt(KEY_UI_MODE, UI_MODE_DEFAULT);
+        mConsentActivity = bundle.getParcelable(KEY_CONSENT_ACTIVITY);
+    }
+
+    private ProjectionOptions(Builder builder) {
+        mActivityOptions = builder.mActivityOptions;
+        mUiMode = builder.mUiMode;
+        mConsentActivity = builder.mConsentActivity;
+    }
+
+    /**
+     * Returns combination of flags from View.SYSTEM_UI_FLAG_* which will be used by projection
+     * receiver app during rendering.
+     */
+    public @ProjectionUiMode int getUiMode() {
+        return mUiMode;
+    }
+
+    /**
+     * Returns {@link ActivityOptions} that needs to be applied when launching projection activity
+     */
+    public @Nullable ActivityOptions getActivityOptions() {
+        return mActivityOptions;
+    }
+
+    /**
+     * Returns package/activity name of the consent activity provided by OEM which needs to be shown
+     * for all mobile devices unless user accepted the consent.
+     *
+     * <p>If the method returns null then consent dialog should not be shown.
+     */
+    public @Nullable ComponentName getConsentActivity() {
+        return mConsentActivity;
+    }
+
+    /** Converts current object to {@link Bundle} */
+    public @NonNull Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        if (mActivityOptions != null) {
+            bundle.putBundle(KEY_ACTIVITY_OPTIONS, mActivityOptions.toBundle());
+        }
+        bundle.putParcelable(KEY_CONSENT_ACTIVITY, mConsentActivity);
+        if (mUiMode != UI_MODE_DEFAULT) {
+            bundle.putInt(KEY_UI_MODE, mUiMode);
+        }
+        return bundle;
+    }
+
+    /** @hide */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** @hide */
+    public static class Builder {
+        private ActivityOptions mActivityOptions;
+        private int mUiMode = UI_MODE_DEFAULT;
+        private ComponentName mConsentActivity;
+
+        /** Sets {@link ActivityOptions} to launch projection activity. */
+        public Builder setProjectionActivityOptions(ActivityOptions activityOptions) {
+            mActivityOptions = activityOptions;
+            return this;
+        }
+
+        /** Set UI for projection activity. It can be one of {@code UI_MODE_*} constants. */
+        public Builder setUiMode(@ProjectionUiMode int uiMode) {
+            mUiMode = uiMode;
+            return this;
+        }
+
+        /** Sets consent activity which will be shown before starting projection. */
+        public Builder setConsentActivity(ComponentName consentActivity) {
+            mConsentActivity = consentActivity;
+            return this;
+        }
+
+        /** Creates an instance of {@link android.car.projection.ProjectionOptions} */
+        public ProjectionOptions build() {
+            return new ProjectionOptions(this);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        return toBundle().toString();
+    }
+}
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.aidl b/car-lib/src/android/car/projection/ProjectionStatus.aidl
new file mode 100644
index 0000000..f2ad77a
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.projection;
+
+parcelable ProjectionStatus;
\ No newline at end of file
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.java b/car-lib/src/android/car/projection/ProjectionStatus.java
new file mode 100644
index 0000000..37d0aee
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.java
@@ -0,0 +1,424 @@
+/*
+ * 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.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.IntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class encapsulates information about projection status and connected mobile devices.
+ *
+ * <p>Since the number of connected devices expected to be small we include information about
+ * connected devices in every status update.
+ *
+ * @hide
+ */
+@SystemApi
+public final class ProjectionStatus implements Parcelable {
+    /** This state indicates that projection is not actively running and no compatible mobile
+     * devices available. */
+    public static final int PROJECTION_STATE_INACTIVE = 0;
+
+    /** At least one phone connected and ready to project. */
+    public static final int PROJECTION_STATE_READY_TO_PROJECT = 1;
+
+    /** Projecting in the foreground */
+    public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2;
+
+    /** Projection is running in the background */
+    public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3;
+
+    private static final int PROJECTION_STATE_MAX = PROJECTION_STATE_ACTIVE_BACKGROUND;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_NONE = 0;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_USB = 1;
+
+    /** This status is used when projection is not actively running */
+    public static final int PROJECTION_TRANSPORT_WIFI = 2;
+
+    private static final int PROJECTION_TRANSPORT_MAX = PROJECTION_TRANSPORT_WIFI;
+
+    /** @hide */
+    @IntDef(value = {
+            PROJECTION_TRANSPORT_NONE,
+            PROJECTION_TRANSPORT_USB,
+            PROJECTION_TRANSPORT_WIFI,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionTransport {}
+
+    /** @hide */
+    @IntDef(value = {
+            PROJECTION_STATE_INACTIVE,
+            PROJECTION_STATE_READY_TO_PROJECT,
+            PROJECTION_STATE_ACTIVE_FOREGROUND,
+            PROJECTION_STATE_ACTIVE_BACKGROUND,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProjectionState {}
+
+    private final String mPackageName;
+    private final int mState;
+    private final int mTransport;
+    private final List<MobileDevice> mConnectedMobileDevices;
+    private final Bundle mExtras;
+
+    /** Creator for this class. Required to have in parcelable implementations. */
+    public static final Creator<ProjectionStatus> CREATOR = new Creator<ProjectionStatus>() {
+        @Override
+        public ProjectionStatus createFromParcel(Parcel source) {
+            return new ProjectionStatus(source);
+        }
+
+        @Override
+        public ProjectionStatus[] newArray(int size) {
+            return new ProjectionStatus[size];
+        }
+    };
+
+    private ProjectionStatus(Builder builder) {
+        mPackageName = builder.mPackageName;
+        mState = builder.mState;
+        mTransport = builder.mTransport;
+        mConnectedMobileDevices = new ArrayList<>(builder.mMobileDevices);
+        mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+    }
+
+    private ProjectionStatus(Parcel source) {
+        mPackageName = source.readString();
+        mState = source.readInt();
+        mTransport = source.readInt();
+        mExtras = source.readBundle(getClass().getClassLoader());
+        mConnectedMobileDevices = source.createTypedArrayList(MobileDevice.CREATOR);
+    }
+
+    /** Parcelable implementation */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mPackageName);
+        dest.writeInt(mState);
+        dest.writeInt(mTransport);
+        dest.writeBundle(mExtras);
+        dest.writeTypedList(mConnectedMobileDevices);
+    }
+
+    /** Returns projection state which could be one of the constants starting with
+     * {@code #PROJECTION_STATE_}.
+     */
+    public @ProjectionState int getState() {
+        return mState;
+    }
+
+    /** Returns package name of the projection receiver app. */
+    public @NonNull String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns extra information provided by projection receiver app */
+    public @NonNull Bundle getExtras() {
+        return mExtras == null ? new Bundle() : new Bundle(mExtras);
+    }
+
+    /** Returns true if currently projecting either in the foreground or in the background. */
+    public boolean isActive() {
+        return mState == PROJECTION_STATE_ACTIVE_BACKGROUND
+                || mState == PROJECTION_STATE_ACTIVE_FOREGROUND;
+    }
+
+    /** Returns transport which is used for active projection or
+     * {@link #PROJECTION_TRANSPORT_NONE} if projection is not running.
+     */
+    public @ProjectionTransport int getTransport() {
+        return mTransport;
+    }
+
+    /** Returns a list of currently connected mobile devices. */
+    public @NonNull List<MobileDevice> getConnectedMobileDevices() {
+        return new ArrayList<>(mConnectedMobileDevices);
+    }
+
+    /**
+     * Returns new {@link Builder} instance.
+     *
+     * @param packageName package name that will be associated with this status
+     * @param state current projection state, must be one of the {@code PROJECTION_STATE_*}
+     */
+    @NonNull
+    public static Builder builder(String packageName, @ProjectionState int state) {
+        return new Builder(packageName, state);
+    }
+
+    /** Builder class for {@link ProjectionStatus} */
+    public static final class Builder {
+        private final int mState;
+        private final String mPackageName;
+        private int mTransport = PROJECTION_TRANSPORT_NONE;
+        private List<MobileDevice> mMobileDevices = new ArrayList<>();
+        private Bundle mExtras;
+
+        private Builder(String packageName, @ProjectionState int state) {
+            if (packageName == null) {
+                throw new IllegalArgumentException("Package name can't be null");
+            }
+            if (state < 0 || state > PROJECTION_STATE_MAX) {
+                throw new IllegalArgumentException("Invalid projection state: " + state);
+            }
+            mPackageName = packageName;
+            mState = state;
+        }
+
+        /**
+         * Sets the transport which is used for currently projecting phone if any.
+         *
+         * @param transport transport of current projection, must be one of the
+         * {@code PROJECTION_TRANSPORT_*}
+         */
+        public @NonNull Builder setProjectionTransport(@ProjectionTransport int transport) {
+            checkProjectionTransport(transport);
+            mTransport = transport;
+            return this;
+        }
+
+        /**
+         * Add connected mobile device
+         *
+         * @param mobileDevice connected mobile device
+         * @return this builder
+         */
+        public @NonNull Builder addMobileDevice(MobileDevice mobileDevice) {
+            mMobileDevices.add(mobileDevice);
+            return this;
+        }
+
+        /**
+         * Add extra information.
+         *
+         * @param extras may contain an extra information that can be passed from the projection
+         * app to the projection status listeners
+         * @return this builder
+         */
+        public @NonNull Builder setExtras(Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Creates {@link ProjectionStatus} object. */
+        public ProjectionStatus build() {
+            return new ProjectionStatus(this);
+        }
+    }
+
+    private static void checkProjectionTransport(@ProjectionTransport int transport) {
+        if (transport < 0 || transport > PROJECTION_TRANSPORT_MAX) {
+            throw new IllegalArgumentException("Invalid projection transport: " + transport);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ProjectionStatus{"
+                + "mPackageName='" + mPackageName + '\''
+                + ", mState=" + mState
+                + ", mTransport=" + mTransport
+                + ", mConnectedMobileDevices=" + mConnectedMobileDevices
+                + (mExtras != null ? " (has extras)" : "")
+                + '}';
+    }
+
+    /** Class that represents information about connected mobile device. */
+    public static final class MobileDevice implements Parcelable {
+        private final int mId;
+        private final String mName;
+        private final int[] mAvailableTransports;
+        private final boolean mProjecting;
+        private final Bundle mExtras;
+
+        /** Creator for this class. Required to have in parcelable implementations. */
+        public static final Creator<MobileDevice> CREATOR = new Creator<MobileDevice>() {
+            @Override
+            public MobileDevice createFromParcel(Parcel source) {
+                return new MobileDevice(source);
+            }
+
+            @Override
+            public MobileDevice[] newArray(int size) {
+                return new MobileDevice[size];
+            }
+        };
+
+        private MobileDevice(Builder builder) {
+            mId = builder.mId;
+            mName = builder.mName;
+            mAvailableTransports = builder.mAvailableTransports.toArray();
+            mProjecting = builder.mProjecting;
+            mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+        }
+
+        private MobileDevice(Parcel source) {
+            mId = source.readInt();
+            mName = source.readString();
+            mAvailableTransports = source.createIntArray();
+            mProjecting = source.readBoolean();
+            mExtras = source.readBundle(getClass().getClassLoader());
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mId);
+            dest.writeString(mName);
+            dest.writeIntArray(mAvailableTransports);
+            dest.writeBoolean(mProjecting);
+            dest.writeBundle(mExtras);
+        }
+
+        /** Returns the device id which uniquely identifies the mobile device within projection  */
+        public int getId() {
+            return mId;
+        }
+
+        /** Returns the name of the device */
+        public @NonNull String getName() {
+            return mName;
+        }
+
+        /** Returns a list of available projection transports. See {@code PROJECTION_TRANSPORT_*}
+         * for possible values. */
+        public @NonNull List<Integer> getAvailableTransports() {
+            List<Integer> transports = new ArrayList<>(mAvailableTransports.length);
+            for (int transport : mAvailableTransports) {
+                transports.add(transport);
+            }
+            return transports;
+        }
+
+        /** Indicates whether this mobile device is currently projecting */
+        public boolean isProjecting() {
+            return mProjecting;
+        }
+
+        /** Returns extra information for mobile device */
+        public @NonNull Bundle getExtras() {
+            return mExtras == null ? new Bundle() : new Bundle(mExtras);
+        }
+
+        /** Parcelable implementation */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /**
+         * Creates new instance of {@link Builder}
+         *
+         * @param id uniquely identifies the device
+         * @param name name of the connected device
+         * @return the instance of {@link Builder}
+         */
+        public static @NonNull Builder builder(int id, String name) {
+            return new Builder(id, name);
+        }
+
+        @Override
+        public String toString() {
+            return "MobileDevice{"
+                    + "mId=" + mId
+                    + ", mName='" + mName + '\''
+                    + ", mAvailableTransports=" + Arrays.toString(mAvailableTransports)
+                    + ", mProjecting=" + mProjecting
+                    + (mExtras != null ? ", (has extras)" : "")
+                    + '}';
+        }
+
+        /**
+         * Builder class for {@link MobileDevice}
+         */
+        public static final class Builder {
+            private int mId;
+            private String mName;
+            private IntArray mAvailableTransports = new IntArray();
+            private boolean mProjecting;
+            private Bundle mExtras;
+
+            private Builder(int id, String name) {
+                mId = id;
+                if (name == null) {
+                    throw new IllegalArgumentException("Name of the device can't be null");
+                }
+                mName = name;
+            }
+
+            /**
+             * Add supported transport
+             *
+             * @param transport supported transport by given device, must be one of the
+             * {@code PROJECTION_TRANSPORT_*}
+             * @return this builder
+             */
+            public @NonNull Builder addTransport(@ProjectionTransport int transport) {
+                checkProjectionTransport(transport);
+                mAvailableTransports.add(transport);
+                return this;
+            }
+
+            /**
+             * Indicate whether the mobile device currently projecting or not.
+             *
+             * @param projecting {@code True} if this mobile device currently projecting
+             * @return this builder
+             */
+            public @NonNull Builder setProjecting(boolean projecting) {
+                mProjecting = projecting;
+                return this;
+            }
+
+            /**
+             * Add extra information for mobile device
+             *
+             * @param extras provides an arbitrary extra information about this mobile device
+             * @return this builder
+             */
+            public @NonNull Builder setExtras(Bundle extras) {
+                mExtras = extras;
+                return this;
+            }
+
+            /** Creates new instance of {@link MobileDevice} */
+            public @NonNull MobileDevice build() {
+                return new MobileDevice(this);
+            }
+        }
+    }
+}
diff --git a/car-test-lib/src/android/car/testapi/CarProjectionController.java b/car-test-lib/src/android/car/testapi/CarProjectionController.java
index 91de599..c3fada6 100644
--- a/car-test-lib/src/android/car/testapi/CarProjectionController.java
+++ b/car-test-lib/src/android/car/testapi/CarProjectionController.java
@@ -16,10 +16,16 @@
 
 package android.car.testapi;
 
+import android.car.projection.ProjectionOptions;
 import android.net.wifi.WifiConfiguration;
 
 /** Controller to change behavior of {@link android.car.CarProjectionManager} */
 public interface CarProjectionController {
     /** Set WifiConfiguration for wireless projection or null to simulate failure to start AP */
     void setWifiConfiguration(WifiConfiguration wifiConfiguration);
+    /**
+     * Sets {@link ProjectionOptions} object returns by
+     * {@link android.car.CarProjectionManager#getProjectionOptions()} call
+     */
+    void setProjectionOptions(ProjectionOptions projectionOptions);
 }
diff --git a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
index 1bf15cd..bba5a04 100644
--- a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
+++ b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
@@ -21,16 +21,25 @@
 import android.car.CarProjectionManager.ProjectionAccessPointCallback;
 import android.car.ICarProjection;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.net.wifi.WifiConfiguration;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  * Fake implementation of {@link ICarProjection} interface.
  *
@@ -44,6 +53,11 @@
     private WifiConfiguration mWifiConfiguration;
     private Messenger mApMessenger;
     private IBinder mApBinder;
+    private List<ICarProjectionStatusListener> mStatusListeners = new ArrayList<>();
+    private Map<IBinder, ProjectionStatus> mProjectionStatusMap = new HashMap<>();
+    private ProjectionStatus mCurrentProjectionStatus = ProjectionStatus.builder(
+            "", ProjectionStatus.PROJECTION_STATE_INACTIVE).build();
+    private ProjectionOptions mProjectionOptions;
 
     private final ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
@@ -55,6 +69,7 @@
 
     FakeCarProjectionService(Context context) {
         mContext = context;
+        mProjectionOptions = ProjectionOptions.builder().build();
     }
 
     @Override
@@ -118,7 +133,55 @@
     }
 
     @Override
+    public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+            throws RemoteException {
+        mCurrentProjectionStatus = status;
+        mProjectionStatusMap.put(token, status);
+        notifyStatusListeners(status,
+                mStatusListeners.toArray(new ICarProjectionStatusListener[0]));
+    }
+
+    private void notifyStatusListeners(ProjectionStatus status,
+            ICarProjectionStatusListener... listeners) throws RemoteException {
+        for (ICarProjectionStatusListener listener : listeners) {
+            listener.onProjectionStatusChanged(
+                    status.getState(),
+                    status.getPackageName(),
+                    new ArrayList<>(mProjectionStatusMap.values()));
+        }
+    }
+
+    @Override
+    public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        mStatusListeners.add(listener);
+        notifyStatusListeners(mCurrentProjectionStatus, listener);
+    }
+
+    @Override
+    public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        mStatusListeners.remove(listener);
+    }
+
+    @Override
     public void setWifiConfiguration(WifiConfiguration wifiConfiguration) {
         mWifiConfiguration = wifiConfiguration;
     }
+
+    @Override
+    public Bundle getProjectionOptions() throws RemoteException {
+        return mProjectionOptions.toBundle();
+    }
+
+    @Override
+    public int[] getAvailableWifiChannels(int band) throws RemoteException {
+        return new int[] {2412 /* Channel 1 */, 5180 /* Channel 36 */};
+    }
+
+
+    @Override
+    public void setProjectionOptions(ProjectionOptions projectionOptions) {
+        mProjectionOptions = projectionOptions;
+    }
 }
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 34c4c31..76c4d37 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -119,6 +119,16 @@
         android:label="@string/car_permission_label_projection"
         android:description="@string/car_permission_desc_projection" />
     <permission
+            android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"
+            android:protectionLevel="system|signature"
+            android:label="@string/car_permission_label_access_projection_status"
+            android:description="@string/car_permission_desc_access_projection_status" />
+    <permission
+            android:name="android.car.permission.BIND_PROJECTION_SERVICE"
+            android:protectionLevel="signature"
+            android:label="@string/car_permission_label_bind_projection_service"
+            android:description="@string/car_permission_desc_bind_projection_service" />
+    <permission
         android:name="android.car.permission.CAR_MOCK_VEHICLE_HAL"
         android:protectionLevel="system|signature"
         android:label="@string/car_permission_label_mock_vehicle_hal"
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 6f5975a..48f346e 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -143,4 +143,19 @@
 
     <string name="config_TetheredProjectionAccessPointSsid" translatable="false">CarAP</string>
 
+    <!-- The consent activity that must be shown for every unknown mobile device before projection
+         gets started.  The format is: com.your.package/com.your.Activity  -->
+    <string name="config_projectionConsentActivity" translatable="false"/>
+
+    <!-- Display Id where projection rendering activity needs to be shown, Specify -1 to use system
+         defaults -->
+    <integer name="config_projectionActivityDisplayId" translatable="false">-1</integer>
+
+    <!-- Bounds of the projection activity on the screen. It should be in the pixels and screen
+         coordinates in the following order: left, top, right, bottom. -->
+    <integer-array name="config_projectionActivityLaunchBounds" translatable="false"/>
+
+    <!-- UI mode for projection activity. See ProjectionOptions class for possible values. -->
+    <integer name="config_projectionUiMode" translatable="false">0</integer>
+
 </resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 0d9e698..f74710c 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -60,12 +60,20 @@
     <string name="car_permission_desc_radio">Access your car\u2019s radio.</string>
     <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_projection">Car Projection</string>
+    <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_projection">Allows an app to project an interface from a phone on the car\u2019s display.</string>
+    <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_access_projection_status">Access projection status</string>
+    <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_access_projection_status">Allows an app to get the status of other apps projecting to the car\u2019s display.</string>
+    <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+    <string name="car_permission_label_bind_projection_service">Bind to a projection service</string>
+    <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+    <string name="car_permission_desc_bind_projection_service">Allows the holder to bind to the top-level interface of a projection service. Should never be needed for normal apps."</string>
     <!-- Permission text: apps can control car-audio-volume [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_audio_volume">Car Audio Volume</string>
     <!-- Permission text: apps can control car-audio-settings [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_audio_settings">Car Audio Settings</string>
-    <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
-    <string name="car_permission_desc_projection">Project phone interface on car display.</string>
     <string name="car_permission_label_mock_vehicle_hal">Emulate vehicle HAL</string>
     <!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
     <string name="car_permission_desc_mock_vehicle_hal">Emulate your car\u2019s vehicle HAL for internal
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index 0bec14c..8cf7c46 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -19,6 +19,7 @@
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.power.ICarPower;
 import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
 import android.os.Handler;
@@ -29,6 +30,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Log;
 
 import com.android.car.hal.PowerHalService;
@@ -75,6 +77,9 @@
     private int mNextWakeupSec = 0;
     private int mTokenValue = 1;
     private boolean mShutdownOnFinish = false;
+    private boolean mIsBooting = true;
+
+    private final CarUserManagerHelper mCarUserManagerHelper;
 
     // TODO:  Make this OEM configurable.
     private static final int SHUTDOWN_POLLING_INTERVAL_MS = 2000;
@@ -96,10 +101,12 @@
     }
 
     public CarPowerManagementService(
-            Context context, PowerHalService powerHal, SystemInterface systemInterface) {
+            Context context, PowerHalService powerHal, SystemInterface systemInterface,
+            CarUserManagerHelper carUserManagerHelper) {
         mContext = context;
         mHal = powerHal;
         mSystemInterface = systemInterface;
+        mCarUserManagerHelper = carUserManagerHelper;
     }
 
     /**
@@ -113,6 +120,7 @@
         mSystemInterface = null;
         mHandlerThread = null;
         mHandler = new PowerHandler(Looper.getMainLooper());
+        mCarUserManagerHelper = null;
     }
 
     @VisibleForTesting
@@ -186,6 +194,11 @@
         handler.handlePowerStateChange();
     }
 
+    @VisibleForTesting
+    protected void clearIsBooting() {
+        mIsBooting = false;
+    }
+
     /**
      * Initiate state change from CPMS directly.
      */
@@ -262,6 +275,17 @@
     }
 
     private void handleOn() {
+        // Do not switch user if it is booting as there can be a race with CarServiceHelperService
+        if (mIsBooting) {
+            mIsBooting = false;
+        } else {
+            int targetUserId = mCarUserManagerHelper.getInitialUser();
+            if (targetUserId != UserHandle.USER_SYSTEM
+                    && targetUserId != mCarUserManagerHelper.getCurrentForegroundUserId()) {
+                Log.i(CarLog.TAG_POWER, "Desired user changed, switching to user:" + targetUserId);
+                mCarUserManagerHelper.switchToUserId(targetUserId);
+            }
+        }
         mSystemInterface.setDisplayState(true);
         sendPowerManagerEvent(CarPowerStateListener.ON);
         mHal.sendOn();
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 582d535..9570b2a 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -18,6 +18,7 @@
 import static android.car.CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH;
 import static android.car.CarProjectionManager.PROJECTION_VOICE_SEARCH;
 import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
 import static android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_FAILURE_REASON;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
@@ -28,17 +29,25 @@
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLING;
 
 import android.annotation.Nullable;
+import android.app.ActivityOptions;
 import android.bluetooth.BluetoothDevice;
 import android.car.CarProjectionManager;
 import android.car.CarProjectionManager.ProjectionAccessPointCallback;
 import android.car.ICarProjection;
 import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
 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.res.Resources;
+import android.graphics.Rect;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiConfiguration.GroupCipher;
 import android.net.wifi.WifiConfiguration.KeyMgmt;
@@ -47,13 +56,16 @@
 import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
 import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
 import android.net.wifi.WifiManager.SoftApCallback;
+import android.net.wifi.WifiScanner;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -67,6 +79,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Random;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * Car projection service allows to bound to projected app to boost it prioirity.
@@ -88,17 +101,36 @@
     @GuardedBy("mLock")
     private final HashMap<IBinder, WirelessClient> mWirelessClients = new HashMap<>();
 
-    @Nullable
     @GuardedBy("mLock")
-    private LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
+    private @Nullable LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
 
-    @Nullable
+
     @GuardedBy("mLock")
-    private SoftApCallback mSoftApCallback;
+    private @Nullable SoftApCallback mSoftApCallback;
+
+    @GuardedBy("mLock")
+    private final HashMap<IBinder, ProjectionReceiverClient> mProjectionReceiverClients =
+            new HashMap<>();
 
     @Nullable
     private String mApBssid;
 
+    @GuardedBy("mLock")
+    private @Nullable WifiScanner mWifiScanner;
+
+    @GuardedBy("mLock")
+    private @ProjectionState int mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+
+    @GuardedBy("mLock")
+    private ProjectionOptions mProjectionOptions;
+
+    @GuardedBy("mLock")
+    private @Nullable String mCurrentProjectionPackage;
+
+    private final List<ICarProjectionStatusListener> mProjectionStatusListeners =
+            new CopyOnWriteArrayList<>();
+
+
     private static final int WIFI_MODE_TETHERED = 1;
     private static final int WIFI_MODE_LOCALONLY = 2;
 
@@ -139,10 +171,10 @@
     private boolean mBound;
     private Intent mRegisteredService;
 
-    CarProjectionService(Context context, CarInputService carInputService,
-            CarBluetoothService carBluetoothService) {
+    CarProjectionService(Context context, @Nullable Handler handler,
+            CarInputService carInputService, CarBluetoothService carBluetoothService) {
         mContext = context;
-        mHandler = new Handler();
+        mHandler = handler == null ? new Handler() : handler;
         mCarInputService = carInputService;
         mCarBluetoothService = carBluetoothService;
         mProjectionCallbacks = new ProjectionCallbackHolder(this);
@@ -152,6 +184,7 @@
 
     @Override
     public void registerProjectionRunner(Intent serviceIntent) {
+        ICarImpl.assertProjectionPermission(mContext);
         // We assume one active projection app running in the system at one time.
         synchronized (mLock) {
             if (serviceIntent.filterEquals(mRegisteredService) && mBound) {
@@ -168,6 +201,7 @@
 
     @Override
     public void unregisterProjectionRunner(Intent serviceIntent) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             if (!serviceIntent.filterEquals(mRegisteredService)) {
                 Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
@@ -218,6 +252,7 @@
 
     @Override
     public void registerProjectionListener(ICarProjectionCallback callback, int filter) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             ProjectionCallback info = mProjectionCallbacks.get(callback);
             if (info == null) {
@@ -232,6 +267,7 @@
 
     @Override
     public void unregisterProjectionListener(ICarProjectionCallback listener) {
+        ICarImpl.assertProjectionPermission(mContext);
         synchronized (mLock) {
             mProjectionCallbacks.removeBinder(listener);
         }
@@ -241,6 +277,7 @@
     @Override
     public void startProjectionAccessPoint(final Messenger messenger, IBinder binder)
             throws RemoteException {
+        ICarImpl.assertProjectionPermission(mContext);
         //TODO: check if access point already started with the desired configuration.
         registerWirelessClient(WirelessClient.of(messenger, binder));
         startAccessPoint();
@@ -248,6 +285,7 @@
 
     @Override
     public void stopProjectionAccessPoint(IBinder token) {
+        ICarImpl.assertProjectionPermission(mContext);
         Log.i(TAG, "Received stop access point request from " + token);
 
         boolean shouldReleaseAp;
@@ -264,6 +302,35 @@
         }
     }
 
+    @Override
+    public int[] getAvailableWifiChannels(int band) {
+        ICarImpl.assertProjectionPermission(mContext);
+        WifiScanner scanner;
+        synchronized (mLock) {
+            // Lazy initialization
+            if (mWifiScanner == null) {
+                mWifiScanner = mContext.getSystemService(WifiScanner.class);
+            }
+            scanner = mWifiScanner;
+        }
+        if (scanner == null) {
+            Log.w(TAG, "Unable to get WifiScanner");
+            return new int[0];
+        }
+
+        List<Integer> channels = scanner.getAvailableChannels(band);
+        if (channels == null || channels.isEmpty()) {
+            Log.w(TAG, "WifiScanner reported no available channels");
+            return new int[0];
+        }
+
+        int[] array = new int[channels.size()];
+        for (int i = 0; i < channels.size(); i++) {
+            array[i] = channels.get(i);
+        }
+        return array;
+    }
+
     /**
      * Request to disconnect the given profile on the given device, and prevent it from reconnecting
      * until either the request is released, or the process owning the given token dies.
@@ -281,6 +348,7 @@
             Log.d(TAG, "requestBluetoothProfileInhibit device=" + device + " profile=" + profile
                     + " from uid " + Binder.getCallingUid());
         }
+        ICarImpl.assertProjectionPermission(mContext);
         try {
             if (device == null) {
                 // Will be caught by AIDL and thrown to caller.
@@ -313,6 +381,7 @@
             Log.d(TAG, "releaseBluetoothProfileInhibit device=" + device + " profile=" + profile
                     + " from uid " + Binder.getCallingUid());
         }
+        ICarImpl.assertProjectionPermission(mContext);
         try {
             if (device == null) {
                 // Will be caught by AIDL and thrown to caller.
@@ -328,6 +397,155 @@
         }
     }
 
+    @Override
+    public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+            throws RemoteException {
+        if (DBG) {
+            Log.d(TAG, "updateProjectionStatus, status: " + status + ", token: " + token);
+        }
+        ICarImpl.assertProjectionPermission(mContext);
+        final String packageName = status.getPackageName();
+        final int uid = Binder.getCallingUid();
+        try {
+            if (uid != mContext.getPackageManager().getPackageUid(packageName, 0)) {
+                throw new SecurityException(
+                        "UID " + uid + " cannot update status for package " + packageName);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new SecurityException("Package " + packageName + " does not exist", e);
+        }
+
+        synchronized (mLock) {
+            ProjectionReceiverClient client = getOrCreateProjectionReceiverClientLocked(token);
+            client.mProjectionStatus = status;
+
+            if (status.isActive() || TextUtils.equals(packageName, mCurrentProjectionPackage)) {
+                mCurrentProjectionState = status.getState();
+                mCurrentProjectionPackage = packageName;
+            }
+        }
+        notifyProjectionStatusChanged(null /* notify all listeners */);
+    }
+
+    @Override
+    public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        ICarImpl.assertProjectionStatusPermission(mContext);
+        mProjectionStatusListeners.add(listener);
+
+        // Immediately notify listener with the current status.
+        notifyProjectionStatusChanged(listener);
+    }
+
+    @Override
+    public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+            throws RemoteException {
+        ICarImpl.assertProjectionStatusPermission(mContext);
+        mProjectionStatusListeners.remove(listener);
+    }
+
+    private ProjectionReceiverClient getOrCreateProjectionReceiverClientLocked(
+            IBinder token) throws RemoteException {
+        ProjectionReceiverClient client;
+        client = mProjectionReceiverClients.get(token);
+        if (client == null) {
+            client = new ProjectionReceiverClient(() -> unregisterProjectionReceiverClient(token));
+            token.linkToDeath(client.mDeathRecipient, 0 /* flags */);
+            mProjectionReceiverClients.put(token, client);
+        }
+        return client;
+    }
+
+    private void unregisterProjectionReceiverClient(IBinder token) {
+        synchronized (mLock) {
+            ProjectionReceiverClient client = mProjectionReceiverClients.remove(token);
+            if (client != null && TextUtils.equals(
+                    client.mProjectionStatus.getPackageName(), mCurrentProjectionPackage)) {
+                mCurrentProjectionPackage = null;
+                mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+            }
+        }
+    }
+
+    private void notifyProjectionStatusChanged(
+            @Nullable ICarProjectionStatusListener singleListenerToNotify)
+            throws RemoteException {
+        int currentState;
+        String currentPackage;
+        List<ProjectionStatus> statuses = new ArrayList<>();
+        synchronized (mLock) {
+            for (ProjectionReceiverClient client : mProjectionReceiverClients.values()) {
+                statuses.add(client.mProjectionStatus);
+            }
+            currentState = mCurrentProjectionState;
+            currentPackage = mCurrentProjectionPackage;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "Notify projection status change, state: " + currentState + ", pkg: "
+                    + currentPackage + ", listeners: " + mProjectionStatusListeners.size()
+                    + ", listenerToNotify: " + singleListenerToNotify);
+        }
+
+        if (singleListenerToNotify == null) {
+            for (ICarProjectionStatusListener listener : mProjectionStatusListeners) {
+                listener.onProjectionStatusChanged(currentState, currentPackage, statuses);
+            }
+        } else {
+            singleListenerToNotify.onProjectionStatusChanged(
+                    currentState, currentPackage, statuses);
+        }
+    }
+
+    @Override
+    public Bundle getProjectionOptions() {
+        ICarImpl.assertProjectionPermission(mContext);
+        synchronized (mLock) {
+            if (mProjectionOptions == null) {
+                mProjectionOptions = createProjectionOptionsBuilder()
+                        .build();
+            }
+        }
+        return mProjectionOptions.toBundle();
+    }
+
+    private ProjectionOptions.Builder createProjectionOptionsBuilder() {
+        Resources res = mContext.getResources();
+
+        ProjectionOptions.Builder builder = ProjectionOptions.builder();
+
+        ActivityOptions activityOptions = createActivityOptions(res);
+        if (activityOptions != null) {
+            builder.setProjectionActivityOptions(activityOptions);
+        }
+
+        String consentActivity = res.getString(R.string.config_projectionConsentActivity);
+        if (!TextUtils.isEmpty(consentActivity)) {
+            builder.setConsentActivity(ComponentName.unflattenFromString(consentActivity));
+        }
+
+        builder.setUiMode(res.getInteger(R.integer.config_projectionUiMode));
+        return builder;
+    }
+
+    @Nullable
+    private static ActivityOptions createActivityOptions(Resources res) {
+        ActivityOptions activityOptions = ActivityOptions.makeBasic();
+        boolean changed = false;
+        int displayId = res.getInteger(R.integer.config_projectionActivityDisplayId);
+        if (displayId != -1) {
+            activityOptions.setLaunchDisplayId(displayId);
+            changed = true;
+        }
+        int[] rawBounds = res.getIntArray(R.array.config_projectionActivityLaunchBounds);
+        if (rawBounds != null && rawBounds.length == 4) {
+            Rect bounds = new Rect(rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3]);
+            activityOptions.setLaunchBounds(bounds);
+            changed = true;
+        }
+        return changed ? activityOptions : null;
+    }
+
     private void startAccessPoint() {
         synchronized (mLock) {
             switch (mWifiMode) {
@@ -588,6 +806,10 @@
             writer.println("SoftApCallback: " + mSoftApCallback);
             writer.println("Bound to projection app: " + mBound);
             writer.println("Registered Service: " + mRegisteredService);
+            writer.println("Current projection state: " + mCurrentProjectionState);
+            writer.println("Current projection package: " + mCurrentProjectionPackage);
+            writer.println("Projection status: " + mProjectionReceiverClients);
+            writer.println("WifiScanner: " + mWifiScanner);
         }
     }
 
@@ -599,6 +821,14 @@
         }
     }
 
+    void setUiMode(Integer uiMode) {
+        synchronized (mLock) {
+            mProjectionOptions = createProjectionOptionsBuilder()
+                    .setUiMode(uiMode)
+                    .build();
+        }
+    }
+
     private static class ProjectionCallbackHolder
             extends BinderInterfaceContainer<ICarProjectionCallback> {
         ProjectionCallbackHolder(CarProjectionService service) {
@@ -796,4 +1026,21 @@
         Random random = new Random();
         return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
     }
+
+    private static class ProjectionReceiverClient {
+        private final DeathRecipient mDeathRecipient;
+        private ProjectionStatus mProjectionStatus;
+
+        ProjectionReceiverClient(DeathRecipient deathRecipient) {
+            mDeathRecipient = deathRecipient;
+        }
+
+        @Override
+        public String toString() {
+            return "ProjectionReceiverClient{"
+                    + "mDeathRecipient=" + mDeathRecipient
+                    + ", mProjectionStatus=" + mProjectionStatus
+                    + '}';
+        }
+    }
 }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 6d51640..2df562e 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -121,7 +121,7 @@
         mCarUserService = new CarUserService(serviceContext, mUserManagerHelper);
         mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
         mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
-                systemInterface);
+                systemInterface, mUserManagerHelper);
         mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
         mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
         mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
@@ -134,7 +134,7 @@
                 mPerUserCarServiceHelper, mCarUXRestrictionsService);
         mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
         mCarProjectionService = new CarProjectionService(
-                serviceContext, mCarInputService, mCarBluetoothService);
+                serviceContext, null /* handler */, mCarInputService, mCarBluetoothService);
         mGarageModeService = new GarageModeService(mContext);
         mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
         mCarAudioService = new CarAudioService(serviceContext);
@@ -274,7 +274,6 @@
                 assertClusterManagerPermission(mContext);
                 return mInstrumentClusterService.getManagerService();
             case Car.PROJECTION_SERVICE:
-                assertProjectionPermission(mContext);
                 return mCarProjectionService;
             case Car.VMS_SUBSCRIBER_SERVICE:
                 assertVmsSubscriberPermission(mContext);
@@ -349,6 +348,11 @@
         assertPermission(context, Car.PERMISSION_CAR_PROJECTION);
     }
 
+    /** Verify the calling context has the {@link Car#PERMISSION_CAR_PROJECTION_STATUS} */
+    public static void assertProjectionStatusPermission(Context context) {
+        assertPermission(context, Car.PERMISSION_CAR_PROJECTION_STATUS);
+    }
+
     public static void assertAnyDiagnosticPermission(Context context) {
         assertAnyPermission(context,
                 Car.PERMISSION_CAR_DIAGNOSTIC_READ_ALL,
@@ -471,6 +475,7 @@
         private static final String COMMAND_GARAGE_MODE = "garage-mode";
         private static final String COMMAND_GET_DO_ACTIVITIES = "get-do-activities";
         private static final String COMMAND_GET_CARPROPERTYCONFIG = "get-carpropertyconfig";
+        private static final String COMMAND_PROJECTION_UI_MODE = "projection-ui-mode";
 
         private static final String PARAM_DAY_MODE = "day";
         private static final String PARAM_NIGHT_MODE = "night";
@@ -568,6 +573,13 @@
                     String propertyId = args.length < 2 ? "" : args[1];
                     mHal.dumpPropertyConfigs(writer, propertyId);
                     break;
+                case COMMAND_PROJECTION_UI_MODE:
+                    if (args.length != 2) {
+                        writer.println("Incorrect number of arguments");
+                        dumpHelp(writer);
+                        break;
+                    }
+                    mCarProjectionService.setUiMode(Integer.valueOf(args[1]));
                 default:
                     writer.println("Unknown command: \"" + arg + "\"");
                     dumpHelp(writer);
diff --git a/service/src/com/android/car/SystemActivityMonitoringService.java b/service/src/com/android/car/SystemActivityMonitoringService.java
index c2abdc6..11b6887 100644
--- a/service/src/com/android/car/SystemActivityMonitoringService.java
+++ b/service/src/com/android/car/SystemActivityMonitoringService.java
@@ -425,6 +425,10 @@
         }
 
         @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+        }
+
+        @Override
         public void onProcessDied(int pid, int uid) {
             mHandler.requestProcessDied(pid, uid);
         }
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index dd38c33..ca90f78 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -397,6 +397,9 @@
         mProps.put(VehicleProperty.EV_BATTERY_DISPLAY_UNITS, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
+                Car.PERMISSION_READ_DISPLAY_UNITS,
+                Car.PERMISSION_CONTROL_DISPLAY_UNITS));
     }
 
     /**
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
index d102a49..6cabd69 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -60,6 +60,10 @@
         }
 
         @Override
+        public void onForegroundServicesChanged(int pid, int uid, int fgServicetypes) {
+        }
+
+        @Override
         public void onProcessDied(int pid, int uid) {
             notifyTopActivities();
         }
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
index abfeb11..db04c24 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
@@ -85,7 +85,7 @@
         try {
             mManager.registerProjectionListener(null, CarProjectionManager.PROJECTION_VOICE_SEARCH);
             fail();
-        } catch (IllegalArgumentException e) {
+        } catch (NullPointerException e) {
             // expected.
         }
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
index 557816a..e1d9c90 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -16,8 +16,15 @@
 
 package com.android.car;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateShutdownParam;
 import android.os.RemoteException;
@@ -52,6 +59,7 @@
     private final MockWakeLockInterface mWakeLockInterface = new MockWakeLockInterface();
     private final MockIOInterface mIOInterface = new MockIOInterface();
     private final PowerSignalListener mPowerSignalListener = new PowerSignalListener();
+    private CarUserManagerHelper mCarUserManagerHelper;
 
     private MockedPowerHalService mPowerHal;
     private SystemInterface mSystemInterface;
@@ -68,6 +76,10 @@
             .withSystemStateInterface(mSystemStateInterface)
             .withWakeLockInterface(mWakeLockInterface)
             .withIOInterface(mIOInterface).build();
+        mCarUserManagerHelper = mock(CarUserManagerHelper.class);
+        doReturn(true).when(mCarUserManagerHelper).switchToUserId(anyInt());
+        doReturn(10).when(mCarUserManagerHelper).getInitialUser();
+        doReturn(10).when(mCarUserManagerHelper).getCurrentForegroundUserId();
     }
 
     @Override
@@ -83,7 +95,8 @@
      * Helper method to create mService and initialize a test case
      */
     private void initTest(int wakeupTime) throws Exception {
-        mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface);
+        mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface,
+                mCarUserManagerHelper);
         mService.init();
         CarPowerManagementService.setShutdownPrepareTimeout(0);
         mPowerHal.setSignalListener(mPowerSignalListener);
@@ -103,7 +116,6 @@
         mSystemInterface.setDisplayState(false);
         mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS);
         initTest(0);
-
         // Transition to ON state
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
 
@@ -160,6 +172,13 @@
     public void testSleepEntryAndWakeUpForProcessing() throws Exception {
         final int wakeupTime = 100;
         initTest(wakeupTime);
+
+        // set up for user switching after display on
+        final int currentUserId = 10;
+        final int newUserId = 11;
+        doReturn(newUserId).when(mCarUserManagerHelper).getInitialUser();
+        doReturn(currentUserId).when(mCarUserManagerHelper).getCurrentForegroundUserId();
+
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
         assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
@@ -177,8 +196,12 @@
         mService.scheduleNextWakeupTime(wakeupTime);
         // second processing after wakeup
         assertFalse(mDisplayInterface.getDisplayState());
+        // do not skip user switching part.
+        mService.clearIsBooting();
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
         assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+        // user switching should have been requested.
+        verify(mCarUserManagerHelper, times(1)).switchToUserId(newUserId);
         mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
                 VehicleApPowerStateShutdownParam.CAN_SLEEP));
         assertStateReceivedForShutdownOrSleepWithPostpone(
diff --git a/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
new file mode 100644
index 0000000..476c152
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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;
+
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_USB;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_WIFI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.MobileDevice;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.net.wifi.WifiScanner;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class CarProjectionServiceTest {
+    private static final int MD_ID1 = 1;
+    private static final int MD_ID2 = 2;
+    private static final String MD_NAME1 = "Device1";
+    private static final String MD_NAME2 = "Device2";
+    private static final int DEFAULT_TIMEOUT_MS = 1000;
+    private static final String MD_EXTRA_KEY = "com.some.key.md";
+    private static final String MD_EXTRA_VALUE = "this is dummy value";
+    private static final String STATUS_EXTRA_KEY = "com.some.key.status";
+    private static final String STATUS_EXTRA_VALUE = "additional status value";
+
+    private final IBinder mToken = new Binder();
+
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+
+    private CarProjectionService mService;
+
+    @Spy
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Mock
+    private Resources mResources;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    @Mock
+    private CarInputService mCarInputService;
+    @Mock
+    private CarBluetoothService mCarBluetoothService;
+
+    @Before
+    public void setUp() {
+        mService = new CarProjectionService(mContext, mHandler, mCarInputService,
+                mCarBluetoothService);
+    }
+
+    @Test
+    public void updateProjectionStatus_defaultState() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(2);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+                assertThat(activeProjectionPackageName).isNull();
+                assertThat(details).isEmpty();
+
+                latch.countDown();
+            }
+        });
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void updateProjectionStatus_subscribeAfterUpdate() throws Exception {
+        final ProjectionStatus status = createProjectionStatus();
+        mService.updateProjectionStatus(status, mToken);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+                assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+                assertThat(details).hasSize(1);
+                assertThat(details.get(0)).isEqualTo(status);
+                ProjectionStatus status = details.get(0);
+                assertThat(status.getTransport()).isEqualTo(PROJECTION_TRANSPORT_WIFI);
+                assertThat(status.getExtras()).isNotNull();
+                assertThat(status.getExtras().getString(STATUS_EXTRA_KEY))
+                        .isEqualTo(STATUS_EXTRA_VALUE);
+                assertThat(status.getConnectedMobileDevices()).hasSize(2);
+                MobileDevice md1 = status.getConnectedMobileDevices().get(0);
+                assertThat(md1.getId()).isEqualTo(MD_ID1);
+                assertThat(md1.getName()).isEqualTo(MD_NAME1);
+                assertThat(md1.getExtras()).isNotNull();
+                assertThat(md1.getExtras().getString(MD_EXTRA_KEY)).isEqualTo(MD_EXTRA_VALUE);
+                assertThat(md1.getAvailableTransports()).hasSize(1);
+                assertThat(md1.getAvailableTransports()).containsExactly(
+                        PROJECTION_TRANSPORT_USB);
+
+                MobileDevice md2 = status.getConnectedMobileDevices().get(1);
+                assertThat(md2.getId()).isEqualTo(MD_ID2);
+                assertThat(md2.getName()).isEqualTo(MD_NAME2);
+                assertThat(md2.getExtras()).isNotNull();
+                assertThat(md2.getExtras().isEmpty()).isTrue();
+                assertThat(md2.getAvailableTransports()).containsExactly(
+                        PROJECTION_TRANSPORT_USB, PROJECTION_TRANSPORT_WIFI);
+
+                latch.countDown();
+            }
+        });
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void updateProjectionStatus_subscribeBeforeUpdate() throws Exception {
+
+        // We will receive notification twice - with default value and with updated one.
+        final CountDownLatch latch = new CountDownLatch(2);
+
+        mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+            @Override
+            public void onProjectionStatusChanged(int projectionState,
+                    String activeProjectionPackageName, List<ProjectionStatus> details) {
+                if (latch.getCount() == 2) {
+                    assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+                    assertThat(activeProjectionPackageName).isNull();
+                } else {
+                    assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+                    assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+                }
+
+                latch.countDown();
+            }
+        });
+        mService.updateProjectionStatus(createProjectionStatus(), mToken);
+
+        latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void getProjectionOptions_defaults() {
+        when(mContext.getResources()).thenReturn(mResources);
+        final int uiMode = ProjectionOptions.UI_MODE_FULL_SCREEN;
+
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+                .thenReturn(uiMode);
+        when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+                .thenReturn("");
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+                .thenReturn(-1);
+        when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+                .thenReturn(new int[0]);
+
+        Bundle bundle = mService.getProjectionOptions();
+
+        ProjectionOptions options = new ProjectionOptions(bundle);
+        assertThat(options.getActivityOptions()).isNull();
+        assertThat(options.getConsentActivity()).isNull();
+        assertThat(options.getUiMode()).isEqualTo(uiMode);
+    }
+
+    @Test
+    public void getProjectionOptions_nonDefaults() {
+        when(mContext.getResources()).thenReturn(mResources);
+        final int uiMode = ProjectionOptions.UI_MODE_BLENDED;
+        final String consentActivity = "com.my.app/.MyActivity";
+        final int[] bounds = new int[] {1, 2, 3, 4};
+        final int displayId = 1;
+
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+                .thenReturn(uiMode);
+        when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+                .thenReturn(consentActivity);
+        when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+                .thenReturn(displayId);
+        when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+                .thenReturn(bounds);
+
+        Bundle bundle = mService.getProjectionOptions();
+
+        ProjectionOptions options = new ProjectionOptions(bundle);
+        assertThat(options.getActivityOptions().getLaunchDisplayId()).isEqualTo(displayId);
+        assertThat(options.getActivityOptions().getLaunchBounds())
+                .isEqualTo(new Rect(bounds[0], bounds[1], bounds[2], bounds[3]));
+        assertThat(options.getConsentActivity()).isEqualTo(
+                ComponentName.unflattenFromString(consentActivity));
+        assertThat(options.getUiMode()).isEqualTo(uiMode);
+    }
+
+    @Test
+    public void getWifiChannels() {
+        int[] wifiChannels = mService.getAvailableWifiChannels(WifiScanner.WIFI_BAND_BOTH_WITH_DFS);
+        assertThat(wifiChannels).isNotNull();
+        assertThat(wifiChannels).isNotEmpty();
+    }
+
+    private ProjectionStatus createProjectionStatus() {
+        Bundle statusExtra = new Bundle();
+        statusExtra.putString(STATUS_EXTRA_KEY, STATUS_EXTRA_VALUE);
+        Bundle mdExtra = new Bundle();
+        mdExtra.putString(MD_EXTRA_KEY, MD_EXTRA_VALUE);
+
+        return ProjectionStatus
+                .builder(mContext.getPackageName(), PROJECTION_STATE_ACTIVE_FOREGROUND)
+                .setExtras(statusExtra)
+                .setProjectionTransport(PROJECTION_TRANSPORT_WIFI)
+                .addMobileDevice(MobileDevice
+                        .builder(MD_ID1, MD_NAME1)
+                        .addTransport(PROJECTION_TRANSPORT_USB)
+                        .setExtras(mdExtra)
+                        .build())
+                .addMobileDevice(MobileDevice
+                        .builder(MD_ID2, MD_NAME2)
+                        .addTransport(PROJECTION_TRANSPORT_USB)
+                        .addTransport(PROJECTION_TRANSPORT_WIFI)
+                        .setProjecting(true)
+                        .build())
+                .build();
+    }
+}