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;
18 
19 import static android.car.builtin.app.ActivityManagerHelper.createActivityOptions;
20 import static android.content.Intent.ACTION_MAIN;
21 
22 import static com.android.car.PermissionHelper.checkHasDumpPermissionGranted;
23 import static com.android.car.hal.ClusterHalService.DISPLAY_OFF;
24 import static com.android.car.hal.ClusterHalService.DISPLAY_ON;
25 import static com.android.car.hal.ClusterHalService.DONT_CARE;
26 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
27 
28 import android.app.ActivityOptions;
29 import android.car.Car;
30 import android.car.CarOccupantZoneManager;
31 import android.car.ICarOccupantZoneCallback;
32 import android.car.builtin.os.UserManagerHelper;
33 import android.car.builtin.util.Slogf;
34 import android.car.cluster.ClusterHomeManager;
35 import android.car.cluster.ClusterState;
36 import android.car.cluster.IClusterHomeService;
37 import android.car.cluster.IClusterNavigationStateListener;
38 import android.car.cluster.IClusterStateListener;
39 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
40 import android.car.navigation.CarNavigationInstrumentCluster;
41 import android.content.ComponentName;
42 import android.content.Context;
43 import android.content.Intent;
44 import android.content.pm.PackageManager;
45 import android.graphics.Insets;
46 import android.graphics.Point;
47 import android.graphics.Rect;
48 import android.hardware.display.DisplayManager;
49 import android.os.Bundle;
50 import android.os.RemoteCallbackList;
51 import android.os.RemoteException;
52 import android.text.TextUtils;
53 import android.util.proto.ProtoOutputStream;
54 import android.view.Display;
55 import android.view.SurfaceControl;
56 
57 import com.android.car.CarLog;
58 import com.android.car.CarOccupantZoneService;
59 import com.android.car.CarServiceBase;
60 import com.android.car.R;
61 import com.android.car.am.FixedActivityService;
62 import com.android.car.hal.ClusterHalService;
63 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
64 import com.android.car.internal.util.IndentingPrintWriter;
65 
66 /**
67  * Service responsible for interactions between ClusterOS and ClusterHome.
68  */
69 public final class ClusterHomeService extends IClusterHomeService.Stub
70         implements CarServiceBase, ClusterNavigationService.ClusterNavigationServiceCallback,
71         ClusterHalService.ClusterHalEventCallback {
72     private static final String TAG = CarLog.TAG_CLUSTER;
73     private static final int DEFAULT_MIN_UPDATE_INTERVAL_MILLIS = 1000;
74     private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
75 
76     private final Context mContext;
77     private final ClusterHalService mClusterHalService;
78     private final ClusterNavigationService mClusterNavigationService;
79     private final CarOccupantZoneService mOccupantZoneService;
80     private final FixedActivityService mFixedActivityService;
81     private final ComponentName mClusterHomeActivity;
82     private final ClusterHealthMonitor mClusterHealthMonitor;
83 
84     private boolean mServiceEnabled;
85 
86     private int mClusterDisplayId = Display.INVALID_DISPLAY;
87 
88     private int mOnOff = DISPLAY_OFF;
89     private Rect mBounds = new Rect();
90     private Insets mInsets = Insets.NONE;
91     private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
92     private Intent mLastIntent;
93     private int mLastIntentUserId = UserManagerHelper.USER_SYSTEM;
94 
95     private final RemoteCallbackList<IClusterStateListener> mClientListeners =
96             new RemoteCallbackList<>();
97 
98     private final RemoteCallbackList<IClusterNavigationStateListener> mClientNavigationListeners =
99             new RemoteCallbackList<>();
100 
ClusterHomeService(Context context, ClusterHalService clusterHalService, ClusterNavigationService navigationService, CarOccupantZoneService occupantZoneService, FixedActivityService fixedActivityService)101     public ClusterHomeService(Context context, ClusterHalService clusterHalService,
102             ClusterNavigationService navigationService,
103             CarOccupantZoneService occupantZoneService,
104             FixedActivityService fixedActivityService) {
105         mContext = context;
106         mClusterHalService = clusterHalService;
107         mClusterNavigationService = navigationService;
108         mOccupantZoneService = occupantZoneService;
109         mFixedActivityService = fixedActivityService;
110         mClusterHomeActivity = ComponentName.unflattenFromString(
111                 mContext.getString(R.string.config_clusterHomeActivity));
112         mClusterHealthMonitor = new ClusterHealthMonitor(mContext, mClusterHalService);
113         mLastIntent = new Intent(ACTION_MAIN).setComponent(mClusterHomeActivity);
114     }
115 
116     @Override
init()117     public void init() {
118         Slogf.d(TAG, "initClusterHomeService");
119         if (TextUtils.isEmpty(mClusterHomeActivity.getPackageName())
120                 || TextUtils.isEmpty(mClusterHomeActivity.getClassName())) {
121             Slogf.i(TAG, "Improper ClusterHomeActivity: %s", mClusterHomeActivity);
122             return;
123         }
124         if (!mClusterHalService.isServiceEnabled()) {
125             Slogf.e(TAG, "ClusterHomeService is disabled. To enable, it must be either in LIGHT "
126                     + "mode, or all core properties must be defined in FULL mode.");
127             return;
128         }
129         // In FULL mode mOnOff is set to 'OFF', and can be changed by the CLUSTER_DISPLAY_STATE
130         // property. In LIGHT mode, we set it to 'ON' because the CLUSTER_DISPLAY_STATE property may
131         // not be available, and we do not subscribe to it.
132         if (mClusterHalService.isLightMode()) {
133             mOnOff = DISPLAY_ON;
134         }
135 
136         mServiceEnabled = true;
137         mClusterHalService.setCallback(this);
138         mClusterNavigationService.setClusterServiceCallback(this);
139 
140         mOccupantZoneService.registerCallback(mOccupantZoneCallback);
141 
142         initClusterDisplay();
143     }
144 
initClusterDisplay()145     private void initClusterDisplay() {
146         int clusterDisplayId = mOccupantZoneService.getDisplayIdForDriver(
147                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
148         Slogf.d(TAG, "initClusterDisplay: displayId=%d", clusterDisplayId);
149         if (clusterDisplayId == Display.INVALID_DISPLAY) {
150             Slogf.i(TAG, "No cluster display is defined");
151         }
152         if (clusterDisplayId == mClusterDisplayId) {
153             return;  // Skip if the cluster display isn't changed.
154         }
155         mClusterDisplayId = clusterDisplayId;
156         sendDisplayState(ClusterHomeManager.CONFIG_DISPLAY_ID);
157         if (clusterDisplayId == Display.INVALID_DISPLAY) {
158             return;
159         }
160 
161         // Initialize mBounds only once.
162         if (mBounds.right == 0 && mBounds.bottom == 0 && mBounds.left == 0 && mBounds.top == 0) {
163             DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
164             Display clusterDisplay = displayManager.getDisplay(clusterDisplayId);
165             Point size = new Point();
166             clusterDisplay.getRealSize(size);
167             mBounds.right = size.x;
168             mBounds.bottom = size.y;
169             Slogf.d(TAG, "Found cluster displayId=%d, bounds=%s", clusterDisplayId, mBounds);
170         }
171 
172         ActivityOptions activityOptions = ActivityOptions.makeBasic()
173                 .setLaunchDisplayId(clusterDisplayId);
174         mFixedActivityService.startFixedActivityModeForDisplayAndUser(
175                 mLastIntent, activityOptions, clusterDisplayId, mLastIntentUserId);
176     }
177 
178     private final ICarOccupantZoneCallback mOccupantZoneCallback =
179             new ICarOccupantZoneCallback.Stub() {
180                 @Override
181                 public void onOccupantZoneConfigChanged(int flags) throws RemoteException {
182                     if ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0
183                             || (flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER) != 0) {
184                         initClusterDisplay();
185                     }
186                 }
187             };
188 
189     @Override
release()190     public void release() {
191         Slogf.d(TAG, "releaseClusterHomeService");
192         mOccupantZoneService.unregisterCallback(mOccupantZoneCallback);
193         mClusterHalService.setCallback(null);
194         mClusterNavigationService.setClusterServiceCallback(null);
195         mClientListeners.kill();
196         mClientNavigationListeners.kill();
197     }
198 
199     @Override
200     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)201     public void dump(IndentingPrintWriter writer) {
202         checkHasDumpPermissionGranted(mContext, "dump()");
203         writer.println("*ClusterHomeService*");
204 
205         writer.increaseIndent();
206         writer.printf("mServiceEnabled: %b\n", mServiceEnabled);
207         writer.printf("isLightMode: %b\n", mClusterHalService.isLightMode());
208         writer.printf("mClusterDisplayId: %d\n", mClusterDisplayId);
209         writer.printf("mClusterHomeActivity: %s\n", mClusterHomeActivity);
210         writer.printf("mOnOff: %d\n", mOnOff);
211         writer.printf("mBounds: %s\n", mBounds);
212         writer.printf("mInsets: %s\n", mInsets);
213         writer.printf("mUiType: %d\n", mUiType);
214         writer.printf("mLastIntent: %s\n", mLastIntent);
215         writer.printf("mLastIntentUserId: %d\n", mLastIntentUserId);
216         mClusterHealthMonitor.dump(writer);
217         writer.decreaseIndent();
218     }
219 
220     @Override
221     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)222     public void dumpProto(ProtoOutputStream proto) {}
223 
224     // ClusterHalEventListener starts
225     @Override
onSwitchUi(int uiType)226     public void onSwitchUi(int uiType) {
227         Slogf.d(TAG, "onSwitchUi: uiType=%d", uiType);
228         int changes = 0;
229         if (mUiType != uiType) {
230             mUiType = uiType;
231             changes |= ClusterHomeManager.CONFIG_UI_TYPE;
232         }
233         sendDisplayState(changes);
234     }
235 
236     @Override
onDisplayState(int onOff, Rect bounds, Insets insets)237     public void onDisplayState(int onOff, Rect bounds, Insets insets) {
238         Slogf.d(TAG, "onDisplayState: onOff=%d, bounds=%s, insets=%s", onOff, bounds, insets);
239         int changes = 0;
240         if (onOff != DONT_CARE && mOnOff != onOff) {
241             mOnOff = onOff;
242             changes |= ClusterHomeManager.CONFIG_DISPLAY_ON_OFF;
243         }
244         if (bounds != null && !mBounds.equals(bounds)) {
245             mBounds = bounds;
246             changes |= ClusterHomeManager.CONFIG_DISPLAY_BOUNDS;
247         }
248         if (insets != null && !mInsets.equals(insets)) {
249             mInsets = insets;
250             changes |= ClusterHomeManager.CONFIG_DISPLAY_INSETS;
251         }
252         sendDisplayState(changes);
253     }
254     // ClusterHalEventListener ends
255 
sendDisplayState(int changes)256     private void sendDisplayState(int changes) {
257         ClusterState state = createClusterState();
258         int n = mClientListeners.beginBroadcast();
259         for (int i = 0; i < n; i++) {
260             IClusterStateListener callback = mClientListeners.getBroadcastItem(i);
261             try {
262                 callback.onClusterStateChanged(state, changes);
263             } catch (RemoteException ignores) {
264                 // ignore
265             }
266         }
267         mClientListeners.finishBroadcast();
268     }
269 
270     // ClusterNavigationServiceCallback starts
271     @Override
onNavigationStateChanged(Bundle bundle)272     public void onNavigationStateChanged(Bundle bundle) {
273         byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
274 
275         sendNavigationState(protoBytes);
276     }
277 
sendNavigationState(byte[] protoBytes)278     private void sendNavigationState(byte[] protoBytes) {
279         final int n = mClientNavigationListeners.beginBroadcast();
280         for (int i = 0; i < n; i++) {
281             IClusterNavigationStateListener callback =
282                     mClientNavigationListeners.getBroadcastItem(i);
283             try {
284                 callback.onNavigationStateChanged(protoBytes);
285             } catch (RemoteException ignores) {
286                 // ignore
287             }
288         }
289         mClientNavigationListeners.finishBroadcast();
290 
291         if (!mClusterHalService.isNavigationStateSupported()) {
292             Slogf.d(TAG, "No Cluster NavigationState HAL property");
293             return;
294         }
295         mClusterHalService.sendNavigationState(protoBytes);
296     }
297 
298     @Override
getInstrumentClusterInfo()299     public CarNavigationInstrumentCluster getInstrumentClusterInfo() {
300         return CarNavigationInstrumentCluster.createCluster(DEFAULT_MIN_UPDATE_INTERVAL_MILLIS);
301     }
302 
303     @Override
notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner)304     public void notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner) {
305         Slogf.d(TAG, "notifyNavContextOwnerChanged: owner=%s", owner);
306         // Sends the empty NavigationStateProto to clear out the last direction
307         // when the app context owner is changed or the navigation is finished.
308         NavigationStateProto emptyProto = NavigationStateProto.newBuilder()
309                 .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL).build();
310         sendNavigationState(emptyProto.toByteArray());
311     }
312     // ClusterNavigationServiceCallback ends
313 
314     // IClusterHomeService starts
315     @Override
reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability)316     public void reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability) {
317         Slogf.d(TAG, "reportState: main=%d, sub=%d", uiTypeMain, uiTypeSub);
318         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
319         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
320 
321         mUiType = uiTypeMain;
322         mClusterHalService.reportState(mOnOff, mBounds, mInsets,
323                 uiTypeMain, uiTypeSub, uiAvailability);
324     }
325 
326     @Override
requestDisplay(int uiType)327     public void requestDisplay(int uiType) {
328         Slogf.d(TAG, "requestDisplay: uiType=%d", uiType);
329         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
330         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
331 
332         mClusterHalService.requestDisplay(uiType);
333     }
334 
335     @Override
startFixedActivityModeAsUser(Intent intent, Bundle activityOptionsBundle, int userId)336     public boolean startFixedActivityModeAsUser(Intent intent,
337             Bundle activityOptionsBundle, int userId) {
338         Slogf.d(TAG, "startFixedActivityModeAsUser: intent=%s, userId=%d", intent, userId);
339         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
340         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
341         if (mClusterDisplayId == Display.INVALID_DISPLAY) {
342             Slogf.e(TAG, "Cluster display is not ready.");
343             return false;
344         }
345 
346         ActivityOptions activityOptions = activityOptionsBundle != null
347                 ? createActivityOptions(activityOptionsBundle)
348                 : ActivityOptions.makeBasic();
349         activityOptions.setLaunchDisplayId(mClusterDisplayId);
350         mLastIntent = intent;
351         mLastIntentUserId = userId;
352         return mFixedActivityService.startFixedActivityModeForDisplayAndUser(
353                 intent, activityOptions, mClusterDisplayId, userId);
354     }
355 
356     @Override
stopFixedActivityMode()357     public void stopFixedActivityMode() {
358         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
359         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
360         if (mClusterDisplayId == Display.INVALID_DISPLAY) {
361             Slogf.e(TAG, "Cluster display is not ready.");
362             return;
363         }
364 
365         mFixedActivityService.stopFixedActivityMode(mClusterDisplayId);
366     }
367 
368     @Override
registerClusterStateListener(IClusterStateListener listener)369     public void registerClusterStateListener(IClusterStateListener listener) {
370         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
371         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
372 
373         mClientListeners.register(listener);
374     }
375 
376     @Override
unregisterClusterStateListener(IClusterStateListener listener)377     public void unregisterClusterStateListener(IClusterStateListener listener) {
378         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
379         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
380 
381         mClientListeners.unregister(listener);
382     }
383 
384     @Override
registerClusterNavigationStateListener(IClusterNavigationStateListener listener)385     public void registerClusterNavigationStateListener(IClusterNavigationStateListener listener) {
386         enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
387         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
388 
389         mClientNavigationListeners.register(listener);
390     }
391 
392     @Override
unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener)393     public void unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener) {
394         enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
395         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
396 
397         mClientNavigationListeners.unregister(listener);
398     }
399 
400     @Override
getClusterState()401     public ClusterState getClusterState() {
402         Slogf.d(TAG, "getClusterState");
403         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
404         if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
405         return createClusterState();
406     }
407 
408     @Override
sendHeartbeat(long epochTimeNs, byte[] appMetadata)409     public void sendHeartbeat(long epochTimeNs, byte[] appMetadata) {
410         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
411         mClusterHealthMonitor.sendHeartbeat(epochTimeNs, appMetadata);
412     }
413 
414     @Override
startVisibilityMonitoring(SurfaceControl surface)415     public void startVisibilityMonitoring(SurfaceControl surface) {
416         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
417         mClusterHealthMonitor.startVisibilityMonitoring(surface);
418     }
419 
420     @Override
stopVisibilityMonitoring()421     public void stopVisibilityMonitoring() {
422         enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
423         mClusterHealthMonitor.stopVisibilityMonitoring();
424     }
425     // IClusterHomeService ends
426 
enforcePermission(String permissionName)427     private void enforcePermission(String permissionName) {
428         if (mContext.checkCallingOrSelfPermission(permissionName)
429                 != PackageManager.PERMISSION_GRANTED) {
430             throw new SecurityException("requires permission " + permissionName);
431         }
432     }
433 
createClusterState()434     private ClusterState createClusterState() {
435         ClusterState state = new ClusterState();
436         state.on = mOnOff == DISPLAY_ON;
437         state.bounds = mBounds;
438         state.insets = mInsets;
439         state.uiType = mUiType;
440         state.displayId = mClusterDisplayId;
441         return state;
442     }
443 }
444