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