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();
+}