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/CarSystemUIFactory.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java
index dfe5704..f57f26d 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java
@@ -28,6 +28,8 @@
import com.android.systemui.statusbar.car.hvac.HvacController;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.volume.CarVolumeDialogComponent;
+import com.android.systemui.volume.VolumeDialogComponent;
/**
* Class factory to provide car specific SystemUI components.
@@ -39,6 +41,10 @@
return new CarStatusBarKeyguardViewManager(context, viewMediatorCallback, lockPatternUtils);
}
+ public VolumeDialogComponent createVolumeDialogComponent(SystemUI systemUi, Context context) {
+ return new CarVolumeDialogComponent(systemUi, context);
+ }
+
@Override
public void injectDependencies(ArrayMap<Object, DependencyProvider> providers,
Context context) {
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java
new file mode 100644
index 0000000..0563418
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java
@@ -0,0 +1,54 @@
+/*
+ * 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.car;
+
+import android.content.Context;
+
+import com.android.systemui.statusbar.notification.NotificationData;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+public class CarNotificationEntryManager extends NotificationEntryManager {
+ public CarNotificationEntryManager(Context context) {
+ super(context);
+ }
+
+ /**
+ * Returns the
+ * {@link ExpandableNotificationRow.LongPressListener} that will
+ * be triggered when a notification card is long-pressed.
+ */
+ @Override
+ public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
+ // For the automative use case, we do not want to the user to be able to interact with
+ // a notification other than a regular click. As a result, just return null for the
+ // long click listener.
+ return null;
+ }
+
+ @Override
+ public boolean shouldHeadsUp(NotificationData.Entry entry) {
+ // 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 not pinning any
+ // notification if the shade is already opened.
+ if (!getPresenter().isPresenterFullyCollapsed()) {
+ return false;
+ }
+
+ return super.shouldHeadsUp(entry);
+ }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java
new file mode 100644
index 0000000..b74f199
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.qs.car;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.QSFooter;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.statusbar.phone.MultiUserSwitch;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+
+/**
+ * The footer view that displays below the status bar in the auto use-case. This view shows the
+ * user switcher and access to settings.
+ */
+public class CarQSFooter extends RelativeLayout implements QSFooter,
+ UserInfoController.OnUserInfoChangedListener {
+ private static final String TAG = "CarQSFooter";
+
+ private UserInfoController mUserInfoController;
+
+ private MultiUserSwitch mMultiUserSwitch;
+ private TextView mUserName;
+ private ImageView mMultiUserAvatar;
+ private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
+
+ public CarQSFooter(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMultiUserSwitch = findViewById(R.id.multi_user_switch);
+ mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
+ mUserName = findViewById(R.id.user_name);
+
+ mUserInfoController = Dependency.get(UserInfoController.class);
+
+ mMultiUserSwitch.setOnClickListener(v -> {
+ if (mUserSwitchCallback == null) {
+ Log.e(TAG, "CarQSFooter not properly set up; cannot display user switcher.");
+ return;
+ }
+
+ if (!mUserSwitchCallback.isShowing()) {
+ mUserSwitchCallback.show();
+ } else {
+ mUserSwitchCallback.hide();
+ }
+ });
+
+ findViewById(R.id.settings_button).setOnClickListener(v -> {
+ ActivityStarter activityStarter = Dependency.get(ActivityStarter.class);
+
+ if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) {
+ // If user isn't setup just unlock the device and dump them back at SUW.
+ activityStarter.postQSRunnableDismissingKeyguard(() -> { });
+ return;
+ }
+
+ activityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS),
+ true /* dismissShade */);
+ });
+ }
+
+ @Override
+ public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+ mMultiUserAvatar.setImageDrawable(picture);
+ mUserName.setText(name);
+ }
+
+ @Override
+ public void setQSPanel(@Nullable QSPanel panel) {
+ if (panel != null) {
+ mMultiUserSwitch.setQsPanel(panel);
+ }
+ }
+
+ public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
+ mUserSwitchCallback = callback;
+ }
+
+ @Override
+ public void setListening(boolean listening) {
+ if (listening) {
+ mUserInfoController.addCallback(this);
+ } else {
+ mUserInfoController.removeCallback(this);
+ }
+ }
+
+ @Override
+ public void setExpandClickListener(OnClickListener onClickListener) {
+ // No view that should expand/collapse the quick settings.
+ }
+
+ @Override
+ public void setExpanded(boolean expanded) {
+ // Do nothing because the quick settings cannot be expanded.
+ }
+
+ @Override
+ public void setExpansion(float expansion) {
+ // Do nothing because the quick settings cannot be expanded.
+ }
+
+ @Override
+ public void setKeyguardShowing(boolean keyguardShowing) {
+ // Do nothing because the footer will not be shown when the keyguard is up.
+ }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java
new file mode 100644
index 0000000..41c37d3
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java
@@ -0,0 +1,279 @@
+/*
+ * 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.qs.car;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.GridLayoutManager;
+
+import com.android.systemui.R;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.QSFooter;
+import com.android.systemui.statusbar.car.UserGridRecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A quick settings fragment for the car. For auto, there is no row for quick settings or ability
+ * to expand the quick settings panel. Instead, the only thing is that displayed is the
+ * status bar, and a static row with access to the user switcher and settings.
+ */
+public class CarQSFragment extends Fragment implements QS {
+ private View mHeader;
+ private View mUserSwitcherContainer;
+ private CarQSFooter mFooter;
+ private View mFooterUserName;
+ private View mFooterExpandIcon;
+ private UserGridRecyclerView mUserGridView;
+ private AnimatorSet mAnimatorSet;
+ private UserSwitchCallback mUserSwitchCallback;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.car_qs_panel, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mHeader = view.findViewById(R.id.header);
+ mFooter = view.findViewById(R.id.qs_footer);
+ mFooterUserName = mFooter.findViewById(R.id.user_name);
+ mFooterExpandIcon = mFooter.findViewById(R.id.user_switch_expand_icon);
+
+ mUserSwitcherContainer = view.findViewById(R.id.user_switcher_container);
+
+ updateUserSwitcherHeight(0);
+
+ Context context = getContext();
+ mUserGridView = mUserSwitcherContainer.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();
+
+ mUserSwitchCallback = new UserSwitchCallback();
+ mFooter.setUserSwitchCallback(mUserSwitchCallback);
+ }
+
+ @Override
+ public void hideImmediately() {
+ getView().setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ public void setQsExpansion(float qsExpansionFraction, float headerTranslation) {
+ // If the header is to be completed translated down, then set it to be visible.
+ getView().setVisibility(headerTranslation == 0 ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ public View getHeader() {
+ return mHeader;
+ }
+
+ @VisibleForTesting
+ QSFooter getFooter() {
+ return mFooter;
+ }
+
+ @Override
+ public void setHeaderListening(boolean listening) {
+ mFooter.setListening(listening);
+ }
+
+ @Override
+ public void setListening(boolean listening) {
+ mFooter.setListening(listening);
+ }
+
+ @Override
+ public int getQsMinExpansionHeight() {
+ return getView().getHeight();
+ }
+
+ @Override
+ public int getDesiredHeight() {
+ return getView().getHeight();
+ }
+
+ @Override
+ public void setPanelView(HeightListener notificationPanelView) {
+ // No quick settings panel.
+ }
+
+ @Override
+ public void setHeightOverride(int desiredHeight) {
+ // No ability to expand quick settings.
+ }
+
+ @Override
+ public void setHeaderClickable(boolean qsExpansionEnabled) {
+ // Usually this sets the expand button to be clickable, but there is no quick settings to
+ // expand.
+ }
+
+ @Override
+ public boolean isCustomizing() {
+ // No ability to customize the quick settings.
+ return false;
+ }
+
+ @Override
+ public void setOverscrolling(boolean overscrolling) {
+ // No overscrolling to reveal quick settings.
+ }
+
+ @Override
+ public void setExpanded(boolean qsExpanded) {
+ // No quick settings to expand
+ }
+
+ @Override
+ public boolean isShowingDetail() {
+ // No detail panel to close.
+ return false;
+ }
+
+ @Override
+ public void closeDetail() {
+ // No detail panel to close.
+ }
+
+ @Override
+ public void setKeyguardShowing(boolean keyguardShowing) {
+ // No keyguard to show.
+ }
+
+ @Override
+ public void animateHeaderSlidingIn(long delay) {
+ // No header to animate.
+ }
+
+ @Override
+ public void animateHeaderSlidingOut() {
+ // No header to animate.
+ }
+
+ @Override
+ public void notifyCustomizeChanged() {
+ // There is no ability to customize quick settings.
+ }
+
+ @Override
+ public void setContainer(ViewGroup container) {
+ // No quick settings, so no container to set.
+ }
+
+ @Override
+ public void setExpandClickListener(OnClickListener onClickListener) {
+ // No ability to expand the quick settings.
+ }
+
+ public class UserSwitchCallback {
+ private boolean mShowing;
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ public void show() {
+ mShowing = true;
+ animateHeightChange(true /* opening */);
+ }
+
+ public void hide() {
+ mShowing = false;
+ animateHeightChange(false /* opening */);
+ }
+ }
+
+ private void updateUserSwitcherHeight(int height) {
+ ViewGroup.LayoutParams layoutParams = mUserSwitcherContainer.getLayoutParams();
+ layoutParams.height = height;
+ mUserSwitcherContainer.requestLayout();
+ }
+
+ private void animateHeightChange(boolean opening) {
+ // Animation in progress; cancel it to avoid contention.
+ if (mAnimatorSet != null) {
+ mAnimatorSet.cancel();
+ }
+
+ List<Animator> allAnimators = new ArrayList<>();
+ ValueAnimator heightAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_animation
+ : R.anim.car_user_switcher_close_animation);
+ heightAnimator.addUpdateListener(valueAnimator -> {
+ updateUserSwitcherHeight((Integer) valueAnimator.getAnimatedValue());
+ });
+ allAnimators.add(heightAnimator);
+
+ Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_name_animation
+ : R.anim.car_user_switcher_close_name_animation);
+ nameAnimator.setTarget(mFooterUserName);
+ allAnimators.add(nameAnimator);
+
+ Animator iconAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_icon_animation
+ : R.anim.car_user_switcher_close_icon_animation);
+ iconAnimator.setTarget(mFooterExpandIcon);
+ allAnimators.add(iconAnimator);
+
+ mAnimatorSet = new AnimatorSet();
+ mAnimatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimatorSet = null;
+ }
+ });
+ mAnimatorSet.playTogether(allAnimators.toArray(new Animator[0]));
+
+ // Setup all values to the start values in the animations, since there are delays, but need
+ // to have all values start at the beginning.
+ setupInitialValues(mAnimatorSet);
+
+ mAnimatorSet.start();
+ }
+
+ private void setupInitialValues(Animator anim) {
+ if (anim instanceof AnimatorSet) {
+ for (Animator a : ((AnimatorSet) anim).getChildAnimations()) {
+ setupInitialValues(a);
+ }
+ } else if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentFraction(0.0f);
+ }
+ }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java
new file mode 100644
index 0000000..d5dd3c3
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java
@@ -0,0 +1,64 @@
+/*
+ * 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.qs.car;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.IdRes;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.BatteryMeterView;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
+
+/**
+ * A view that forms the header of the notification panel. This view will ensure that any
+ * status icons that are displayed are tinted accordingly to the current theme.
+ */
+public class CarStatusBarHeader extends LinearLayout {
+ public CarStatusBarHeader(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // Set the light/dark theming on the header status UI to match the current theme.
+ int colorForeground = Utils.getColorAttrDefaultColor(getContext(),
+ android.R.attr.colorForeground);
+ float intensity = colorForeground == Color.WHITE ? 0f : 1f;
+ Rect tintArea = new Rect(0, 0, 0, 0);
+
+ applyDarkness(R.id.battery, tintArea, intensity, colorForeground);
+ applyDarkness(R.id.clock, tintArea, intensity, colorForeground);
+
+ ((BatteryMeterView) findViewById(R.id.battery)).setForceShowPercent(true);
+ }
+
+ private void applyDarkness(@IdRes int id, Rect tintArea, float intensity, int color) {
+ View v = findViewById(id);
+ if (v instanceof DarkIconDispatcher.DarkReceiver) {
+ ((DarkIconDispatcher.DarkReceiver) v).onDarkChanged(tintArea, intensity, color);
+ }
+ }
+}
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();
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java
new file mode 100644
index 0000000..71cc19b
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogComponent.java
@@ -0,0 +1,36 @@
+/*
+ * 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.volume;
+
+import android.content.Context;
+
+import com.android.systemui.SystemUI;
+import com.android.systemui.plugins.VolumeDialog;
+
+/**
+ * Allows for adding car specific dialog when the volume dialog is created.
+ */
+public class CarVolumeDialogComponent extends VolumeDialogComponent {
+
+ public CarVolumeDialogComponent(SystemUI sysui, Context context) {
+ super(sysui, context);
+ }
+
+ protected VolumeDialog createDefault() {
+ return new CarVolumeDialogImpl(mContext);
+ }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
new file mode 100644
index 0000000..12df263
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
@@ -0,0 +1,602 @@
+/*
+ * 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.volume;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorSet;
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.app.Dialog;
+import android.app.KeyguardManager;
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.media.CarAudioManager;
+import android.car.media.ICarVolumeCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.ServiceConnection;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemAdapter.BackgroundStyle;
+import androidx.car.widget.ListItemProvider.ListProvider;
+import androidx.car.widget.PagedListView;
+import androidx.car.widget.SeekbarListItem;
+
+import com.android.systemui.R;
+import com.android.systemui.plugins.VolumeDialog;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Car version of the volume dialog.
+ *
+ * Methods ending in "H" must be called on the (ui) handler.
+ */
+public class CarVolumeDialogImpl implements VolumeDialog {
+
+ private static final String TAG = Util.logTag(CarVolumeDialogImpl.class);
+
+ private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems";
+ private static final String XML_TAG_VOLUME_ITEM = "item";
+ private static final int HOVERING_TIMEOUT = 16000;
+ private static final int NORMAL_TIMEOUT = 3000;
+ private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250;
+ private static final int DISMISS_DELAY_IN_MILLIS = 50;
+ private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100;
+
+ private final Context mContext;
+ private final H mHandler = new H();
+ // All the volume items.
+ private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>();
+ // Available volume items in car audio manager.
+ private final List<VolumeItem> mAvailableVolumeItems = new ArrayList<>();
+ // Volume items in the PagedListView.
+ private final List<ListItem> mVolumeLineItems = new ArrayList<>();
+ private final KeyguardManager mKeyguard;
+ private Window mWindow;
+ private CustomDialog mDialog;
+ private PagedListView mListView;
+ private ListItemAdapter mPagedListAdapter;
+ private Car mCar;
+ private CarAudioManager mCarAudioManager;
+ private final ICarVolumeCallback mVolumeChangeCallback = new ICarVolumeCallback.Stub() {
+ @Override
+ public void onGroupVolumeChanged(int groupId, int flags) {
+ VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+ int value = getSeekbarValue(mCarAudioManager, groupId);
+ // Do not update the progress if it is the same as before. When car audio manager sets
+ // its group volume caused by the seekbar progress changed, it also triggers this
+ // callback. Updating the seekbar at the same time could block the continuous seeking.
+ if (value != volumeItem.progress) {
+ volumeItem.listItem.setProgress(value);
+ volumeItem.progress = value;
+ }
+ if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
+ mHandler.obtainMessage(H.SHOW, Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onMasterMuteChanged(int flags) {
+ // ignored
+ }
+ };
+ private boolean mHovering;
+ private boolean mShowing;
+ private boolean mExpanded;
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ try {
+ mExpanded = false;
+ mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+ int volumeGroupCount = mCarAudioManager.getVolumeGroupCount();
+ // Populates volume slider items from volume groups to UI.
+ for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
+ VolumeItem volumeItem = getVolumeItemForUsages(
+ mCarAudioManager.getUsagesForVolumeGroupId(groupId));
+ mAvailableVolumeItems.add(volumeItem);
+ // The first one is the default item.
+ if (groupId == 0) {
+ volumeItem.defaultItem = true;
+ addSeekbarListItem(volumeItem, groupId,
+ R.drawable.car_ic_keyboard_arrow_down,
+ new ExpandIconListener());
+ }
+ }
+
+ // If list is already initiated, update its content.
+ if (mPagedListAdapter != null) {
+ mPagedListAdapter.notifyDataSetChanged();
+ }
+ mCarAudioManager.registerVolumeCallback(mVolumeChangeCallback.asBinder());
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ }
+
+ /**
+ * This does not get called when service is properly disconnected.
+ * So we need to also handle cleanups in destroy().
+ */
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ cleanupAudioManager();
+ }
+ };
+
+ public CarVolumeDialogImpl(Context context) {
+ mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
+ mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ mCar = Car.createCar(mContext, mServiceConnection);
+ }
+
+ private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
+ try {
+ return carAudioManager.getGroupVolume(volumeGroupId);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ return 0;
+ }
+
+ private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
+ try {
+ return carAudioManager.getGroupMaxVolume(volumeGroupId);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Build the volume window and connect to the CarService which registers with car audio
+ * manager.
+ */
+ @Override
+ public void init(int windowType, Callback callback) {
+ initDialog();
+
+ mCar.connect();
+ }
+
+ @Override
+ public void destroy() {
+ mHandler.removeCallbacksAndMessages(null);
+
+ cleanupAudioManager();
+ // unregisterVolumeCallback is not being called when disconnect car, so we manually cleanup
+ // audio manager beforehand.
+ mCar.disconnect();
+ }
+
+ private void initDialog() {
+ loadAudioUsageItems();
+ mVolumeLineItems.clear();
+ mDialog = new CustomDialog(mContext);
+
+ mHovering = false;
+ mShowing = false;
+ mExpanded = false;
+ mWindow = mDialog.getWindow();
+ mWindow.requestFeature(Window.FEATURE_NO_TITLE);
+ mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+ mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+ mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
+ mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
+ final WindowManager.LayoutParams lp = mWindow.getAttributes();
+ lp.format = PixelFormat.TRANSLUCENT;
+ lp.setTitle(VolumeDialogImpl.class.getSimpleName());
+ lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
+ lp.windowAnimations = -1;
+ mWindow.setAttributes(lp);
+ mWindow.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mDialog.setCanceledOnTouchOutside(true);
+ mDialog.setContentView(R.layout.car_volume_dialog);
+ mDialog.setOnShowListener(dialog -> {
+ mListView.setTranslationY(-mListView.getHeight());
+ mListView.setAlpha(0);
+ mListView.animate()
+ .alpha(1)
+ .translationY(0)
+ .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
+ .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
+ .start();
+ });
+ mListView = (PagedListView) mWindow.findViewById(R.id.volume_list);
+ mListView.setOnHoverListener((v, event) -> {
+ int action = event.getActionMasked();
+ mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
+ || (action == MotionEvent.ACTION_HOVER_MOVE);
+ rescheduleTimeoutH();
+ return true;
+ });
+
+ mPagedListAdapter = new ListItemAdapter(mContext, new ListProvider(mVolumeLineItems),
+ BackgroundStyle.PANEL);
+ mListView.setAdapter(mPagedListAdapter);
+ mListView.setMaxPages(PagedListView.UNLIMITED_PAGES);
+ }
+
+
+ private void showH(int reason) {
+ if (D.BUG) {
+ Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
+ }
+
+ mHandler.removeMessages(H.SHOW);
+ mHandler.removeMessages(H.DISMISS);
+ rescheduleTimeoutH();
+ // Refresh the data set before showing.
+ mPagedListAdapter.notifyDataSetChanged();
+ if (mShowing) {
+ return;
+ }
+ mShowing = true;
+
+ mDialog.show();
+ Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
+ }
+
+ private void rescheduleTimeoutH() {
+ mHandler.removeMessages(H.DISMISS);
+ final int timeout = computeTimeoutH();
+ mHandler.sendMessageDelayed(mHandler
+ .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT), timeout);
+
+ if (D.BUG) {
+ Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
+ }
+ }
+
+ private int computeTimeoutH() {
+ return mHovering ? HOVERING_TIMEOUT : NORMAL_TIMEOUT;
+ }
+
+ private void dismissH(int reason) {
+ if (D.BUG) {
+ Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]);
+ }
+
+ mHandler.removeMessages(H.DISMISS);
+ mHandler.removeMessages(H.SHOW);
+ if (!mShowing) {
+ return;
+ }
+
+ mListView.animate().cancel();
+
+ mListView.setTranslationY(0);
+ mListView.setAlpha(1);
+ mListView.animate()
+ .alpha(0)
+ .translationY(-mListView.getHeight())
+ .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
+ .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
+ .withEndAction(() -> mHandler.postDelayed(() -> {
+ if (D.BUG) {
+ Log.d(TAG, "mDialog.dismiss()");
+ }
+ mDialog.dismiss();
+ mShowing = false;
+ }, DISMISS_DELAY_IN_MILLIS))
+ .start();
+
+ Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason);
+ }
+
+ private void loadAudioUsageItems() {
+ try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ // Traverse to the first start tag
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && type != XmlResourceParser.START_TAG) {
+ // Do Nothing (moving parser to start element)
+ }
+
+ if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) {
+ throw new RuntimeException("Meta-data does not start with carVolumeItems tag");
+ }
+ int outerDepth = parser.getDepth();
+ int rank = 0;
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlResourceParser.END_TAG) {
+ continue;
+ }
+ if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) {
+ TypedArray item = mContext.getResources().obtainAttributes(
+ attrs, R.styleable.carVolumeItems_item);
+ int usage = item.getInt(R.styleable.carVolumeItems_item_usage, -1);
+ if (usage >= 0) {
+ VolumeItem volumeItem = new VolumeItem();
+ volumeItem.rank = rank;
+ volumeItem.icon = item.getResourceId(R.styleable.carVolumeItems_item_icon,
+ 0);
+ mVolumeItems.put(usage, volumeItem);
+ rank++;
+ }
+ item.recycle();
+ }
+ }
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Error parsing volume groups configuration", e);
+ }
+ }
+
+ private VolumeItem getVolumeItemForUsages(int[] usages) {
+ int rank = Integer.MAX_VALUE;
+ VolumeItem result = null;
+ for (int usage : usages) {
+ VolumeItem volumeItem = mVolumeItems.get(usage);
+ if (volumeItem.rank < rank) {
+ rank = volumeItem.rank;
+ result = volumeItem;
+ }
+ }
+ return result;
+ }
+
+ private SeekbarListItem addSeekbarListItem(VolumeItem volumeItem,
+ int volumeGroupId,
+ int supplementalIconId,
+ @Nullable View.OnClickListener supplementalIconOnClickListener) {
+ SeekbarListItem listItem = new SeekbarListItem(mContext);
+ listItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeGroupId));
+ int color = mContext.getResources().getColor(R.color.car_volume_dialog_tint);
+ int progress = getSeekbarValue(mCarAudioManager, volumeGroupId);
+ listItem.setProgress(progress);
+ listItem.setOnSeekBarChangeListener(new CarVolumeDialogImpl
+ .VolumeSeekBarChangeListener(volumeGroupId, mCarAudioManager));
+ Drawable primaryIcon = mContext.getResources().getDrawable(volumeItem.icon);
+ primaryIcon.mutate().setTint(color);
+ listItem.setPrimaryActionIcon(primaryIcon);
+ if (supplementalIconId != 0) {
+ Drawable supplementalIcon = mContext.getResources().getDrawable(supplementalIconId);
+ supplementalIcon.mutate().setTint(color);
+ listItem.setSupplementalIcon(supplementalIcon, true);
+ listItem.setSupplementalIconListener(supplementalIconOnClickListener);
+ } else {
+ listItem.setSupplementalEmptyIcon(true);
+ listItem.setSupplementalIconListener(null);
+ }
+
+ mVolumeLineItems.add(listItem);
+ volumeItem.listItem = listItem;
+ volumeItem.progress = progress;
+ return listItem;
+ }
+
+ private VolumeItem findVolumeItem(SeekbarListItem targetItem) {
+ for (int i = 0; i < mVolumeItems.size(); ++i) {
+ VolumeItem volumeItem = mVolumeItems.valueAt(i);
+ if (volumeItem.listItem == targetItem) {
+ return volumeItem;
+ }
+ }
+ return null;
+ }
+
+ private void cleanupAudioManager() {
+ try {
+ mCarAudioManager.unregisterVolumeCallback(mVolumeChangeCallback.asBinder());
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ mVolumeLineItems.clear();
+ mCarAudioManager = null;
+ }
+
+ /**
+ * Wrapper class which contains information of each volume group.
+ */
+ private static class VolumeItem {
+
+ private int rank;
+ private boolean defaultItem = false;
+ private @DrawableRes int icon;
+ private SeekbarListItem listItem;
+ private int progress;
+ }
+
+ private final class H extends Handler {
+
+ private static final int SHOW = 1;
+ private static final int DISMISS = 2;
+
+ private H() {
+ super(Looper.getMainLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW:
+ showH(msg.arg1);
+ break;
+ case DISMISS:
+ dismissH(msg.arg1);
+ break;
+ default:
+ }
+ }
+ }
+
+ private final class CustomDialog extends Dialog implements DialogInterface {
+
+ private CustomDialog(Context context) {
+ super(context, com.android.systemui.R.style.qs_theme);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ rescheduleTimeoutH();
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ protected void onStart() {
+ super.setCanceledOnTouchOutside(true);
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (isShowing()) {
+ if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ mHandler.obtainMessage(
+ H.DISMISS, Events.DISMISS_REASON_TOUCH_OUTSIDE).sendToTarget();
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private final class ExpandIconListener implements View.OnClickListener {
+
+ @Override
+ public void onClick(final View v) {
+ mExpanded = !mExpanded;
+ Animator inAnimator;
+ if (mExpanded) {
+ for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
+ // Adding the items which are not coming from the default item.
+ VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+ if (volumeItem.defaultItem) {
+ // Set progress here due to the progress of seekbar may not be updated.
+ volumeItem.listItem.setProgress(volumeItem.progress);
+ } else {
+ addSeekbarListItem(volumeItem, groupId, 0, null);
+ }
+ }
+ inAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_in_rotate_up);
+ } else {
+ // Only keeping the default stream if it is not expended.
+ Iterator itr = mVolumeLineItems.iterator();
+ while (itr.hasNext()) {
+ SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
+ VolumeItem volumeItem = findVolumeItem(seekbarListItem);
+ if (!volumeItem.defaultItem) {
+ itr.remove();
+ } else {
+ // Set progress here due to the progress of seekbar may not be updated.
+ seekbarListItem.setProgress(volumeItem.progress);
+ }
+ }
+ inAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_in_rotate_down);
+ }
+
+ Animator outAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_out);
+ inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
+ AnimatorSet animators = new AnimatorSet();
+ animators.playTogether(outAnimator, inAnimator);
+ animators.setTarget(v);
+ animators.start();
+ mPagedListAdapter.notifyDataSetChanged();
+ }
+ }
+
+ private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
+
+ private final int mVolumeGroupId;
+ private final CarAudioManager mCarAudioManager;
+
+ private VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager) {
+ mVolumeGroupId = volumeGroupId;
+ mCarAudioManager = carAudioManager;
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (!fromUser) {
+ // For instance, if this event is originated from AudioService,
+ // we can ignore it as it has already been handled and doesn't need to be
+ // sent back down again.
+ return;
+ }
+ try {
+ if (mCarAudioManager == null) {
+ Log.w(TAG, "Ignoring volume change event because the car isn't connected");
+ return;
+ }
+ mAvailableVolumeItems.get(mVolumeGroupId).progress = progress;
+ mCarAudioManager.setGroupVolume(mVolumeGroupId, progress, 0);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ }
+}