blob: be5176ca6d90931a4e73ed0541fe299b5665575b [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.cluster.osdouble;
import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
import static com.android.car.cluster.osdouble.ClusterOsDoubleApplication.TAG;
import android.car.Car;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.VehiclePropertyIds;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import androidx.activity.ComponentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import com.android.car.cluster.sensors.Sensors;
import com.android.car.cluster.view.BitmapFetcher;
import com.android.car.cluster.view.ClusterViewModel;
import com.android.car.cluster.view.ImageResolver;
import com.android.car.cluster.view.NavStateController;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
/**
* The Activity which plays the role of ClusterOs for the testing.
*/
public class ClusterOsDoubleActivity extends ComponentActivity {
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
// VehiclePropertyGroup
private static final int SYSTEM = 0x10000000;
private static final int VENDOR = 0x20000000;
private static final int MASK = 0xf0000000;
private static final int VENDOR_CLUSTER_REPORT_STATE = toVendorId(
VehiclePropertyIds.CLUSTER_REPORT_STATE);
private static final int VENDOR_CLUSTER_SWITCH_UI = toVendorId(
VehiclePropertyIds.CLUSTER_SWITCH_UI);
private static final int VENDOR_CLUSTER_NAVIGATION_STATE = toVendorId(
VehiclePropertyIds.CLUSTER_NAVIGATION_STATE);
private static final int VENDOR_CLUSTER_REQUEST_DISPLAY = toVendorId(
VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY);
private static final int VENDOR_CLUSTER_DISPLAY_STATE = toVendorId(
VehiclePropertyIds.CLUSTER_DISPLAY_STATE);
private DisplayManager mDisplayManager;
private CarPropertyManager mPropertyManager;
private SurfaceView mSurfaceView;
private Rect mBounds;
private Insets mInsets;
private VirtualDisplay mVirtualDisplay;
private ClusterViewModel mClusterViewModel;
private final ArrayMap<Sensors.Gear, View> mGearsToIcon = new ArrayMap<>();
private final ArrayList<View> mUiToButton = new ArrayList<>();
int mCurrentUi = UI_TYPE_CLUSTER_HOME;
int mTotalUiSize;
private NavStateController mNavStateController;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDisplayManager = getSystemService(DisplayManager.class);
Car.createCar(getApplicationContext(), /* handler= */ null,
Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
(car, ready) -> {
if (!ready) return;
mPropertyManager = (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE);
initClusterOsDouble();
});
View view = getLayoutInflater().inflate(R.layout.cluster_os_double_activity, null);
mSurfaceView = view.findViewById(R.id.cluster_display);
mSurfaceView.getHolder().addCallback(mSurfaceViewCallback);
setContentView(view);
registerGear(findViewById(R.id.gear_parked), Sensors.Gear.PARK);
registerGear(findViewById(R.id.gear_reverse), Sensors.Gear.REVERSE);
registerGear(findViewById(R.id.gear_neutral), Sensors.Gear.NEUTRAL);
registerGear(findViewById(R.id.gear_drive), Sensors.Gear.DRIVE);
mClusterViewModel = new ViewModelProvider(this).get(ClusterViewModel.class);
mClusterViewModel.getSensor(Sensors.SENSOR_GEAR).observe(this, this::updateSelectedGear);
registerSensor(findViewById(R.id.info_fuel), mClusterViewModel.getFuelLevel());
registerSensor(findViewById(R.id.info_speed), mClusterViewModel.getSpeed());
registerSensor(findViewById(R.id.info_range), mClusterViewModel.getRange());
registerSensor(findViewById(R.id.info_rpm), mClusterViewModel.getRPM());
// The order should be matched with ClusterHomeApplication.
registerUi(findViewById(R.id.btn_car_info));
registerUi(findViewById(R.id.btn_nav));
registerUi(findViewById(R.id.btn_music));
registerUi(findViewById(R.id.btn_phone));
BitmapFetcher bitmapFetcher = new BitmapFetcher(this);
ImageResolver imageResolver = new ImageResolver(bitmapFetcher);
mNavStateController = new NavStateController(
findViewById(R.id.navigation_state), imageResolver);
}
@Override
protected void onDestroy() {
mVirtualDisplay.release();
mVirtualDisplay = null;
super.onDestroy();
}
private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated, holder: " + holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height
+ ", format:" + format);
// Create mock unobscured area to report to navigation activity.
int obscuredWidth = (int) getResources()
.getDimension(R.dimen.speedometer_overlap_width);
int obscuredHeight = (int) getResources()
.getDimension(R.dimen.navigation_gradient_height);
mBounds = new Rect(/* left= */ 0, /* top= */ 0,
/* right= */ width, /* bottom= */ height);
// Adds some empty space in the boundary of the display to verify if mBounds works.
mBounds.inset(/* dx= */ 12, /* dy= */ 12);
mInsets = Insets.of(obscuredWidth, obscuredHeight, obscuredWidth, obscuredHeight);
if (mVirtualDisplay == null) {
mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
} else {
mVirtualDisplay.setSurface(holder.getSurface());
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from"
+ " display, surface: " + holder.getSurface());
// detaching surface is similar to turning off the display
mVirtualDisplay.setSurface(null);
}
};
private VirtualDisplay createVirtualDisplay(Surface surface, int width, int height) {
Log.i(TAG, "createVirtualDisplay, surface: " + surface + ", width: " + width
+ "x" + height);
return mDisplayManager.createVirtualDisplay(/* projection= */ null, "ClusterOsDouble-VD",
width, height, 160, surface,
VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_TRUSTED,
/* callback= */ null, /* handler= */ null, "ClusterDisplay");
}
private void initClusterOsDouble() {
mPropertyManager.registerCallback(mPropertyEventCallback,
VENDOR_CLUSTER_REPORT_STATE, CarPropertyManager.SENSOR_RATE_ONCHANGE);
mPropertyManager.registerCallback(mPropertyEventCallback,
VENDOR_CLUSTER_NAVIGATION_STATE, CarPropertyManager.SENSOR_RATE_ONCHANGE);
mPropertyManager.registerCallback(mPropertyEventCallback,
VENDOR_CLUSTER_REQUEST_DISPLAY, CarPropertyManager.SENSOR_RATE_ONCHANGE);
}
private final CarPropertyEventCallback mPropertyEventCallback = new CarPropertyEventCallback() {
@Override
public void onChangeEvent(CarPropertyValue carProp) {
int propertyId = carProp.getPropertyId();
if (propertyId == VENDOR_CLUSTER_REPORT_STATE) {
onClusterReportState((Object[]) carProp.getValue());
} else if (propertyId == VENDOR_CLUSTER_NAVIGATION_STATE) {
onClusterNavigationState((byte[]) carProp.getValue());
} else if (propertyId == VENDOR_CLUSTER_REQUEST_DISPLAY) {
onClusterRequestDisplay((Integer) carProp.getValue());
}
}
@Override
public void onErrorEvent(int propId, int zone) {
}
};
private void onClusterReportState(Object[] values) {
if (DBG) Log.d(TAG, "onClusterReportState: " + Arrays.toString(values));
// CLUSTER_REPORT_STATE should have at least 11 elements, check vehicle/2.0/types.hal.
if (values.length < 11) {
throw new IllegalArgumentException("Insufficient size of CLUSTER_REPORT_STATE");
}
// mainUI is the 10th element, refer to vehicle/2.0/types.hal.
int mainUi = (Integer) values[9];
if (mainUi >= 0 && mainUi < mTotalUiSize) {
selectUiButton(mainUi);
}
}
private void selectUiButton(int mainUi) {
for (int i = 0; i < mTotalUiSize; ++i) {
View button = mUiToButton.get(i);
button.setSelected(i == mainUi);
}
mCurrentUi = mainUi;
}
private void onClusterNavigationState(byte[] protoBytes) {
if (DBG) Log.d(TAG, "onClusterNavigationState: " + Arrays.toString(protoBytes));
try {
NavigationStateProto navState = NavigationStateProto.parseFrom(protoBytes);
mNavStateController.update(navState);
if (DBG) Log.d(TAG, "onClusterNavigationState: " + navState);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Error parsing navigation state proto", e);
}
}
private void onClusterRequestDisplay(Integer mainUi) {
if (DBG) Log.d(TAG, "onClusterRequestDisplay: " + mainUi);
sendDisplayState();
}
private static int toVendorId(int propId) {
return (propId & ~MASK) | VENDOR;
}
private <V> void registerSensor(TextView textView, LiveData<V> source) {
String emptyValue = getString(R.string.info_value_empty);
source.observe(this, value -> {
// Need to check that the text is actually different, or else
// it will generate a bunch of CONTENT_CHANGE_TYPE_TEXT accessability
// actions. This will cause cts tests to fail when they waitForIdle(),
// and the system never idles because it's constantly updating these
// TextViews
if (value != null && !value.toString().contentEquals(textView.getText())) {
textView.setText(value.toString());
}
if (value == null && !emptyValue.contentEquals(textView.getText())) {
textView.setText(emptyValue);
}
});
}
private void registerGear(View view, Sensors.Gear gear) {
mGearsToIcon.put(gear, view);
}
private void updateSelectedGear(Sensors.Gear gear) {
for (Map.Entry<Sensors.Gear, View> entry : mGearsToIcon.entrySet()) {
entry.getValue().setSelected(entry.getKey() == gear);
}
}
private void registerUi(View view) {
int currentUi = mUiToButton.size();
mUiToButton.add(view);
mTotalUiSize = mUiToButton.size();
view.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Log.d(TAG, "onTouch: " + currentUi);
switchUi(currentUi);
}
return true;
});
}
private void sendDisplayState() {
if (mBounds == null || mInsets == null) return;
mPropertyManager.setProperty(Integer[].class, VENDOR_CLUSTER_DISPLAY_STATE,
VEHICLE_AREA_TYPE_GLOBAL, new Integer[] {
1 /* Display On */,
mBounds.left, mBounds.top, mBounds.right, mBounds.bottom,
mInsets.left, mInsets.top, mInsets.right, mInsets.bottom,
UI_TYPE_CLUSTER_HOME, UI_TYPE_CLUSTER_NONE});
}
private void switchUi(int mainUi) {
mPropertyManager.setProperty(Integer.class, VENDOR_CLUSTER_SWITCH_UI,
VEHICLE_AREA_TYPE_GLOBAL, Integer.valueOf(mainUi));
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(TAG, "onKeyDown: " + keyCode);
if (keyCode == KeyEvent.KEYCODE_MENU) {
switchUi((mCurrentUi + 1) % mTotalUiSize);
return true;
}
return super.onKeyDown(keyCode, event);
}
}