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 24 import static com.android.car.cluster.osdouble.ClusterOsDoubleApplication.TAG; 25 26 import android.car.Car; 27 import android.car.VehiclePropertyIds; 28 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 29 import android.car.hardware.CarPropertyValue; 30 import android.car.hardware.property.CarPropertyManager; 31 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback; 32 import android.content.res.CompatibilityInfo; 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.DisplayMetrics; 40 import android.util.IntArray; 41 import android.util.Log; 42 import android.view.DisplayInfo; 43 import android.view.KeyEvent; 44 import android.view.MotionEvent; 45 import android.view.Surface; 46 import android.view.SurfaceHolder; 47 import android.view.SurfaceView; 48 import android.view.View; 49 import android.widget.TextView; 50 51 import androidx.activity.ComponentActivity; 52 import androidx.lifecycle.LiveData; 53 import androidx.lifecycle.ViewModelProvider; 54 55 import com.android.car.cluster.sensors.Sensors; 56 import com.android.car.cluster.view.BitmapFetcher; 57 import com.android.car.cluster.view.ClusterViewModel; 58 import com.android.car.cluster.view.ImageResolver; 59 import com.android.car.cluster.view.NavStateController; 60 61 import com.google.protobuf.InvalidProtocolBufferException; 62 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.Map; 66 67 /** 68 * The Activity which plays the role of ClusterOs for the testing. 69 */ 70 public class ClusterOsDoubleActivity extends ComponentActivity { 71 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 72 73 // VehiclePropertyGroup 74 private static final int SYSTEM = 0x10000000; 75 private static final int VENDOR = 0x20000000; 76 private static final int MASK = 0xf0000000; 77 78 private static final int VENDOR_CLUSTER_REPORT_STATE = toVendorId( 79 VehiclePropertyIds.CLUSTER_REPORT_STATE); 80 private static final int VENDOR_CLUSTER_SWITCH_UI = toVendorId( 81 VehiclePropertyIds.CLUSTER_SWITCH_UI); 82 private static final int VENDOR_CLUSTER_NAVIGATION_STATE = toVendorId( 83 VehiclePropertyIds.CLUSTER_NAVIGATION_STATE); 84 private static final int VENDOR_CLUSTER_REQUEST_DISPLAY = toVendorId( 85 VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY); 86 private static final int VENDOR_CLUSTER_DISPLAY_STATE = toVendorId( 87 VehiclePropertyIds.CLUSTER_DISPLAY_STATE); 88 89 // For the detail, please refer to vehicle/2.0/types.hal. 90 private static final int REPORT_STATE_MAIN_UI_INDEX = 9; 91 private static final int REPORT_STATE_UI_AVAILABILITY_INDEX = 11; 92 93 private DisplayManager mDisplayManager; 94 private CarPropertyManager mPropertyManager; 95 96 private SurfaceView mSurfaceView; 97 private Rect mBounds; 98 private Insets mInsets; 99 private static VirtualDisplay sVirtualDisplay; 100 101 private ClusterViewModel mClusterViewModel; 102 private final ArrayMap<Sensors.Gear, View> mGearsToIcon = new ArrayMap<>(); 103 private final ArrayList<View> mUiToButton = new ArrayList<>(); 104 int mCurrentUi = UI_TYPE_CLUSTER_HOME; 105 private final IntArray mUiAvailability = new IntArray(); 106 107 private NavStateController mNavStateController; 108 109 @Override onCreate(Bundle savedInstanceState)110 public void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 113 mDisplayManager = getSystemService(DisplayManager.class); 114 115 Car.createCar(getApplicationContext(), /* handler= */ null, 116 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 117 (car, ready) -> { 118 if (!ready) return; 119 mPropertyManager = (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE); 120 initClusterOsDouble(); 121 }); 122 123 View view = getLayoutInflater().inflate(R.layout.cluster_os_double_activity, null); 124 mSurfaceView = view.findViewById(R.id.cluster_display); 125 mSurfaceView.getHolder().addCallback(mSurfaceViewCallback); 126 setContentView(view); 127 128 registerGear(findViewById(R.id.gear_parked), Sensors.Gear.PARK); 129 registerGear(findViewById(R.id.gear_reverse), Sensors.Gear.REVERSE); 130 registerGear(findViewById(R.id.gear_neutral), Sensors.Gear.NEUTRAL); 131 registerGear(findViewById(R.id.gear_drive), Sensors.Gear.DRIVE); 132 133 mClusterViewModel = new ViewModelProvider(this).get(ClusterViewModel.class); 134 mClusterViewModel.getSensor(Sensors.SENSOR_GEAR).observe(this, this::updateSelectedGear); 135 136 registerSensor(findViewById(R.id.info_fuel), mClusterViewModel.getFuelLevel()); 137 registerSensor(findViewById(R.id.info_speed), mClusterViewModel.getSpeed()); 138 registerSensor(findViewById(R.id.info_range), mClusterViewModel.getRange()); 139 registerSensor(findViewById(R.id.info_rpm), mClusterViewModel.getRPM()); 140 141 // The order should be matched with ClusterHomeApplication. 142 registerUi(findViewById(R.id.btn_car_info)); 143 registerUi(findViewById(R.id.btn_nav)); 144 registerUi(findViewById(R.id.btn_music)); 145 registerUi(findViewById(R.id.btn_phone)); 146 147 BitmapFetcher bitmapFetcher = new BitmapFetcher(this); 148 ImageResolver imageResolver = new ImageResolver(bitmapFetcher); 149 mNavStateController = new NavStateController( 150 findViewById(R.id.navigation_state), imageResolver); 151 } 152 153 private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() { 154 @Override 155 public void surfaceCreated(SurfaceHolder holder) { 156 Log.i(TAG, "surfaceCreated, holder: " + holder); 157 } 158 159 @Override 160 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 161 Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height 162 + ", format:" + format); 163 164 // Create mock unobscured area to report to navigation activity. 165 int obscuredWidth = (int) getResources() 166 .getDimension(R.dimen.speedometer_overlap_width); 167 int obscuredHeight = (int) getResources() 168 .getDimension(R.dimen.navigation_gradient_height); 169 mBounds = new Rect(/* left= */ 0, /* top= */ 0, 170 /* right= */ width, /* bottom= */ height); 171 // Adds some empty space in the boundary of the display to verify if mBounds works. 172 mBounds.inset(/* dx= */ 12, /* dy= */ 12); 173 mInsets = Insets.of(obscuredWidth, obscuredHeight, obscuredWidth, obscuredHeight); 174 if (sVirtualDisplay == null) { 175 sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); 176 } else { 177 DisplayInfo displayInfo = new DisplayInfo(); 178 DisplayMetrics boundsMetrics = new DisplayMetrics(); 179 boolean isDisplayValid = sVirtualDisplay.getDisplay().getDisplayInfo(displayInfo); 180 displayInfo.getLogicalMetrics(boundsMetrics, 181 CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, /* configuration= */ null); 182 if (isDisplayValid && boundsMetrics.widthPixels == width 183 && boundsMetrics.heightPixels == height) { 184 sVirtualDisplay.setSurface(holder.getSurface()); 185 } else { 186 // Display was resized, delete existing and create new display. 187 // TODO(b/254931119): Resize the display instead of replacing it. 188 sVirtualDisplay.release(); 189 sVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height); 190 } 191 } 192 } 193 194 @Override 195 public void surfaceDestroyed(SurfaceHolder holder) { 196 Log.i(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from" 197 + " display, surface: " + holder.getSurface()); 198 // detaching surface is similar to turning off the display 199 sVirtualDisplay.setSurface(null); 200 } 201 }; 202 createVirtualDisplay(Surface surface, int width, int height)203 private VirtualDisplay createVirtualDisplay(Surface surface, int width, int height) { 204 Log.i(TAG, "createVirtualDisplay, surface: " + surface + ", width: " + width 205 + "x" + height); 206 return mDisplayManager.createVirtualDisplay(/* projection= */ null, "ClusterOsDouble-VD", 207 width, height, 160, surface, 208 // Don't use VIRTUAL_DISPLAY_FLAG_TRUSTED, because we don't want the cluster display 209 // to be the focus display which can hinder Rotary service (b/206862329). 210 VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY, 211 /* callback= */ null, /* handler= */ null, "ClusterDisplay"); 212 } 213 initClusterOsDouble()214 private void initClusterOsDouble() { 215 try { 216 mPropertyManager.subscribePropertyEvents(VENDOR_CLUSTER_REPORT_STATE, 217 mPropertyEventCallback); 218 mPropertyManager.subscribePropertyEvents(VENDOR_CLUSTER_NAVIGATION_STATE, 219 mPropertyEventCallback); 220 mPropertyManager.subscribePropertyEvents(VENDOR_CLUSTER_REQUEST_DISPLAY, 221 mPropertyEventCallback); 222 } catch (Exception e) { 223 Log.e(TAG, "Failed to subscribe to cluster properties", e); 224 } 225 } 226 227 private final CarPropertyEventCallback mPropertyEventCallback = new CarPropertyEventCallback() { 228 @Override 229 public void onChangeEvent(CarPropertyValue carProp) { 230 int propertyId = carProp.getPropertyId(); 231 if (propertyId == VENDOR_CLUSTER_REPORT_STATE) { 232 onClusterReportState((Object[]) carProp.getValue()); 233 } else if (propertyId == VENDOR_CLUSTER_NAVIGATION_STATE) { 234 onClusterNavigationState((byte[]) carProp.getValue()); 235 } else if (propertyId == VENDOR_CLUSTER_REQUEST_DISPLAY) { 236 onClusterRequestDisplay((Integer) carProp.getValue()); 237 } 238 } 239 240 @Override 241 public void onErrorEvent(int propId, int zone) { 242 243 } 244 }; 245 onClusterReportState(Object[] values)246 private void onClusterReportState(Object[] values) { 247 if (DBG) Log.d(TAG, "onClusterReportState: " + Arrays.toString(values)); 248 // CLUSTER_REPORT_STATE should have at least 11 elements, check vehicle/2.0/types.hal. 249 if (values.length < 11) { 250 throw new IllegalArgumentException("Insufficient size of CLUSTER_REPORT_STATE"); 251 } 252 int mainUi = (Integer) values[REPORT_STATE_MAIN_UI_INDEX]; 253 int totalUiSize = values.length - REPORT_STATE_UI_AVAILABILITY_INDEX; 254 mUiAvailability.resize(totalUiSize); 255 for (int i = 0; i < totalUiSize; ++i) { 256 mUiAvailability.set(i, (Byte) values[i + REPORT_STATE_UI_AVAILABILITY_INDEX]); 257 } 258 selectUiButton(mainUi); 259 } 260 selectUiButton(int mainUi)261 private void selectUiButton(int mainUi) { 262 for (int i = mUiToButton.size() - 1; i >= 0; --i) { 263 View button = mUiToButton.get(i); 264 button.setSelected(i == mainUi); 265 } 266 mCurrentUi = mainUi; 267 } 268 onClusterNavigationState(byte[] protoBytes)269 private void onClusterNavigationState(byte[] protoBytes) { 270 if (DBG) Log.d(TAG, "onClusterNavigationState: " + Arrays.toString(protoBytes)); 271 try { 272 NavigationStateProto navState = NavigationStateProto.parseFrom(protoBytes); 273 mNavStateController.update(navState); 274 if (DBG) Log.d(TAG, "onClusterNavigationState: " + navState); 275 } catch (InvalidProtocolBufferException e) { 276 Log.e(TAG, "Error parsing navigation state proto", e); 277 } 278 } 279 onClusterRequestDisplay(Integer mainUi)280 private void onClusterRequestDisplay(Integer mainUi) { 281 if (DBG) Log.d(TAG, "onClusterRequestDisplay: " + mainUi); 282 sendDisplayState(); 283 } 284 toVendorId(int propId)285 private static int toVendorId(int propId) { 286 return (propId & ~MASK) | VENDOR; 287 } 288 registerSensor(TextView textView, LiveData<V> source)289 private <V> void registerSensor(TextView textView, LiveData<V> source) { 290 String emptyValue = getString(R.string.info_value_empty); 291 source.observe(this, value -> { 292 // Need to check that the text is actually different, or else 293 // it will generate a bunch of CONTENT_CHANGE_TYPE_TEXT accessability 294 // actions. This will cause cts tests to fail when they waitForIdle(), 295 // and the system never idles because it's constantly updating these 296 // TextViews 297 if (value != null && !value.toString().contentEquals(textView.getText())) { 298 textView.setText(value.toString()); 299 } 300 if (value == null && !emptyValue.contentEquals(textView.getText())) { 301 textView.setText(emptyValue); 302 } 303 }); 304 } 305 registerGear(View view, Sensors.Gear gear)306 private void registerGear(View view, Sensors.Gear gear) { 307 mGearsToIcon.put(gear, view); 308 } 309 updateSelectedGear(Sensors.Gear gear)310 private void updateSelectedGear(Sensors.Gear gear) { 311 for (Map.Entry<Sensors.Gear, View> entry : mGearsToIcon.entrySet()) { 312 entry.getValue().setSelected(entry.getKey() == gear); 313 } 314 } 315 registerUi(View view)316 private void registerUi(View view) { 317 int currentUi = mUiToButton.size(); 318 mUiToButton.add(view); 319 view.setOnTouchListener((v, event) -> { 320 if (event.getAction() == MotionEvent.ACTION_DOWN) { 321 Log.d(TAG, "onTouch: " + currentUi); 322 switchUi(currentUi); 323 } 324 return true; 325 }); 326 } 327 sendDisplayState()328 private void sendDisplayState() { 329 if (mBounds == null || mInsets == null) return; 330 mPropertyManager.setProperty(Integer[].class, VENDOR_CLUSTER_DISPLAY_STATE, 331 VEHICLE_AREA_TYPE_GLOBAL, new Integer[] { 332 1 /* Display On */, 333 mBounds.left, mBounds.top, mBounds.right, mBounds.bottom, 334 mInsets.left, mInsets.top, mInsets.right, mInsets.bottom, 335 UI_TYPE_CLUSTER_HOME, UI_TYPE_CLUSTER_NONE}); 336 } 337 switchUi(int mainUi)338 private void switchUi(int mainUi) { 339 mPropertyManager.setProperty(Integer.class, VENDOR_CLUSTER_SWITCH_UI, 340 VEHICLE_AREA_TYPE_GLOBAL, Integer.valueOf(mainUi)); 341 } 342 343 @Override onKeyDown(int keyCode, KeyEvent event)344 public boolean onKeyDown(int keyCode, KeyEvent event) { 345 Log.d(TAG, "onKeyDown: " + keyCode); 346 if (keyCode == KeyEvent.KEYCODE_MENU) { 347 int nextUi = mCurrentUi; 348 do { 349 nextUi = nextUi + 1; 350 if (nextUi >= mUiToButton.size()) nextUi = 0; 351 } while (mUiAvailability.get(nextUi) == 0); 352 switchUi(nextUi); 353 return true; 354 } 355 return super.onKeyDown(keyCode, event); 356 } 357 } 358