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