1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.cluster.osdouble; 18 19 import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; 20 import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_HOME; 21 import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE; 22 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; 23 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; 24 25 import static com.android.car.cluster.osdouble.ClusterOsDoubleApplication.TAG; 26 27 import android.car.Car; 28 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 29 import android.car.VehiclePropertyIds; 30 import android.car.hardware.CarPropertyValue; 31 import android.car.hardware.property.CarPropertyManager; 32 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback; 33 import android.graphics.Insets; 34 import android.graphics.Rect; 35 import android.hardware.display.DisplayManager; 36 import android.hardware.display.VirtualDisplay; 37 import android.os.Bundle; 38 import android.util.ArrayMap; 39 import android.util.Log; 40 import android.view.KeyEvent; 41 import android.view.MotionEvent; 42 import android.view.Surface; 43 import android.view.SurfaceHolder; 44 import android.view.SurfaceView; 45 import android.view.View; 46 import android.widget.TextView; 47 48 import androidx.activity.ComponentActivity; 49 import androidx.lifecycle.LiveData; 50 import androidx.lifecycle.ViewModelProvider; 51 52 import com.android.car.cluster.sensors.Sensors; 53 import com.android.car.cluster.view.BitmapFetcher; 54 import com.android.car.cluster.view.ClusterViewModel; 55 import com.android.car.cluster.view.ImageResolver; 56 import com.android.car.cluster.view.NavStateController; 57 58 import com.google.protobuf.InvalidProtocolBufferException; 59 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.Map; 63 64 /** 65 * The Activity which plays the role of ClusterOs for the testing. 66 */ 67 public class ClusterOsDoubleActivity extends ComponentActivity { 68 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 69 70 // VehiclePropertyGroup 71 private static final int SYSTEM = 0x10000000; 72 private static final int VENDOR = 0x20000000; 73 private static final int MASK = 0xf0000000; 74 75 private static final int VENDOR_CLUSTER_REPORT_STATE = toVendorId( 76 VehiclePropertyIds.CLUSTER_REPORT_STATE); 77 private static final int VENDOR_CLUSTER_SWITCH_UI = toVendorId( 78 VehiclePropertyIds.CLUSTER_SWITCH_UI); 79 private static final int VENDOR_CLUSTER_NAVIGATION_STATE = toVendorId( 80 VehiclePropertyIds.CLUSTER_NAVIGATION_STATE); 81 private static final int VENDOR_CLUSTER_REQUEST_DISPLAY = toVendorId( 82 VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY); 83 private static final int VENDOR_CLUSTER_DISPLAY_STATE = toVendorId( 84 VehiclePropertyIds.CLUSTER_DISPLAY_STATE); 85 86 private DisplayManager mDisplayManager; 87 private CarPropertyManager mPropertyManager; 88 89 private SurfaceView mSurfaceView; 90 private Rect mBounds; 91 private Insets mInsets; 92 private VirtualDisplay mVirtualDisplay; 93 94 private ClusterViewModel mClusterViewModel; 95 private final ArrayMap<Sensors.Gear, View> mGearsToIcon = new ArrayMap<>(); 96 private final ArrayList<View> mUiToButton = new ArrayList<>(); 97 int mCurrentUi = UI_TYPE_CLUSTER_HOME; 98 int mTotalUiSize; 99 100 private NavStateController mNavStateController; 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 super.onCreate(savedInstanceState); 105 106 mDisplayManager = getSystemService(DisplayManager.class); 107 108 Car.createCar(getApplicationContext(), /* handler= */ null, 109 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 110 (car, ready) -> { 111 if (!ready) return; 112 mPropertyManager = (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE); 113 initClusterOsDouble(); 114 }); 115 116 View view = getLayoutInflater().inflate(R.layout.cluster_os_double_activity, null); 117 mSurfaceView = view.findViewById(R.id.cluster_display); 118 mSurfaceView.getHolder().addCallback(mSurfaceViewCallback); 119 setContentView(view); 120 121 registerGear(findViewById(R.id.gear_parked), Sensors.Gear.PARK); 122 registerGear(findViewById(R.id.gear_reverse), Sensors.Gear.REVERSE); 123 registerGear(findViewById(R.id.gear_neutral), Sensors.Gear.NEUTRAL); 124 registerGear(findViewById(R.id.gear_drive), Sensors.Gear.DRIVE); 125 126 mClusterViewModel = new ViewModelProvider(this).get(ClusterViewModel.class); 127 mClusterViewModel.getSensor(Sensors.SENSOR_GEAR).observe(this, this::updateSelectedGear); 128 129 registerSensor(findViewById(R.id.info_fuel), mClusterViewModel.getFuelLevel()); 130 registerSensor(findViewById(R.id.info_speed), mClusterViewModel.getSpeed()); 131 registerSensor(findViewById(R.id.info_range), mClusterViewModel.getRange()); 132 registerSensor(findViewById(R.id.info_rpm), mClusterViewModel.getRPM()); 133 134 // The order should be matched with ClusterHomeApplication. 135 registerUi(findViewById(R.id.btn_car_info)); 136 registerUi(findViewById(R.id.btn_nav)); 137 registerUi(findViewById(R.id.btn_music)); 138 registerUi(findViewById(R.id.btn_phone)); 139 140 BitmapFetcher bitmapFetcher = new BitmapFetcher(this); 141 ImageResolver imageResolver = new ImageResolver(bitmapFetcher); 142 mNavStateController = new NavStateController( 143 findViewById(R.id.navigation_state), imageResolver); 144 } 145 146 @Override onDestroy()147 protected void onDestroy() { 148 if (mVirtualDisplay != null) { 149 mVirtualDisplay.release(); 150 mVirtualDisplay = null; 151 } 152 super.onDestroy(); 153 } 154 155 private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() { 156 @Override 157 public void surfaceCreated(SurfaceHolder holder) { 158 Log.i(TAG, "surfaceCreated, holder: " + holder); 159 } 160 161 @Override 162 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 163 Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height 164 + ", format:" + format); 165 166 // Create mock unobscured area to report to navigation activity. 167 int obscuredWidth = (int) getResources() 168 .getDimension(R.dimen.speedometer_overlap_width); 169 int obscuredHeight = (int) getResources() 170 .getDimension(R.dimen.navigation_gradient_height); 171 mBounds = new Rect(/* left= */ 0, /* top= */ 0, 172 /* right= */ width, /* bottom= */ height); 173 // Adds some empty space in the boundary of the display to verify if mBounds works. 174 mBounds.inset(/* dx= */ 12, /* dy= */ 12); 175 mInsets = Insets.of(obscuredWidth, obscuredHeight, obscuredWidth, obscuredHeight); 176 if (mVirtualDisplay == null) { 177 mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); 178 } else { 179 mVirtualDisplay.setSurface(holder.getSurface()); 180 } 181 } 182 183 @Override 184 public void surfaceDestroyed(SurfaceHolder holder) { 185 Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from" 186 + " display, surface: " + holder.getSurface()); 187 // detaching surface is similar to turning off the display 188 mVirtualDisplay.setSurface(null); 189 } 190 }; 191 createVirtualDisplay(Surface surface, int width, int height)192 private VirtualDisplay createVirtualDisplay(Surface surface, int width, int height) { 193 Log.i(TAG, "createVirtualDisplay, surface: " + surface + ", width: " + width 194 + "x" + height); 195 return mDisplayManager.createVirtualDisplay(/* projection= */ null, "ClusterOsDouble-VD", 196 width, height, 160, surface, 197 VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_TRUSTED, 198 /* callback= */ null, /* handler= */ null, "ClusterDisplay"); 199 } 200 initClusterOsDouble()201 private void initClusterOsDouble() { 202 mPropertyManager.registerCallback(mPropertyEventCallback, 203 VENDOR_CLUSTER_REPORT_STATE, CarPropertyManager.SENSOR_RATE_ONCHANGE); 204 mPropertyManager.registerCallback(mPropertyEventCallback, 205 VENDOR_CLUSTER_NAVIGATION_STATE, CarPropertyManager.SENSOR_RATE_ONCHANGE); 206 mPropertyManager.registerCallback(mPropertyEventCallback, 207 VENDOR_CLUSTER_REQUEST_DISPLAY, CarPropertyManager.SENSOR_RATE_ONCHANGE); 208 } 209 210 private final CarPropertyEventCallback mPropertyEventCallback = new CarPropertyEventCallback() { 211 @Override 212 public void onChangeEvent(CarPropertyValue carProp) { 213 int propertyId = carProp.getPropertyId(); 214 if (propertyId == VENDOR_CLUSTER_REPORT_STATE) { 215 onClusterReportState((Object[]) carProp.getValue()); 216 } else if (propertyId == VENDOR_CLUSTER_NAVIGATION_STATE) { 217 onClusterNavigationState((byte[]) carProp.getValue()); 218 } else if (propertyId == VENDOR_CLUSTER_REQUEST_DISPLAY) { 219 onClusterRequestDisplay((Integer) carProp.getValue()); 220 } 221 } 222 223 @Override 224 public void onErrorEvent(int propId, int zone) { 225 226 } 227 }; 228 onClusterReportState(Object[] values)229 private void onClusterReportState(Object[] values) { 230 if (DBG) Log.d(TAG, "onClusterReportState: " + Arrays.toString(values)); 231 // CLUSTER_REPORT_STATE should have at least 11 elements, check vehicle/2.0/types.hal. 232 if (values.length < 11) { 233 throw new IllegalArgumentException("Insufficient size of CLUSTER_REPORT_STATE"); 234 } 235 // mainUI is the 10th element, refer to vehicle/2.0/types.hal. 236 int mainUi = (Integer) values[9]; 237 if (mainUi >= 0 && mainUi < mTotalUiSize) { 238 selectUiButton(mainUi); 239 } 240 } 241 selectUiButton(int mainUi)242 private void selectUiButton(int mainUi) { 243 for (int i = 0; i < mTotalUiSize; ++i) { 244 View button = mUiToButton.get(i); 245 button.setSelected(i == mainUi); 246 } 247 mCurrentUi = mainUi; 248 } 249 onClusterNavigationState(byte[] protoBytes)250 private void onClusterNavigationState(byte[] protoBytes) { 251 if (DBG) Log.d(TAG, "onClusterNavigationState: " + Arrays.toString(protoBytes)); 252 try { 253 NavigationStateProto navState = NavigationStateProto.parseFrom(protoBytes); 254 mNavStateController.update(navState); 255 if (DBG) Log.d(TAG, "onClusterNavigationState: " + navState); 256 } catch (InvalidProtocolBufferException e) { 257 Log.e(TAG, "Error parsing navigation state proto", e); 258 } 259 } 260 onClusterRequestDisplay(Integer mainUi)261 private void onClusterRequestDisplay(Integer mainUi) { 262 if (DBG) Log.d(TAG, "onClusterRequestDisplay: " + mainUi); 263 sendDisplayState(); 264 } 265 266 toVendorId(int propId)267 private static int toVendorId(int propId) { 268 return (propId & ~MASK) | VENDOR; 269 } 270 registerSensor(TextView textView, LiveData<V> source)271 private <V> void registerSensor(TextView textView, LiveData<V> source) { 272 String emptyValue = getString(R.string.info_value_empty); 273 source.observe(this, value -> { 274 // Need to check that the text is actually different, or else 275 // it will generate a bunch of CONTENT_CHANGE_TYPE_TEXT accessability 276 // actions. This will cause cts tests to fail when they waitForIdle(), 277 // and the system never idles because it's constantly updating these 278 // TextViews 279 if (value != null && !value.toString().contentEquals(textView.getText())) { 280 textView.setText(value.toString()); 281 } 282 if (value == null && !emptyValue.contentEquals(textView.getText())) { 283 textView.setText(emptyValue); 284 } 285 }); 286 } 287 registerGear(View view, Sensors.Gear gear)288 private void registerGear(View view, Sensors.Gear gear) { 289 mGearsToIcon.put(gear, view); 290 } 291 updateSelectedGear(Sensors.Gear gear)292 private void updateSelectedGear(Sensors.Gear gear) { 293 for (Map.Entry<Sensors.Gear, View> entry : mGearsToIcon.entrySet()) { 294 entry.getValue().setSelected(entry.getKey() == gear); 295 } 296 } 297 registerUi(View view)298 private void registerUi(View view) { 299 int currentUi = mUiToButton.size(); 300 mUiToButton.add(view); 301 mTotalUiSize = mUiToButton.size(); 302 view.setOnTouchListener((v, event) -> { 303 if (event.getAction() == MotionEvent.ACTION_DOWN) { 304 Log.d(TAG, "onTouch: " + currentUi); 305 switchUi(currentUi); 306 } 307 return true; 308 }); 309 } 310 sendDisplayState()311 private void sendDisplayState() { 312 if (mBounds == null || mInsets == null) return; 313 mPropertyManager.setProperty(Integer[].class, VENDOR_CLUSTER_DISPLAY_STATE, 314 VEHICLE_AREA_TYPE_GLOBAL, new Integer[] { 315 1 /* Display On */, 316 mBounds.left, mBounds.top, mBounds.right, mBounds.bottom, 317 mInsets.left, mInsets.top, mInsets.right, mInsets.bottom, 318 UI_TYPE_CLUSTER_HOME, UI_TYPE_CLUSTER_NONE}); 319 } 320 switchUi(int mainUi)321 private void switchUi(int mainUi) { 322 mPropertyManager.setProperty(Integer.class, VENDOR_CLUSTER_SWITCH_UI, 323 VEHICLE_AREA_TYPE_GLOBAL, Integer.valueOf(mainUi)); 324 } 325 326 @Override onKeyDown(int keyCode, KeyEvent event)327 public boolean onKeyDown(int keyCode, KeyEvent event) { 328 Log.d(TAG, "onKeyDown: " + keyCode); 329 if (keyCode == KeyEvent.KEYCODE_MENU) { 330 switchUi((mCurrentUi + 1) % mTotalUiSize); 331 return true; 332 } 333 return super.onKeyDown(keyCode, event); 334 } 335 } 336