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