Move car related code from SystemUI to CarSystemUI


Test: Emulator phone and Car
Change-Id: Ia64a23c1d3643899118e578b82c665c034af1c8e
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
new file mode 100644
index 0000000..58f80a4
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.BatteryController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon
+ * displays the battery status of a device that is connected via bluetooth and not the system's
+ * battery.
+ */
+public class CarBatteryController extends BroadcastReceiver implements BatteryController {
+    private static final String TAG = "CarBatteryController";
+
+    // According to the Bluetooth HFP 1.5 specification, battery levels are indicated by a
+    // value from 1-5, where these values represent the following:
+    // 0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+    // As a result, set the level as the average within that range.
+    private static final int BATTERY_LEVEL_EMPTY = 0;
+    private static final int BATTERY_LEVEL_1 = 12;
+    private static final int BATTERY_LEVEL_2 = 28;
+    private static final int BATTERY_LEVEL_3 = 63;
+    private static final int BATTERY_LEVEL_4 = 87;
+    private static final int BATTERY_LEVEL_FULL = 100;
+
+    private static final int INVALID_BATTERY_LEVEL = -1;
+
+    private final Context mContext;
+
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+    private BluetoothHeadsetClient mBluetoothHeadsetClient;
+    private final ServiceListener mHfpServiceListener = new ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = null;
+            }
+        }
+    };
+    private int mLevel;
+    private BatteryViewHandler mBatteryViewHandler;
+
+    public CarBatteryController(Context context) {
+        mContext = context;
+
+        if (mAdapter == null) {
+            return;
+        }
+
+        mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+                BluetoothProfile.HEADSET_CLIENT);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CarBatteryController state:");
+        pw.print("    mLevel=");
+        pw.println(mLevel);
+    }
+
+    @Override
+    public void setPowerSaveMode(boolean powerSave) {
+        // No-op. No power save mode for the car.
+    }
+
+    @Override
+    public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.add(cb);
+
+        // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
+        // false for these values.
+        cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        cb.onPowerSaveChanged(false /* isPowerSave */);
+    }
+
+    @Override
+    public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.remove(cb);
+    }
+
+    public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
+        mBatteryViewHandler = batteryViewHandler;
+    }
+
+    public void startListening() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+        mContext.registerReceiver(this, filter);
+    }
+
+    public void stopListening() {
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onReceive(). action: " + action);
+        }
+
+        if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Received ACTION_AG_EVENT");
+            }
+
+            int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+
+            updateBatteryLevel(batteryLevel);
+
+            if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+                Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+                        + oldState + " -> " + newState);
+
+            }
+            BluetoothDevice device =
+                    (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+            updateBatteryIcon(device, newState);
+        }
+    }
+
+    /**
+     * Converts the battery level to a percentage that can be displayed on-screen and notifies
+     * any {@link BatteryStateChangeCallback}s of this.
+     */
+    private void updateBatteryLevel(int batteryLevel) {
+        if (batteryLevel == INVALID_BATTERY_LEVEL) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Battery level invalid. Ignoring.");
+            }
+            return;
+        }
+
+        // The battery level is a value between 0-5. Let the default battery level be 0.
+        switch (batteryLevel) {
+            case 5:
+                mLevel = BATTERY_LEVEL_FULL;
+                break;
+            case 4:
+                mLevel = BATTERY_LEVEL_4;
+                break;
+            case 3:
+                mLevel = BATTERY_LEVEL_3;
+                break;
+            case 2:
+                mLevel = BATTERY_LEVEL_2;
+                break;
+            case 1:
+                mLevel = BATTERY_LEVEL_1;
+                break;
+            case 0:
+            default:
+                mLevel = BATTERY_LEVEL_EMPTY;
+        }
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
+        }
+
+        notifyBatteryLevelChanged();
+    }
+
+    /**
+     * Updates the display of the battery icon depending on the given connection state from the
+     * given {@link BluetoothDevice}.
+     */
+    private void updateBatteryIcon(BluetoothDevice device, int newState) {
+        if (newState == BluetoothProfile.STATE_CONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device connected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+
+            if (mBluetoothHeadsetClient == null || device == null) {
+                return;
+            }
+
+            // Check if battery information is available and immediately update.
+            Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+            if (featuresBundle == null) {
+                return;
+            }
+
+            int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+            updateBatteryLevel(batteryLevel);
+        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device disconnected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.hideBatteryView();
+            }
+        }
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        // TODO: Car demo mode.
+    }
+
+    @Override
+    public boolean isPowerSave() {
+        // Power save is not valid for the car, so always return false.
+        return false;
+    }
+
+    private void notifyBatteryLevelChanged() {
+        for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
+            mChangeCallbacks.get(i)
+                    .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        }
+    }
+
+    /**
+     * An interface indicating the container of a View that will display what the information
+     * in the {@link CarBatteryController}.
+     */
+    public interface BatteryViewHandler {
+        void hideBatteryView();
+
+        void showBatteryView();
+    }
+
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java
new file mode 100644
index 0000000..56db242
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.Display;
+import android.view.View;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * CarFacetButtons placed on the nav bar are designed to have visual indication that the active
+ * application on screen is associated with it. This is basically a similar concept to a radio
+ * button group.
+ */
+public class CarFacetButtonController {
+
+    protected HashMap<String, CarFacetButton> mButtonsByCategory = new HashMap<>();
+    protected HashMap<String, CarFacetButton> mButtonsByPackage = new HashMap<>();
+    protected HashMap<String, CarFacetButton> mButtonsByComponentName = new HashMap<>();
+    protected CarFacetButton mSelectedFacetButton;
+    protected Context mContext;
+
+    public CarFacetButtonController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Add facet button to this controller. The expected use is for the facet button
+     * to get a reference to this controller via {@link com.android.systemui.Dependency}
+     * and self add.
+     */
+    public void addFacetButton(CarFacetButton facetButton) {
+        String[] categories = facetButton.getCategories();
+        for (int i = 0; i < categories.length; i++) {
+            mButtonsByCategory.put(categories[i], facetButton);
+        }
+
+        String[] facetPackages = facetButton.getFacetPackages();
+        for (int i = 0; i < facetPackages.length; i++) {
+            mButtonsByPackage.put(facetPackages[i], facetButton);
+        }
+        String[] componentNames = facetButton.getComponentName();
+        for (int i = 0; i < componentNames.length; i++) {
+            mButtonsByComponentName.put(componentNames[i], facetButton);
+        }
+        // Using the following as a default button for display id info it's not
+        // attached to a screen at this point so it can't be extracted here.
+        mSelectedFacetButton = facetButton;
+    }
+
+    public void removeAll() {
+        mButtonsByCategory.clear();
+        mButtonsByPackage.clear();
+        mButtonsByComponentName.clear();
+        mSelectedFacetButton = null;
+    }
+
+    /**
+     * This will unselect the currently selected CarFacetButton and determine which one should be
+     * selected next. It does this by reading the properties on the CarFacetButton and seeing if
+     * they are a match with the supplied StackInfo list.
+     * The order of selection detection is ComponentName, PackageName then Category
+     * They will then be compared with the supplied StackInfo list.
+     * The StackInfo is expected to be supplied in order of recency and StackInfo will only be used
+     * for consideration if it has the same displayId as the CarFacetButtons.
+     *
+     * @param stackInfoList of the currently running application
+     */
+    public void taskChanged(List<ActivityManager.StackInfo> stackInfoList) {
+        int displayId = getDisplayId();
+        ActivityManager.StackInfo validStackInfo = null;
+        for (ActivityManager.StackInfo stackInfo : stackInfoList) {
+            // If the display id is unknown or it matches the stack, it's valid for use
+            if ((displayId == -1 || displayId == stackInfo.displayId)
+                    && stackInfo.topActivity != null) {
+                validStackInfo = stackInfo;
+                break;
+            }
+        }
+
+        if (validStackInfo == null) {
+            // No stack was found that was on the same display as the facet buttons thus return
+            return;
+        }
+
+        if (mSelectedFacetButton != null) {
+            mSelectedFacetButton.setSelected(false);
+        }
+
+        String packageName = validStackInfo.topActivity.getPackageName();
+        CarFacetButton facetButton = findFacetButtongByComponentName(validStackInfo.topActivity);
+        if (facetButton == null) {
+            facetButton = mButtonsByPackage.get(packageName);
+        }
+
+        if (facetButton == null) {
+            String category = getPackageCategory(packageName);
+            if (category != null) {
+                facetButton = mButtonsByCategory.get(category);
+            }
+        }
+
+        if (facetButton != null && facetButton.getVisibility() == View.VISIBLE) {
+            facetButton.setSelected(true);
+            mSelectedFacetButton = facetButton;
+        }
+
+    }
+
+    private int getDisplayId() {
+        if (mSelectedFacetButton != null) {
+            Display display = mSelectedFacetButton.getDisplay();
+            if (display != null) {
+                return display.getDisplayId();
+            }
+        }
+        return -1;
+    }
+
+    private CarFacetButton findFacetButtongByComponentName(ComponentName componentName) {
+        CarFacetButton button = mButtonsByComponentName.get(componentName.flattenToShortString());
+        return (button != null) ? button :
+                mButtonsByComponentName.get(componentName.flattenToString());
+    }
+
+    protected String getPackageCategory(String packageName) {
+        PackageManager pm = mContext.getPackageManager();
+        Set<String> supportedCategories = mButtonsByCategory.keySet();
+        for (String category : supportedCategories) {
+            Intent intent = new Intent();
+            intent.setPackage(packageName);
+            intent.setAction(Intent.ACTION_MAIN);
+            intent.addCategory(category);
+            List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+            if (list.size() > 0) {
+                // Cache this package name into facetPackageMap, so we won't have to query
+                // all categories next time this package name shows up.
+                mButtonsByPackage.put(packageName, mButtonsByCategory.get(category));
+                return category;
+            }
+        }
+        return null;
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java
new file mode 100644
index 0000000..81f7846
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+
+/**
+ * A custom navigation bar for the automotive use case.
+ * <p>
+ * The navigation bar in the automotive use case is more like a list of shortcuts, rendered
+ * in a linear layout.
+ */
+class CarNavigationBarView extends LinearLayout {
+    private View mNavButtons;
+    private AlphaOptimizedImageButton mNotificationsButton;
+    private CarStatusBar mCarStatusBar;
+    private Context mContext;
+    private View mLockScreenButtons;
+
+    public CarNavigationBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    @Override
+    public void onFinishInflate() {
+        mNavButtons = findViewById(R.id.nav_buttons);
+        mLockScreenButtons = findViewById(R.id.lock_screen_nav_buttons);
+
+        mNotificationsButton = findViewById(R.id.notifications);
+        if (mNotificationsButton != null) {
+            mNotificationsButton.setOnClickListener(this::onNotificationsClick);
+        }
+        View mStatusIcons = findViewById(R.id.statusIcons);
+        if (mStatusIcons != null) {
+            // Attach the controllers for Status icons such as wifi and bluetooth if the standard
+            // container is in the view.
+            StatusBarIconController.DarkIconManager mDarkIconManager =
+                    new StatusBarIconController.DarkIconManager(
+                            mStatusIcons.findViewById(R.id.statusIcons));
+            mDarkIconManager.setShouldLog(true);
+            Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
+        }
+
+    }
+
+    void setStatusBar(CarStatusBar carStatusBar) {
+        mCarStatusBar = carStatusBar;
+    }
+
+    protected void onNotificationsClick(View v) {
+        mCarStatusBar.togglePanel();
+    }
+
+    /**
+     * If there are buttons declared in the layout they will be shown and the normal
+     * Nav buttons will be hidden.
+     */
+    public void showKeyguardButtons() {
+        if (mLockScreenButtons == null) {
+            return;
+        }
+        mLockScreenButtons.setVisibility(View.VISIBLE);
+        mNavButtons.setVisibility(View.GONE);
+    }
+
+    /**
+     * If there are buttons declared in the layout they will be hidden and the normal
+     * Nav buttons will be shown.
+     */
+    public void hideKeyguardButtons() {
+        if (mLockScreenButtons == null) {
+            return;
+        }
+        mNavButtons.setVisibility(View.VISIBLE);
+        mLockScreenButtons.setVisibility(View.GONE);
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java
new file mode 100644
index 0000000..e640baa
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+import java.net.URISyntaxException;
+
+/**
+ * CarNavigationButton is an image button that allows for a bit more configuration at the
+ * xml file level. This allows for more control via overlays instead of having to update
+ * code.
+ */
+public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImageButton {
+
+    private static final String TAG = "CarNavigationButton";
+    private Context mContext;
+    private String mIntent;
+    private String mLongIntent;
+    private boolean mBroadcastIntent;
+    private boolean mSelected = false;
+    private float mSelectedAlpha = 1f;
+    private float mUnselectedAlpha = 1f;
+    private int mSelectedIconResourceId;
+    private int mIconResourceId;
+
+
+    public CarNavigationButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        TypedArray typedArray = context.obtainStyledAttributes(
+                attrs, R.styleable.CarNavigationButton);
+        mIntent = typedArray.getString(R.styleable.CarNavigationButton_intent);
+        mLongIntent = typedArray.getString(R.styleable.CarNavigationButton_longIntent);
+        mBroadcastIntent = typedArray.getBoolean(R.styleable.CarNavigationButton_broadcast, false);
+        mSelectedAlpha = typedArray.getFloat(
+                R.styleable.CarNavigationButton_selectedAlpha, mSelectedAlpha);
+        mUnselectedAlpha = typedArray.getFloat(
+                R.styleable.CarNavigationButton_unselectedAlpha, mUnselectedAlpha);
+        mIconResourceId = typedArray.getResourceId(
+                com.android.internal.R.styleable.ImageView_src, 0);
+        mSelectedIconResourceId = typedArray.getResourceId(
+                R.styleable.CarNavigationButton_selectedIcon, mIconResourceId);
+    }
+
+
+    /**
+     * After the standard inflate this then adds the xml defined intents to click and long click
+     * actions if defined.
+     */
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        setScaleType(ImageView.ScaleType.CENTER);
+        setAlpha(mUnselectedAlpha);
+        try {
+            if (mIntent != null) {
+                final Intent intent = Intent.parseUri(mIntent, Intent.URI_INTENT_SCHEME);
+                setOnClickListener(v -> {
+                    try {
+                        if (mBroadcastIntent) {
+                            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+                            return;
+                        }
+                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to launch intent", e);
+                    }
+                });
+            }
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Failed to attach intent", e);
+        }
+
+        try {
+            if (mLongIntent != null) {
+                final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME);
+                setOnLongClickListener(v -> {
+                    try {
+                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to launch intent", e);
+                    }
+                    // consume event either way
+                    return true;
+                });
+            }
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Failed to attach long press intent", e);
+        }
+    }
+
+    /**
+     * @param selected true if should indicate if this is a selected state, false otherwise
+     */
+    public void setSelected(boolean selected) {
+        super.setSelected(selected);
+        mSelected = selected;
+        setAlpha(mSelected ? mSelectedAlpha : mUnselectedAlpha);
+        setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId);
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
new file mode 100644
index 0000000..2d90f8f
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.app.ActivityTaskManager;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingLog;
+import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.car.CarQSFragment;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.car.hvac.HvacController;
+import com.android.systemui.statusbar.car.hvac.TemperatureView;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Map;
+
+/**
+ * A status bar (and navigation bar) tailored for the automotive use case.
+ */
+public class CarStatusBar extends StatusBar implements
+        CarBatteryController.BatteryViewHandler {
+    private static final String TAG = "CarStatusBar";
+
+    private TaskStackListenerImpl mTaskStackListener;
+
+    private FullscreenUserSwitcher mFullscreenUserSwitcher;
+
+    private CarBatteryController mCarBatteryController;
+    private BatteryMeterView mBatteryMeterView;
+    private Drawable mNotificationPanelBackground;
+
+    private ConnectedDeviceSignalController mConnectedDeviceSignalController;
+    private ViewGroup mNavigationBarWindow;
+    private ViewGroup mLeftNavigationBarWindow;
+    private ViewGroup mRightNavigationBarWindow;
+    private CarNavigationBarView mNavigationBarView;
+    private CarNavigationBarView mLeftNavigationBarView;
+    private CarNavigationBarView mRightNavigationBarView;
+
+    private final Object mQueueLock = new Object();
+    private boolean mShowLeft;
+    private boolean mShowRight;
+    private boolean mShowBottom;
+    private CarFacetButtonController mCarFacetButtonController;
+    private ActivityManagerWrapper mActivityManagerWrapper;
+    private DeviceProvisionedController mDeviceProvisionedController;
+    private boolean mDeviceIsProvisioned = true;
+    private HvacController mHvacController;
+    private DrivingStateHelper mDrivingStateHelper;
+    private SwitchToGuestTimer mSwitchToGuestTimer;
+
+    @Override
+    public void start() {
+        super.start();
+        mTaskStackListener = new TaskStackListenerImpl();
+        mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
+        mActivityManagerWrapper.registerTaskStackListener(mTaskStackListener);
+
+        mNotificationPanel.setScrollingEnabled(true);
+
+        createBatteryController();
+        mCarBatteryController.startListening();
+
+        mHvacController.connectToCarService();
+
+        mCarFacetButtonController = Dependency.get(CarFacetButtonController.class);
+        mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
+        mDeviceIsProvisioned = mDeviceProvisionedController.isDeviceProvisioned();
+        if (!mDeviceIsProvisioned) {
+            mDeviceProvisionedController.addCallback(
+                    new DeviceProvisionedController.DeviceProvisionedListener() {
+                        @Override
+                        public void onDeviceProvisionedChanged() {
+                            mDeviceIsProvisioned =
+                                    mDeviceProvisionedController.isDeviceProvisioned();
+                            restartNavBars();
+                        }
+                    });
+        }
+
+        // Register a listener for driving state changes.
+        mDrivingStateHelper = new DrivingStateHelper(mContext, this::onDrivingStateChanged);
+        mDrivingStateHelper.connectToCarService();
+
+        mSwitchToGuestTimer = new SwitchToGuestTimer(mContext);
+    }
+
+    /**
+     * Remove all content from navbars and rebuild them. Used to allow for different nav bars
+     * before and after the device is provisioned
+     */
+    private void restartNavBars() {
+        // remove and reattach all hvac components such that we don't keep a reference to unused
+        // ui elements
+        mHvacController.removeAllComponents();
+        addTemperatureViewToController(mStatusBarWindow);
+        mCarFacetButtonController.removeAll();
+        if (mNavigationBarWindow != null) {
+            mNavigationBarWindow.removeAllViews();
+            mNavigationBarView = null;
+        }
+
+        if (mLeftNavigationBarWindow != null) {
+            mLeftNavigationBarWindow.removeAllViews();
+            mLeftNavigationBarView = null;
+        }
+
+        if (mRightNavigationBarWindow != null) {
+            mRightNavigationBarWindow.removeAllViews();
+            mRightNavigationBarView = null;
+        }
+
+        buildNavBarContent();
+    }
+
+    private void addTemperatureViewToController(View v) {
+        if (v instanceof TemperatureView) {
+            Log.d(TAG, "addTemperatureViewToController: found ");
+            mHvacController.addHvacTextView((TemperatureView) v);
+        } else if (v instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) v;
+            for (int i = 0; i < viewGroup.getChildCount(); i++) {
+                addTemperatureViewToController(viewGroup.getChildAt(i));
+            }
+        }
+    }
+
+    /**
+     * Allows for showing or hiding just the navigation bars. This is indented to be used when
+     * the full screen user selector is shown.
+     */
+    void setNavBarVisibility(@View.Visibility int visibility) {
+        if (mNavigationBarWindow != null) {
+            mNavigationBarWindow.setVisibility(visibility);
+        }
+        if (mLeftNavigationBarWindow != null) {
+            mLeftNavigationBarWindow.setVisibility(visibility);
+        }
+        if (mRightNavigationBarWindow != null) {
+            mRightNavigationBarWindow.setVisibility(visibility);
+        }
+    }
+
+
+    @Override
+    public boolean hideKeyguard() {
+        boolean result = super.hideKeyguard();
+        if (mNavigationBarView != null) {
+            mNavigationBarView.hideKeyguardButtons();
+        }
+        if (mLeftNavigationBarView != null) {
+            mLeftNavigationBarView.hideKeyguardButtons();
+        }
+        if (mRightNavigationBarView != null) {
+            mRightNavigationBarView.hideKeyguardButtons();
+        }
+        return result;
+    }
+
+
+    @Override
+    public void showKeyguard() {
+        super.showKeyguard();
+        if (mNavigationBarView != null) {
+            mNavigationBarView.showKeyguardButtons();
+        }
+        if (mLeftNavigationBarView != null) {
+            mLeftNavigationBarView.showKeyguardButtons();
+        }
+        if (mRightNavigationBarView != null) {
+            mRightNavigationBarView.showKeyguardButtons();
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mCarBatteryController.stopListening();
+        mConnectedDeviceSignalController.stopListening();
+        mActivityManagerWrapper.unregisterTaskStackListener(mTaskStackListener);
+        mDrivingStateHelper.disconnectFromCarService();
+
+        if (mNavigationBarWindow != null) {
+            mWindowManager.removeViewImmediate(mNavigationBarWindow);
+            mNavigationBarView = null;
+        }
+
+        if (mLeftNavigationBarWindow != null) {
+            mWindowManager.removeViewImmediate(mLeftNavigationBarWindow);
+            mLeftNavigationBarView = null;
+        }
+
+        if (mRightNavigationBarWindow != null) {
+            mWindowManager.removeViewImmediate(mRightNavigationBarWindow);
+            mRightNavigationBarView = null;
+        }
+        super.destroy();
+    }
+
+
+    @Override
+    protected void makeStatusBarView() {
+        super.makeStatusBarView();
+        mHvacController = Dependency.get(HvacController.class);
+
+        mNotificationPanelBackground = getDefaultWallpaper();
+        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+
+        FragmentHostManager manager = FragmentHostManager.get(mStatusBarWindow);
+        manager.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
+            mBatteryMeterView = fragment.getView().findViewById(R.id.battery);
+
+            // By default, the BatteryMeterView should not be visible. It will be toggled
+            // when a device has connected by bluetooth.
+            mBatteryMeterView.setVisibility(View.GONE);
+        });
+        addTemperatureViewToController(mStatusBarWindow);
+    }
+
+    @Override
+    protected QS createDefaultQSFragment() {
+        return new CarQSFragment();
+    }
+
+    private BatteryController createBatteryController() {
+        mCarBatteryController = new CarBatteryController(mContext);
+        mCarBatteryController.addBatteryViewHandler(this);
+        return mCarBatteryController;
+    }
+
+    @Override
+    protected void createNavigationBar() {
+        mShowBottom = mContext.getResources().getBoolean(R.bool.config_enableBottomNavigationBar);
+        mShowLeft = mContext.getResources().getBoolean(R.bool.config_enableLeftNavigationBar);
+        mShowRight = mContext.getResources().getBoolean(R.bool.config_enableRightNavigationBar);
+
+        buildNavBarWindows();
+        buildNavBarContent();
+        attachNavBarWindows();
+
+        mNavigationBarController.createNavigationBars();
+    }
+
+    private void buildNavBarContent() {
+        if (mShowBottom) {
+            buildBottomBar((mDeviceIsProvisioned) ? R.layout.car_navigation_bar :
+                    R.layout.car_navigation_bar_unprovisioned);
+        }
+
+        if (mShowLeft) {
+            buildLeft((mDeviceIsProvisioned) ? R.layout.car_left_navigation_bar :
+                    R.layout.car_left_navigation_bar_unprovisioned);
+        }
+
+        if (mShowRight) {
+            buildRight((mDeviceIsProvisioned) ? R.layout.car_right_navigation_bar :
+                    R.layout.car_right_navigation_bar_unprovisioned);
+        }
+    }
+
+    private void buildNavBarWindows() {
+        if (mShowBottom) {
+            mNavigationBarWindow = (ViewGroup) View.inflate(mContext,
+                    R.layout.navigation_bar_window, null);
+        }
+        if (mShowLeft) {
+            mLeftNavigationBarWindow = (ViewGroup) View.inflate(mContext,
+                    R.layout.navigation_bar_window, null);
+        }
+        if (mShowRight) {
+            mRightNavigationBarWindow = (ViewGroup) View.inflate(mContext,
+                    R.layout.navigation_bar_window, null);
+        }
+
+    }
+
+    private void attachNavBarWindows() {
+
+        if (mShowBottom) {
+            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
+                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                    PixelFormat.TRANSLUCENT);
+            lp.setTitle("CarNavigationBar");
+            lp.windowAnimations = 0;
+            mWindowManager.addView(mNavigationBarWindow, lp);
+        }
+        if (mShowLeft) {
+            int width = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.car_left_navigation_bar_width);
+            WindowManager.LayoutParams leftlp = new WindowManager.LayoutParams(
+                    width, LayoutParams.MATCH_PARENT,
+                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                    PixelFormat.TRANSLUCENT);
+            leftlp.setTitle("LeftCarNavigationBar");
+            leftlp.windowAnimations = 0;
+            leftlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
+            leftlp.gravity = Gravity.LEFT;
+            mWindowManager.addView(mLeftNavigationBarWindow, leftlp);
+        }
+        if (mShowRight) {
+            int width = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.car_right_navigation_bar_width);
+            WindowManager.LayoutParams rightlp = new WindowManager.LayoutParams(
+                    width, LayoutParams.MATCH_PARENT,
+                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                    PixelFormat.TRANSLUCENT);
+            rightlp.setTitle("RightCarNavigationBar");
+            rightlp.windowAnimations = 0;
+            rightlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
+            rightlp.gravity = Gravity.RIGHT;
+            mWindowManager.addView(mRightNavigationBarWindow, rightlp);
+        }
+
+    }
+
+    private void buildBottomBar(int layout) {
+        // SystemUI requires that the navigation bar view have a parent. Since the regular
+        // StatusBar inflates navigation_bar_window as this parent view, use the same view for the
+        // CarNavigationBarView.
+        View.inflate(mContext, layout, mNavigationBarWindow);
+        mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0);
+        if (mNavigationBarView == null) {
+            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
+            throw new RuntimeException("Unable to build botom nav bar due to missing layout");
+        }
+        mNavigationBarView.setStatusBar(this);
+        addTemperatureViewToController(mNavigationBarView);
+    }
+
+    private void buildLeft(int layout) {
+        View.inflate(mContext, layout, mLeftNavigationBarWindow);
+        mLeftNavigationBarView = (CarNavigationBarView) mLeftNavigationBarWindow.getChildAt(0);
+        if (mLeftNavigationBarView == null) {
+            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
+            throw new RuntimeException("Unable to build left nav bar due to missing layout");
+        }
+        mLeftNavigationBarView.setStatusBar(this);
+        addTemperatureViewToController(mLeftNavigationBarView);
+    }
+
+
+    private void buildRight(int layout) {
+        View.inflate(mContext, layout, mRightNavigationBarWindow);
+        mRightNavigationBarView = (CarNavigationBarView) mRightNavigationBarWindow.getChildAt(0);
+        if (mRightNavigationBarView == null) {
+            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
+            throw new RuntimeException("Unable to build right nav bar due to missing layout");
+        }
+        mRightNavigationBarView.setStatusBar(this);
+        addTemperatureViewToController(mRightNavigationBarView);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        //When executing dump() funciton simultaneously, we need to serialize them
+        //to get mStackScroller's position correctly.
+        synchronized (mQueueLock) {
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller));
+            pw.println("  mStackScroller: " + viewInfo(mStackScroller)
+                    + " scroll " + mStackScroller.getScrollX()
+                    + "," + mStackScroller.getScrollY());
+        }
+
+        pw.print("  mTaskStackListener=");
+        pw.println(mTaskStackListener);
+        pw.print("  mCarFacetButtonController=");
+        pw.println(mCarFacetButtonController);
+        pw.print("  mFullscreenUserSwitcher=");
+        pw.println(mFullscreenUserSwitcher);
+        pw.print("  mCarBatteryController=");
+        pw.println(mCarBatteryController);
+        pw.print("  mBatteryMeterView=");
+        pw.println(mBatteryMeterView);
+        pw.print("  mConnectedDeviceSignalController=");
+        pw.println(mConnectedDeviceSignalController);
+        pw.print("  mNavigationBarView=");
+        pw.println(mNavigationBarView);
+
+        if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
+            KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
+        }
+
+        FalsingManager.getInstance(mContext).dump(pw);
+        FalsingLog.dump(pw);
+
+        pw.println("SharedPreferences:");
+        for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
+            pw.print("  ");
+            pw.print(entry.getKey());
+            pw.print("=");
+            pw.println(entry.getValue());
+        }
+    }
+
+
+    @Override
+    public View getNavigationBarWindow() {
+        return mNavigationBarWindow;
+    }
+
+    @Override
+    protected View.OnTouchListener getStatusBarWindowTouchListener() {
+        // Usually, a touch on the background window will dismiss the notification shade. However,
+        // for the car use-case, the shade should remain unless the user switches to a different
+        // facet (e.g. phone).
+        return null;
+    }
+
+    @Override
+    public void showBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void hideBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * An implementation of TaskStackChangeListener, that listens for changes in the system
+     * task stack and notifies the navigation bar.
+     */
+    private class TaskStackListenerImpl extends TaskStackChangeListener {
+        @Override
+        public void onTaskStackChanged() {
+            try {
+                mCarFacetButtonController.taskChanged(
+                        ActivityTaskManager.getService().getAllStackInfos());
+            } catch (Exception e) {
+                Log.e(TAG, "Getting StackInfo from activity manager failed", e);
+            }
+        }
+    }
+
+    private void onDrivingStateChanged(CarDrivingStateEvent notUsed) {
+        // Check if we need to start the timer every time driving state changes.
+        startSwitchToGuestTimerIfDrivingOnKeyguard();
+    }
+
+    private void startSwitchToGuestTimerIfDrivingOnKeyguard() {
+        if (mDrivingStateHelper.isCurrentlyDriving() && mState != StatusBarState.SHADE) {
+            // We're driving while keyguard is up.
+            mSwitchToGuestTimer.start();
+        } else {
+            mSwitchToGuestTimer.cancel();
+        }
+    }
+
+    @Override
+    protected void createUserSwitcher() {
+        UserSwitcherController userSwitcherController =
+                Dependency.get(UserSwitcherController.class);
+        if (userSwitcherController.useFullscreenUserSwitcher()) {
+            mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
+                    mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub), mContext);
+        } else {
+            super.createUserSwitcher();
+        }
+    }
+
+    @Override
+    public void onStateChanged(int newState) {
+        super.onStateChanged(newState);
+
+        startSwitchToGuestTimerIfDrivingOnKeyguard();
+
+        if (mFullscreenUserSwitcher == null) {
+            return; // Not using the full screen user switcher.
+        }
+
+        if (newState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+            if (!mFullscreenUserSwitcher.isVisible()) {
+                // Current execution path continues to set state after this, thus we deffer the
+                // dismissal to the next execution cycle.
+                postDismissKeyguard(); // Dismiss the keyguard if switcher is not visible.
+            }
+        } else {
+            mFullscreenUserSwitcher.hide();
+        }
+    }
+
+    public void showUserSwitcher() {
+        if (mFullscreenUserSwitcher != null && mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
+            mFullscreenUserSwitcher.show(); // Makes the switcher visible.
+        }
+    }
+
+    public void postDismissKeyguard() {
+        mHandler.post(this::dismissKeyguard);
+    }
+
+    /**
+     * Dismisses the keyguard and shows bouncer if authentication is necessary.
+     */
+    public void dismissKeyguard() {
+        executeRunnableDismissingKeyguard(null/* runnable */, null /* cancelAction */,
+                true /* dismissShade */, true /* afterKeyguardGone */, true /* deferred */);
+    }
+
+    @Override
+    public void animateExpandNotificationsPanel() {
+        // Because space is usually constrained in the auto use-case, there should not be a
+        // pinned notification when the shade has been expanded. Ensure this by removing all heads-
+        // up notifications.
+        mHeadsUpManager.releaseAllImmediately();
+        super.animateExpandNotificationsPanel();
+    }
+
+    /**
+     * Ensures that relevant child views are appropriately recreated when the device's density
+     * changes.
+     */
+    @Override
+    public void onDensityOrFontScaleChanged() {
+        super.onDensityOrFontScaleChanged();
+        // Need to update the background on density changed in case the change was due to night
+        // mode.
+        mNotificationPanelBackground = getDefaultWallpaper();
+        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
+    }
+
+    /**
+     * Returns the {@link Drawable} that represents the wallpaper that the user has currently set.
+     */
+    private Drawable getDefaultWallpaper() {
+        return mContext.getDrawable(com.android.internal.R.drawable.default_wallpaper);
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarKeyguardViewManager.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarKeyguardViewManager.java
new file mode 100644
index 0000000..8c6b9b0
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarKeyguardViewManager.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+
+public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManager {
+
+    protected boolean mShouldHideNavBar;
+
+    public CarStatusBarKeyguardViewManager(Context context,
+            ViewMediatorCallback callback,
+            LockPatternUtils lockPatternUtils) {
+        super(context, callback, lockPatternUtils);
+        mShouldHideNavBar = context.getResources()
+                .getBoolean(R.bool.config_hideNavWhenKeyguardBouncerShown);
+    }
+
+    @Override
+    protected void updateNavigationBarVisibility(boolean navBarVisible) {
+        if (!mShouldHideNavBar) {
+            return;
+        }
+        CarStatusBar statusBar = (CarStatusBar) mStatusBar;
+        statusBar.setNavBarVisibility(navBarVisible ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Car is a multi-user system.  There's a cancel button on the bouncer that allows the user to
+     * go back to the user switcher and select another user.  Different user may have different
+     * security mode which requires bouncer container to be resized.  For this reason, the bouncer
+     * view is destroyed on cancel.
+     */
+    @Override
+    protected boolean shouldDestroyViewOnReset() {
+        return true;
+    }
+
+    /**
+     * Called when cancel button in bouncer is pressed.
+     */
+    @Override
+    public void onCancelClicked() {
+        CarStatusBar statusBar = (CarStatusBar) mStatusBar;
+        statusBar.showUserSwitcher();
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
new file mode 100644
index 0000000..3288927
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.telephony.SignalStrength;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.settingslib.graph.SignalDrawable;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ScalingDrawableWrapper;
+import com.android.systemui.statusbar.policy.BluetoothController;
+
+/**
+ * Controller that monitors signal strength for a device that is connected via bluetooth.
+ */
+public class ConnectedDeviceSignalController extends BroadcastReceiver implements
+        BluetoothController.Callback {
+    private static final String TAG = "DeviceSignalCtlr";
+
+    /**
+     * The value that indicates if a network is unavailable. This value is according ot the
+     * Bluetooth HFP 1.5 spec, which indicates this value is one of two: 0 or 1. These stand
+     * for network unavailable and available respectively.
+     */
+    private static final int NETWORK_UNAVAILABLE = 0;
+    private static final int NETWORK_UNAVAILABLE_ICON_ID = R.drawable.stat_sys_signal_null;
+
+    /**
+     * All possible signal strength icons. According to the Bluetooth HFP 1.5 specification,
+     * signal strength is indicated by a value from 1-5, where these values represent the following:
+     *
+     * <p>0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+     *
+     * <p>As a result, these are treated as an index into this array for the corresponding icon.
+     * Note that the icon is the same for 0 and 1.
+     */
+    private static final int[] SIGNAL_STRENGTH_ICONS = {
+            0,
+            0,
+            1,
+            2,
+            3,
+            4,
+    };
+
+    private static final int INVALID_SIGNAL = -1;
+
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final Context mContext;
+    private final BluetoothController mController;
+
+    private final View mSignalsView;
+    private final ImageView mNetworkSignalView;
+
+    private final float mIconScaleFactor;
+    private final SignalDrawable mSignalDrawable;
+
+    private BluetoothHeadsetClient mBluetoothHeadsetClient;
+    private final ServiceListener mHfpServiceListener = new ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = null;
+            }
+        }
+    };
+
+    public ConnectedDeviceSignalController(Context context, View signalsView) {
+        mContext = context;
+        mController = Dependency.get(BluetoothController.class);
+
+        mSignalsView = signalsView;
+        mNetworkSignalView = (ImageView)
+                mSignalsView.findViewById(R.id.connected_device_network_signal);
+
+        TypedValue typedValue = new TypedValue();
+        context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        mIconScaleFactor = typedValue.getFloat();
+        mSignalDrawable = new SignalDrawable(mNetworkSignalView.getContext());
+        mNetworkSignalView.setImageDrawable(
+                new ScalingDrawableWrapper(mSignalDrawable, mIconScaleFactor));
+
+        if (mAdapter == null) {
+            return;
+        }
+
+        mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+                BluetoothProfile.HEADSET_CLIENT);
+    }
+
+    public void startListening() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+        mContext.registerReceiver(this, filter);
+
+        mController.addCallback(this);
+    }
+
+    public void stopListening() {
+        mContext.unregisterReceiver(this);
+        mController.removeCallback(this);
+    }
+
+    @Override
+    public void onBluetoothDevicesChanged() {
+        // Nothing to do here because this Controller is not displaying a list of possible
+        // bluetooth devices.
+    }
+
+    @Override
+    public void onBluetoothStateChange(boolean enabled) {
+        if (DEBUG) {
+            Log.d(TAG, "onBluetoothStateChange(). enabled: " + enabled);
+        }
+
+        // Only need to handle the case if bluetooth has been disabled, in which case the
+        // signal indicators are hidden. If bluetooth has been enabled, then this class should
+        // receive updates to the connection state via onReceive().
+        if (!enabled) {
+            mNetworkSignalView.setVisibility(View.GONE);
+            mSignalsView.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        if (DEBUG) {
+            Log.d(TAG, "onReceive(). action: " + action);
+        }
+
+        if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+            if (DEBUG) {
+                Log.d(TAG, "Received ACTION_AG_EVENT");
+            }
+
+            processActionAgEvent(intent);
+        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (DEBUG) {
+                int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+                Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+                        + oldState + " -> " + newState);
+            }
+            BluetoothDevice device =
+                    (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+            updateViewVisibility(device, newState);
+        }
+    }
+
+    /**
+     * Processes an {@link Intent} that had an action of
+     * {@link BluetoothHeadsetClient#ACTION_AG_EVENT}.
+     */
+    private void processActionAgEvent(Intent intent) {
+        int networkStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS,
+                INVALID_SIGNAL);
+        if (networkStatus != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_STATUS: " + " " + networkStatus);
+            }
+
+            if (networkStatus == NETWORK_UNAVAILABLE) {
+                setNetworkSignalIcon(NETWORK_UNAVAILABLE_ICON_ID);
+            }
+        }
+
+        int signalStrength = intent.getIntExtra(
+                BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+        if (signalStrength != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+            }
+
+            setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+        }
+
+        int roamingStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING,
+                INVALID_SIGNAL);
+        if (roamingStatus != INVALID_SIGNAL) {
+            if (DEBUG) {
+                Log.d(TAG, "EXTRA_NETWORK_ROAMING: " + roamingStatus);
+            }
+        }
+    }
+
+    private void setNetworkSignalIcon(int level) {
+        // Setting the icon on a child view of mSignalView, so toggle this container visible.
+        mSignalsView.setVisibility(View.VISIBLE);
+
+        mSignalDrawable.setLevel(SignalDrawable.getState(level,
+                SignalStrength.NUM_SIGNAL_STRENGTH_BINS, false));
+        mNetworkSignalView.setVisibility(View.VISIBLE);
+    }
+
+    private void updateViewVisibility(BluetoothDevice device, int newState) {
+        if (newState == BluetoothProfile.STATE_CONNECTED) {
+            if (DEBUG) {
+                Log.d(TAG, "Device connected");
+            }
+
+            if (mBluetoothHeadsetClient == null || device == null) {
+                return;
+            }
+
+            // Check if battery information is available and immediately update.
+            Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+            if (featuresBundle == null) {
+                return;
+            }
+
+            int signalStrength = featuresBundle.getInt(
+                    BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL);
+            if (signalStrength != INVALID_SIGNAL) {
+                if (DEBUG) {
+                    Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength);
+                }
+
+                setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]);
+            }
+        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+            if (DEBUG) {
+                Log.d(TAG, "Device disconnected");
+            }
+
+            mNetworkSignalView.setVisibility(View.GONE);
+            mSignalsView.setVisibility(View.GONE);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/DrivingStateHelper.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/DrivingStateHelper.java
new file mode 100644
index 0000000..730c3e3
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/DrivingStateHelper.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarDrivingStateManager;
+import android.car.drivingstate.CarDrivingStateManager.CarDrivingStateEventListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Helper class for connecting to the {@link CarDrivingStateManager} and listening for driving state
+ * changes.
+ */
+public class DrivingStateHelper {
+    public static final String TAG = "DrivingStateHelper";
+
+    private final Context mContext;
+    private CarDrivingStateManager mDrivingStateManager;
+    private Car mCar;
+    private CarDrivingStateEventListener mDrivingStateHandler;
+
+    public DrivingStateHelper(Context context,
+            @NonNull CarDrivingStateEventListener drivingStateHandler) {
+        mContext = context;
+        mDrivingStateHandler = drivingStateHandler;
+    }
+
+    /**
+     * Queries {@link CarDrivingStateManager} for current driving state. Returns {@code true} if car
+     * is idling or moving, {@code false} otherwise.
+     */
+    public boolean isCurrentlyDriving() {
+        try {
+            CarDrivingStateEvent currentState = mDrivingStateManager.getCurrentCarDrivingState();
+            if (currentState != null) {
+                return currentState.eventValue == CarDrivingStateEvent.DRIVING_STATE_IDLING
+                        || currentState.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING;
+            }
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Cannot determine current driving state. Car not connected", e);
+        }
+
+        return false; // Default to false.
+    }
+
+    /**
+     * Establishes connection with the Car service.
+     */
+    public void connectToCarService() {
+        mCar = Car.createCar(mContext, mCarConnectionListener);
+        if (mCar != null) {
+            mCar.connect();
+        }
+    }
+
+    /**
+     * Disconnects from Car service and cleans up listeners.
+     */
+    public void disconnectFromCarService() {
+        if (mCar != null) {
+            mCar.disconnect();
+        }
+    }
+
+    private final ServiceConnection mCarConnectionListener =
+            new ServiceConnection() {
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    logD("Car Service connected");
+                    try {
+                        mDrivingStateManager = (CarDrivingStateManager) mCar.getCarManager(
+                                Car.CAR_DRIVING_STATE_SERVICE);
+                        if (mDrivingStateManager != null) {
+                            mDrivingStateManager.registerListener(mDrivingStateHandler);
+                            mDrivingStateHandler.onDrivingStateChanged(
+                                    mDrivingStateManager.getCurrentCarDrivingState());
+                        } else {
+                            Log.e(TAG, "CarDrivingStateService service not available");
+                        }
+                    } catch (CarNotConnectedException e) {
+                        Log.e(TAG, "Car not connected", e);
+                    }
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    destroyDrivingStateManager();
+                }
+            };
+
+    private void destroyDrivingStateManager() {
+        try {
+            if (mDrivingStateManager != null) {
+                mDrivingStateManager.unregisterListener();
+            }
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Error unregistering listeners", e);
+        }
+    }
+
+    private void logD(String message) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, message);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
new file mode 100644
index 0000000..23fe594
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewStub;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+
+import com.android.systemui.R;
+
+/**
+ * Manages the fullscreen user switcher.
+ */
+public class FullscreenUserSwitcher {
+    private final UserGridRecyclerView mUserGridView;
+    private final View mParent;
+    private final int mShortAnimDuration;
+    private final CarStatusBar mStatusBar;
+
+    public FullscreenUserSwitcher(CarStatusBar statusBar, ViewStub containerStub, Context context) {
+        mStatusBar = statusBar;
+        mParent = containerStub.inflate();
+        mParent.setVisibility(View.VISIBLE);
+        View container = mParent.findViewById(R.id.container);
+
+        // Initialize user grid.
+        mUserGridView = container.findViewById(R.id.user_grid);
+        GridLayoutManager layoutManager = new GridLayoutManager(context,
+                context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
+        mUserGridView.getRecyclerView().setLayoutManager(layoutManager);
+        mUserGridView.buildAdapter();
+        mUserGridView.setUserSelectionListener(this::onUserSelected);
+
+        // Hide the user grid by default. It will only be made visible by clicking on a cancel
+        // button in a bouncer.
+        hide();
+
+        mShortAnimDuration = container.getResources()
+                .getInteger(android.R.integer.config_shortAnimTime);
+    }
+
+    /**
+     * Makes user grid visible.
+     */
+    public void show() {
+        mUserGridView.setVisibility(View.VISIBLE);
+    }
+
+    /**
+     * Hides the user grid.
+     */
+    public void hide() {
+        mUserGridView.setVisibility(View.INVISIBLE);
+    }
+
+    /**
+     * @return {@code true} if user grid is visible, {@code false} otherwise.
+     */
+    public boolean isVisible() {
+        return mUserGridView.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * Every time user clicks on an item in the switcher, we hide the switcher, either
+     * gradually or immediately.
+     *
+     * We dismiss the entire keyguard if user clicked on the foreground user (user we're already
+     * logged in as).
+     */
+    private void onUserSelected(UserGridRecyclerView.UserRecord record) {
+        if (record.mIsForeground) {
+            hide();
+            mStatusBar.dismissKeyguard();
+            return;
+        }
+        // Switching is about to happen, since it takes time, fade out the switcher gradually.
+        fadeOut();
+    }
+
+    private void fadeOut() {
+        mUserGridView.animate()
+                .alpha(0.0f)
+                .setDuration(mShortAnimDuration)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        hide();
+                        mUserGridView.setAlpha(1.0f);
+                    }
+                });
+
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/SwitchToGuestTimer.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/SwitchToGuestTimer.java
new file mode 100644
index 0000000..0c91cba
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/SwitchToGuestTimer.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.os.CountDownTimer;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import com.android.systemui.R;
+
+/**
+ * Wrapper for a countdown timer that switches to Guest if the user has been driving with
+ * the keyguard up for configurable number of seconds.
+ */
+public class SwitchToGuestTimer {
+    private static final String TAG = "SwitchToGuestTimer";
+
+    // After how many ms CountdownTimer.onTick gets triggered.
+    private static final int COUNTDOWN_INTERVAL_MS = 1000;
+
+    private final CarUserManagerHelper mCarUserManagerHelper;
+    private final Object mTimerLock;
+    private final String mGuestName;
+    private final int mTimeoutMs;
+    private final boolean mEnabled;
+
+    @GuardedBy("mTimerLock")
+    private CountDownTimer mSwitchToGuestTimer;
+
+    public SwitchToGuestTimer(Context context) {
+        mCarUserManagerHelper = new CarUserManagerHelper(context);
+        mGuestName = context.getResources().getString(R.string.car_guest);
+        mTimeoutMs = context.getResources().getInteger(R.integer.driving_on_keyguard_timeout_ms);
+
+        // Lock prevents multiple timers being started.
+        mTimerLock = new Object();
+
+        // If milliseconds to switch is a negative number, the feature is disabled.
+        mEnabled = mTimeoutMs >= 0;
+    }
+
+    /**
+     * Starts the timer if it's not already running.
+     */
+    public void start() {
+        if (!mEnabled) {
+            logD("Switching to guest after driving on keyguard is disabled.");
+            return;
+        }
+
+        synchronized (mTimerLock) {
+            if (mSwitchToGuestTimer != null) {
+                logD("Timer is already running.");
+                return;
+            }
+
+            mSwitchToGuestTimer = new CountDownTimer(mTimeoutMs, COUNTDOWN_INTERVAL_MS) {
+                @Override
+                public void onTick(long msUntilFinished) {
+                    logD("Ms until switching to guest: " + Long.toString(msUntilFinished));
+                }
+
+                @Override
+                public void onFinish() {
+                    mCarUserManagerHelper.startGuestSession(mGuestName);
+                    cancel();
+                }
+            };
+
+            logI("Starting timer");
+            mSwitchToGuestTimer.start();
+        }
+    }
+
+    /**
+     * Cancels the running timer.
+     */
+    public void cancel() {
+        synchronized (mTimerLock) {
+            if (mSwitchToGuestTimer != null) {
+                logI("Cancelling timer");
+                mSwitchToGuestTimer.cancel();
+                mSwitchToGuestTimer = null;
+            }
+        }
+    }
+
+    private void logD(String message) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, message);
+        }
+    }
+
+    private void logI(String message) {
+        if (Log.isLoggable(TAG, Log.INFO)) {
+            Log.i(TAG, message);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java
new file mode 100644
index 0000000..fb2b57b
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import static android.content.DialogInterface.BUTTON_NEGATIVE;
+import static android.content.DialogInterface.BUTTON_POSITIVE;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.car.widget.PagedListView;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.internal.util.UserIcons;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays a GridLayout with icons for the users in the system to allow switching between users.
+ * One of the uses of this is for the lock screen in auto.
+ */
+public class UserGridRecyclerView extends PagedListView implements
+        CarUserManagerHelper.OnUsersUpdateListener {
+    private UserSelectionListener mUserSelectionListener;
+    private UserAdapter mAdapter;
+    private CarUserManagerHelper mCarUserManagerHelper;
+    private Context mContext;
+
+    public UserGridRecyclerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        mCarUserManagerHelper = new CarUserManagerHelper(mContext);
+    }
+
+    /**
+     * Register listener for any update to the users
+     */
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        mCarUserManagerHelper.registerOnUsersUpdateListener(this);
+    }
+
+    /**
+     * Unregisters listener checking for any change to the users
+     */
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mCarUserManagerHelper.unregisterOnUsersUpdateListener(this);
+    }
+
+    /**
+     * Initializes the adapter that populates the grid layout
+     *
+     * @return the adapter
+     */
+    public void buildAdapter() {
+        List<UserRecord> userRecords = createUserRecords(mCarUserManagerHelper
+                .getAllUsers());
+        mAdapter = new UserAdapter(mContext, userRecords);
+        super.setAdapter(mAdapter);
+    }
+
+    private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) {
+        List<UserRecord> userRecords = new ArrayList<>();
+
+        // If the foreground user CANNOT switch to other users, only display the foreground user.
+        if (!mCarUserManagerHelper.canForegroundUserSwitchUsers()) {
+            userRecords.add(createForegroundUserRecord());
+            return userRecords;
+        }
+
+        for (UserInfo userInfo : userInfoList) {
+            if (userInfo.isGuest()) {
+                // Don't display guests in the switcher.
+                continue;
+            }
+
+            boolean isForeground =
+                    mCarUserManagerHelper.getCurrentForegroundUserId() == userInfo.id;
+            UserRecord record = new UserRecord(userInfo, false /* isStartGuestSession */,
+                    false /* isAddUser */, isForeground);
+            userRecords.add(record);
+        }
+
+        // Add button for starting guest session.
+        userRecords.add(createStartGuestUserRecord());
+
+        // Add add user record if the foreground user can add users
+        if (mCarUserManagerHelper.canForegroundUserAddUsers()) {
+            userRecords.add(createAddUserRecord());
+        }
+
+        return userRecords;
+    }
+
+    private UserRecord createForegroundUserRecord() {
+        return new UserRecord(mCarUserManagerHelper.getCurrentForegroundUserInfo(),
+                false /* isStartGuestSession */, false /* isAddUser */, true /* isForeground */);
+    }
+
+    /**
+     * Create guest user record
+     */
+    private UserRecord createStartGuestUserRecord() {
+        UserInfo userInfo = new UserInfo();
+        userInfo.name = mContext.getString(R.string.start_guest_session);
+        return new UserRecord(userInfo, true /* isStartGuestSession */, false /* isAddUser */,
+                false /* isForeground */);
+    }
+
+    /**
+     * Create add user record
+     */
+    private UserRecord createAddUserRecord() {
+        UserInfo userInfo = new UserInfo();
+        userInfo.name = mContext.getString(R.string.car_add_user);
+        return new UserRecord(userInfo, false /* isStartGuestSession */,
+                true /* isAddUser */, false /* isForeground */);
+    }
+
+    public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
+        mUserSelectionListener = userSelectionListener;
+    }
+
+    @Override
+    public void onUsersUpdate() {
+        mAdapter.clearUsers();
+        mAdapter.updateUsers(createUserRecords(mCarUserManagerHelper.getAllUsers()));
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Adapter to populate the grid layout with the available user profiles
+     */
+    public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder>
+            implements Dialog.OnClickListener, Dialog.OnCancelListener {
+
+        private final Context mContext;
+        private List<UserRecord> mUsers;
+        private final Resources mRes;
+        private final String mGuestName;
+        private final String mNewUserName;
+        // View that holds the add user button.  Used to enable/disable the view
+        private View mAddUserView;
+        // User record for the add user.  Need to call notifyUserSelected only if the user
+        // confirms adding a user
+        private UserRecord mAddUserRecord;
+
+        public UserAdapter(Context context, List<UserRecord> users) {
+            mRes = context.getResources();
+            mContext = context;
+            updateUsers(users);
+            mGuestName = mRes.getString(R.string.car_guest);
+            mNewUserName = mRes.getString(R.string.car_new_user);
+        }
+
+        public void clearUsers() {
+            mUsers.clear();
+        }
+
+        public void updateUsers(List<UserRecord> users) {
+            mUsers = users;
+        }
+
+        @Override
+        public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(mContext)
+                    .inflate(R.layout.car_fullscreen_user_pod, parent, false);
+            view.setAlpha(1f);
+            view.bringToFront();
+            return new UserAdapterViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
+            UserRecord userRecord = mUsers.get(position);
+            RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create(mRes,
+                    getUserRecordIcon(userRecord));
+            circleIcon.setCircular(true);
+            holder.mUserAvatarImageView.setImageDrawable(circleIcon);
+            holder.mUserNameTextView.setText(userRecord.mInfo.name);
+
+            holder.mView.setOnClickListener(v -> {
+                if (userRecord == null) {
+                    return;
+                }
+
+                if (userRecord.mIsStartGuestSession) {
+                    notifyUserSelected(userRecord);
+                    mCarUserManagerHelper.startGuestSession(mGuestName);
+                    return;
+                }
+
+                // If the user wants to add a user, show dialog to confirm adding a user
+                if (userRecord.mIsAddUser) {
+                    // Disable button so it cannot be clicked multiple times
+                    mAddUserView = holder.mView;
+                    mAddUserView.setEnabled(false);
+                    mAddUserRecord = userRecord;
+
+                    handleAddUserClicked();
+                    return;
+                }
+                // If the user doesn't want to be a guest or add a user, switch to the user selected
+                notifyUserSelected(userRecord);
+                mCarUserManagerHelper.switchToUser(userRecord.mInfo);
+            });
+
+        }
+
+        private void handleAddUserClicked() {
+            if (mCarUserManagerHelper.isUserLimitReached()) {
+                mAddUserView.setEnabled(true);
+                showMaxUserLimitReachedDialog();
+            } else {
+                showConfirmAddUserDialog();
+            }
+        }
+
+        private void showMaxUserLimitReachedDialog() {
+            AlertDialog maxUsersDialog = new Builder(mContext, R.style.Theme_Car_Dark_Dialog_Alert)
+                    .setTitle(R.string.user_limit_reached_title)
+                    .setMessage(getResources().getQuantityString(
+                            R.plurals.user_limit_reached_message,
+                            mCarUserManagerHelper.getMaxSupportedRealUsers(),
+                            mCarUserManagerHelper.getMaxSupportedRealUsers()))
+                    .setPositiveButton(android.R.string.ok, null)
+                    .create();
+            // Sets window flags for the SysUI dialog
+            SystemUIDialog.applyFlags(maxUsersDialog);
+            maxUsersDialog.show();
+        }
+
+        private void showConfirmAddUserDialog() {
+            String message = mRes.getString(R.string.user_add_user_message_setup)
+                    .concat(System.getProperty("line.separator"))
+                    .concat(System.getProperty("line.separator"))
+                    .concat(mRes.getString(R.string.user_add_user_message_update));
+
+            AlertDialog addUserDialog = new Builder(mContext, R.style.Theme_Car_Dark_Dialog_Alert)
+                    .setTitle(R.string.user_add_user_title)
+                    .setMessage(message)
+                    .setNegativeButton(android.R.string.cancel, this)
+                    .setPositiveButton(android.R.string.ok, this)
+                    .setOnCancelListener(this)
+                    .create();
+            // Sets window flags for the SysUI dialog
+            SystemUIDialog.applyFlags(addUserDialog);
+            addUserDialog.show();
+        }
+
+        private void notifyUserSelected(UserRecord userRecord) {
+            // Notify the listener which user was selected
+            if (mUserSelectionListener != null) {
+                mUserSelectionListener.onUserSelected(userRecord);
+            }
+        }
+
+        private Bitmap getUserRecordIcon(UserRecord userRecord) {
+            if (userRecord.mIsStartGuestSession) {
+                return mCarUserManagerHelper.getGuestDefaultIcon();
+            }
+
+            if (userRecord.mIsAddUser) {
+                return UserIcons.convertToBitmap(mContext
+                        .getDrawable(R.drawable.car_add_circle_round));
+            }
+
+            return mCarUserManagerHelper.getUserIcon(userRecord.mInfo);
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == BUTTON_POSITIVE) {
+                notifyUserSelected(mAddUserRecord);
+                new AddNewUserTask().execute(mNewUserName);
+            } else if (which == BUTTON_NEGATIVE) {
+                // Enable the add button only if cancel
+                if (mAddUserView != null) {
+                    mAddUserView.setEnabled(true);
+                }
+            }
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            // Enable the add button again if user cancels dialog by clicking outside the dialog
+            if (mAddUserView != null) {
+                mAddUserView.setEnabled(true);
+            }
+        }
+
+        private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
+
+            @Override
+            protected UserInfo doInBackground(String... userNames) {
+                return mCarUserManagerHelper.createNewNonAdminUser(userNames[0]);
+            }
+
+            @Override
+            protected void onPreExecute() {
+            }
+
+            @Override
+            protected void onPostExecute(UserInfo user) {
+                if (user != null) {
+                    mCarUserManagerHelper.switchToUser(user);
+                }
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mUsers.size();
+        }
+
+        public class UserAdapterViewHolder extends RecyclerView.ViewHolder {
+
+            public ImageView mUserAvatarImageView;
+            public TextView mUserNameTextView;
+            public View mView;
+
+            public UserAdapterViewHolder(View view) {
+                super(view);
+                mView = view;
+                mUserAvatarImageView = (ImageView) view.findViewById(R.id.user_avatar);
+                mUserNameTextView = (TextView) view.findViewById(R.id.user_name);
+            }
+        }
+    }
+
+    /**
+     * Object wrapper class for the userInfo.  Use it to distinguish if a profile is a
+     * guest profile, add user profile, or the foreground user.
+     */
+    public static final class UserRecord {
+
+        public final UserInfo mInfo;
+        public final boolean mIsStartGuestSession;
+        public final boolean mIsAddUser;
+        public final boolean mIsForeground;
+
+        public UserRecord(UserInfo userInfo, boolean isStartGuestSession, boolean isAddUser,
+                boolean isForeground) {
+            mInfo = userInfo;
+            mIsStartGuestSession = isStartGuestSession;
+            mIsAddUser = isAddUser;
+            mIsForeground = isForeground;
+        }
+    }
+
+    /**
+     * Listener used to notify when a user has been selected
+     */
+    interface UserSelectionListener {
+
+        void onUserSelected(UserRecord record);
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/HvacController.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/HvacController.java
new file mode 100644
index 0000000..aec31ee
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/HvacController.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car.hvac;
+
+import android.car.Car;
+import android.car.hardware.CarPropertyValue;
+import android.car.hardware.hvac.CarHvacManager;
+import android.car.hardware.hvac.CarHvacManager.CarHvacEventCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Manages the connection to the Car service and delegates value changes to the registered
+ * {@link TemperatureView}s
+ */
+public class HvacController {
+
+    public static final String TAG = "HvacController";
+    public static final int BIND_TO_HVAC_RETRY_DELAY = 5000;
+
+    private Context mContext;
+    private Handler mHandler;
+    private Car mCar;
+    private CarHvacManager mHvacManager;
+    private HashMap<HvacKey, List<TemperatureView>> mTempComponents = new HashMap<>();
+    /**
+     * Callback for getting changes from {@link CarHvacManager} and setting the UI elements to
+     * match.
+     */
+    private final CarHvacEventCallback mHardwareCallback = new CarHvacEventCallback() {
+        @Override
+        public void onChangeEvent(final CarPropertyValue val) {
+            try {
+                int areaId = val.getAreaId();
+                int propertyId = val.getPropertyId();
+                List<TemperatureView> temperatureViews = mTempComponents.get(
+                        new HvacKey(propertyId, areaId));
+                if (temperatureViews != null && !temperatureViews.isEmpty()) {
+                    float value = (float) val.getValue();
+                    for (TemperatureView tempView : temperatureViews) {
+                        tempView.setTemp(value);
+                    }
+                } // else the data is not of interest
+            } catch (Exception e) {
+                // catch all so we don't take down the sysui if a new data type is
+                // introduced.
+                Log.e(TAG, "Failed handling hvac change event", e);
+            }
+        }
+
+        @Override
+        public void onErrorEvent(final int propertyId, final int zone) {
+            Log.d(TAG, "HVAC error event, propertyId: " + propertyId
+                    + " zone: " + zone);
+        }
+    };
+    /**
+     * If the connection to car service goes away then restart it.
+     */
+    private final IBinder.DeathRecipient mRestart = new IBinder.DeathRecipient() {
+        @Override
+        public void binderDied() {
+            Log.d(TAG, "Death of HVAC triggering a restart");
+            if (mCar != null) {
+                mCar.disconnect();
+            }
+            destroyHvacManager();
+            mHandler.postDelayed(() -> mCar.connect(), BIND_TO_HVAC_RETRY_DELAY);
+        }
+    };
+    /**
+     * Registers callbacks and initializes components upon connection.
+     */
+    private ServiceConnection mServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            try {
+                service.linkToDeath(mRestart, 0);
+                mHvacManager = (CarHvacManager) mCar.getCarManager(Car.HVAC_SERVICE);
+                mHvacManager.registerCallback(mHardwareCallback);
+                initComponents();
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to correctly connect to HVAC", e);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            destroyHvacManager();
+        }
+    };
+
+    public HvacController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Create connection to the Car service. Note: call backs from the Car service
+     * ({@link CarHvacManager}) will happen on the same thread this method was called from.
+     */
+    public void connectToCarService() {
+        mHandler = new Handler();
+        mCar = Car.createCar(mContext, mServiceConnection, mHandler);
+        if (mCar != null) {
+            // note: this connect call handles the retries
+            mCar.connect();
+        }
+    }
+
+    private void destroyHvacManager() {
+        if (mHvacManager != null) {
+            mHvacManager.unregisterCallback(mHardwareCallback);
+            mHvacManager = null;
+        }
+    }
+
+    /**
+     * Add component to list and initialize it if the connection is up.
+     */
+    public void addHvacTextView(TemperatureView temperatureView) {
+
+        HvacKey hvacKey = new HvacKey(temperatureView.getPropertyId(), temperatureView.getAreaId());
+        if (!mTempComponents.containsKey(hvacKey)) {
+            mTempComponents.put(hvacKey, new ArrayList<>());
+        }
+        mTempComponents.get(hvacKey).add(temperatureView);
+        initComponent(temperatureView);
+    }
+
+    private void initComponents() {
+        Iterator<Map.Entry<HvacKey, List<TemperatureView>>> iterator =
+                mTempComponents.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<HvacKey, List<TemperatureView>> next = iterator.next();
+            List<TemperatureView> temperatureViews = next.getValue();
+            for (TemperatureView view : temperatureViews) {
+                initComponent(view);
+            }
+        }
+    }
+
+    private void initComponent(TemperatureView view) {
+        int id = view.getPropertyId();
+        int zone = view.getAreaId();
+        try {
+            if (mHvacManager == null || !mHvacManager.isPropertyAvailable(id, zone)) {
+                view.setTemp(Float.NaN);
+                return;
+            }
+            view.setTemp(mHvacManager.getFloatProperty(id, zone));
+        } catch (Exception e) {
+            view.setTemp(Float.NaN);
+            Log.e(TAG, "Failed to get value from hvac service", e);
+        }
+    }
+
+    /**
+     * Removes all registered components. This is useful if you need to rebuild the UI since
+     * components self register.
+     */
+    public void removeAllComponents() {
+        mTempComponents.clear();
+    }
+
+    /**
+     * Key for storing {@link TemperatureView}s in a hash map
+     */
+    private static class HvacKey {
+
+        int mPropertyId;
+        int mAreaId;
+
+        private HvacKey(int propertyId, int areaId) {
+            mPropertyId = propertyId;
+            mAreaId = areaId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            HvacKey hvacKey = (HvacKey) o;
+            return mPropertyId == hvacKey.mPropertyId
+                    && mAreaId == hvacKey.mAreaId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPropertyId, mAreaId);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureTextView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureTextView.java
new file mode 100644
index 0000000..507c60f
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureTextView.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car.hvac;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+/**
+ * Simple text display of HVAC properties, It is designed to show temperature and is configured in
+ * the XML.
+ * XML properties:
+ * hvacPropertyId - Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
+ * hvacAreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
+ * hvacTempFormat - Example: "%.1f\u00B0" (1 decimal and the degree symbol)
+ */
+public class TemperatureTextView extends TextView implements TemperatureView {
+
+    private final int mAreaId;
+    private final int mPropertyId;
+    private final String mTempFormat;
+
+    public TemperatureTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TemperatureView);
+        mAreaId = typedArray.getInt(R.styleable.TemperatureView_hvacAreaId, -1);
+        mPropertyId = typedArray.getInt(R.styleable.TemperatureView_hvacPropertyId, -1);
+        String format = typedArray.getString(R.styleable.TemperatureView_hvacTempFormat);
+        mTempFormat = (format == null) ? "%.1f\u00B0" : format;
+    }
+
+    /**
+     * Formats the float for display
+     *
+     * @param temp - The current temp or NaN
+     */
+    @Override
+    public void setTemp(float temp) {
+        if (Float.isNaN(temp)) {
+            setText("--");
+            return;
+        }
+        setText(String.format(mTempFormat, temp));
+    }
+
+    /**
+     * @return propertiyId  Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
+     */
+    @Override
+    public int getPropertyId() {
+        return mPropertyId;
+    }
+
+    /**
+     * @return hvac AreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
+     */
+    @Override
+    public int getAreaId() {
+        return mAreaId;
+    }
+}
+
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureView.java
new file mode 100644
index 0000000..7651356
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/hvac/TemperatureView.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car.hvac;
+
+/**
+ * Interface for Views that display temperature HVAC properties
+ */
+public interface TemperatureView {
+    /**
+     * Formats the float for display
+     *
+     * @param temp - The current temp or NaN
+     */
+    void setTemp(float temp);
+
+
+    /**
+     * @return propertiyId  Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
+     */
+    int getPropertyId();
+
+    /**
+     * @return hvac AreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
+     */
+    int getAreaId();
+}