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 android.car.cluster;
18 
19 import static android.car.feature.Flags.FLAG_CLUSTER_HEALTH_MONITORING;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 
22 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
23 
24 import android.annotation.FlaggedApi;
25 import android.annotation.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.RequiresPermission;
29 import android.annotation.SystemApi;
30 import android.app.Activity;
31 import android.car.Car;
32 import android.car.CarManagerBase;
33 import android.car.builtin.util.Slogf;
34 import android.car.builtin.view.SurfaceControlHelper;
35 import android.content.Intent;
36 import android.os.Bundle;
37 import android.os.IBinder;
38 import android.os.RemoteException;
39 import android.view.SurfaceControl;
40 import android.view.ViewTreeObserver;
41 import android.view.ViewTreeObserver.OnPreDrawListener;
42 
43 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
44 import com.android.car.internal.ICarBase;
45 import com.android.internal.annotations.VisibleForTesting;
46 
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.lang.ref.WeakReference;
50 import java.util.Objects;
51 import java.util.concurrent.CopyOnWriteArrayList;
52 import java.util.concurrent.Executor;
53 
54 /**
55  * Provides the api to manage {@code ClusterHome}.
56  *
57  * @hide
58  */
59 @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING)
60 @SystemApi
61 public final class ClusterHomeManager extends CarManagerBase {
62     private static final String TAG = ClusterHomeManager.class.getSimpleName();
63     /**
64      * When the client reports ClusterHome state and if there is no UI in the sub area, it can
65      * reports UI_TYPE_CLUSTER_NONE instead.
66      *
67      * @hide
68      */
69     public static final int UI_TYPE_CLUSTER_NONE = -1;
70     /** @hide */
71     public static final int UI_TYPE_CLUSTER_HOME = 0;
72 
73     /** @hide */
74     @IntDef(flag = true, prefix = { "CONFIG_" }, value = {
75             CONFIG_DISPLAY_ON_OFF,
76             CONFIG_DISPLAY_BOUNDS,
77             CONFIG_DISPLAY_INSETS,
78             CONFIG_UI_TYPE,
79     })
80     @Retention(RetentionPolicy.SOURCE)
81     public @interface Config {}
82 
83     /** Bit fields indicates which fields of {@link ClusterState} are changed */
84     /** @hide */
85     public static final int CONFIG_DISPLAY_ON_OFF = 0x01;
86     /** @hide */
87     public static final int CONFIG_DISPLAY_BOUNDS = 0x02;
88     /** @hide */
89     public static final int CONFIG_DISPLAY_INSETS = 0x04;
90     /** @hide */
91     public static final int CONFIG_UI_TYPE = 0x08;
92     /** @hide */
93     public static final int CONFIG_DISPLAY_ID = 0x10;
94 
95     /**
96      * Callback for ClusterHome to get notifications when cluster state changes.
97      *
98      * @hide
99      */
100     public interface ClusterStateListener {
101         /**
102          * Called when ClusterOS changes the cluster display state, the geometry of cluster display,
103          * or the uiType.
104          * @param state newly updated {@link ClusterState}
105          * @param changes the flag indicates which fields are updated
106          */
onClusterStateChanged(ClusterState state, @Config int changes)107         void onClusterStateChanged(ClusterState state, @Config int changes);
108     }
109 
110     /**
111      * Callback for ClusterHome to get notifications when cluster navigation state changes.
112      */
113     @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING)
114     public interface ClusterNavigationStateListener {
115         /**
116          * Called when the app who owns the navigation focus casts the new navigation state.
117          *
118          * @param navigationState Byte array that is serialized from a {@link
119          *        android.car.cluster.navigation.NavigationState.NavigationStateProto} proto value.
120          */
onNavigationStateChanged(@onNull byte[] navigationState)121         void onNavigationStateChanged(@NonNull byte[] navigationState);
122     }
123 
124     private static class ClusterStateListenerRecord {
125         final Executor mExecutor;
126         final ClusterStateListener mListener;
ClusterStateListenerRecord(Executor executor, ClusterStateListener listener)127         ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) {
128             mExecutor = executor;
129             mListener = listener;
130         }
131 
132         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
133         @Override
equals(Object obj)134         public boolean equals(Object obj) {
135             if (this == obj) {
136                 return true;
137             }
138             if (!(obj instanceof ClusterStateListenerRecord)) {
139                 return false;
140             }
141             return mListener == ((ClusterStateListenerRecord) obj).mListener;
142         }
143 
144         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
145         @Override
hashCode()146         public int hashCode() {
147             return mListener.hashCode();
148         }
149     }
150 
151     private static class ClusterNavigationStateListenerRecord {
152         final Executor mExecutor;
153         final ClusterNavigationStateListener mListener;
154 
ClusterNavigationStateListenerRecord(Executor executor, ClusterNavigationStateListener listener)155         ClusterNavigationStateListenerRecord(Executor executor,
156                 ClusterNavigationStateListener listener) {
157             mExecutor = executor;
158             mListener = listener;
159         }
160 
161         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
162         @Override
equals(Object obj)163         public boolean equals(Object obj) {
164             if (this == obj) {
165                 return true;
166             }
167             if (!(obj instanceof ClusterNavigationStateListenerRecord)) {
168                 return false;
169             }
170             return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener;
171         }
172 
173         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
174         @Override
hashCode()175         public int hashCode() {
176             return mListener.hashCode();
177         }
178     }
179 
180     private final IClusterHomeService mService;
181     private final IClusterStateListenerImpl mClusterStateListenerBinderCallback;
182     private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback;
183     private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners =
184             new CopyOnWriteArrayList<>();
185     private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord>
186             mNavigationStateListeners = new CopyOnWriteArrayList<>();
187 
188     private boolean mVisibilityMonitoringStarted = false;
189 
190     /** @hide */
191     @VisibleForTesting
ClusterHomeManager(ICarBase car, IBinder service)192     public ClusterHomeManager(ICarBase car, IBinder service) {
193         super(car);
194         mService = IClusterHomeService.Stub.asInterface(service);
195         mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this);
196         mClusterNavigationStateListenerBinderCallback =
197                 new IClusterNavigationStateListenerImpl(this);
198     }
199 
200     /**
201      * Registers the callback for ClusterHome.
202      *
203      * @hide
204      */
205     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
registerClusterStateListener( @onNull Executor executor, @NonNull ClusterStateListener callback)206     public void registerClusterStateListener(
207             @NonNull Executor executor, @NonNull ClusterStateListener callback) {
208         Objects.requireNonNull(executor, "executor cannot be null");
209         Objects.requireNonNull(callback, "callback cannot be null");
210         ClusterStateListenerRecord clusterStateListenerRecord =
211                 new ClusterStateListenerRecord(executor, callback);
212         if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) {
213             return;
214         }
215         if (mStateListeners.size() == 1) {
216             try {
217                 mService.registerClusterStateListener(mClusterStateListenerBinderCallback);
218             } catch (RemoteException e) {
219                 handleRemoteExceptionFromCarService(e);
220             }
221         }
222     }
223 
224     /**
225      * Registers a listener for navigation state changes.
226      *
227      * <p>Note that multiple listeners can be registered. All registered listeners are invoked
228      * when the navigation app that has the focus sends a state change.
229      * <p>A listener is invoked only for changes that occur after the registration. It is not
230      * called for the previous or current states at the time of the registration.
231      */
232     @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
registerClusterNavigationStateListener( @onNull Executor executor, @NonNull ClusterNavigationStateListener callback)233     public void registerClusterNavigationStateListener(
234             @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) {
235         Objects.requireNonNull(executor, "executor cannot be null");
236         Objects.requireNonNull(callback, "callback cannot be null");
237         ClusterNavigationStateListenerRecord clusterStateListenerRecord =
238                 new ClusterNavigationStateListenerRecord(executor, callback);
239         if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) {
240             return;
241         }
242         if (mNavigationStateListeners.size() == 1) {
243             try {
244                 mService.registerClusterNavigationStateListener(
245                         mClusterNavigationStateListenerBinderCallback);
246             } catch (RemoteException e) {
247                 handleRemoteExceptionFromCarService(e);
248             }
249         }
250     }
251 
252     /**
253      * Unregisters the callback.
254      *
255      * @hide
256      */
257     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
unregisterClusterStateListener(@onNull ClusterStateListener callback)258     public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) {
259         Objects.requireNonNull(callback, "callback cannot be null");
260         if (!mStateListeners
261                 .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) {
262             return;
263         }
264         if (mStateListeners.isEmpty()) {
265             try {
266                 mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback);
267             } catch (RemoteException ignored) {
268                 // ignore for unregistering
269             }
270         }
271     }
272 
273     /**
274      * Unregisters a listener for navigation state changes.
275      */
276     @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
unregisterClusterNavigationStateListener( @onNull ClusterNavigationStateListener callback)277     public void unregisterClusterNavigationStateListener(
278             @NonNull ClusterNavigationStateListener callback) {
279         Objects.requireNonNull(callback, "callback cannot be null");
280         if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord(
281                 /* executor= */ null, callback))) {
282             return;
283         }
284         if (mNavigationStateListeners.isEmpty()) {
285             try {
286                 mService.unregisterClusterNavigationStateListener(
287                         mClusterNavigationStateListenerBinderCallback);
288             } catch (RemoteException ignored) {
289                 // ignore for unregistering
290             }
291         }
292     }
293 
294     private static class IClusterStateListenerImpl extends IClusterStateListener.Stub {
295         private final WeakReference<ClusterHomeManager> mManager;
296 
IClusterStateListenerImpl(ClusterHomeManager manager)297         private IClusterStateListenerImpl(ClusterHomeManager manager) {
298             mManager = new WeakReference<>(manager);
299         }
300 
301         @Override
onClusterStateChanged(@onNull ClusterState state, @Config int changes)302         public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) {
303             ClusterHomeManager manager = mManager.get();
304             if (manager != null) {
305                 for (ClusterStateListenerRecord cb : manager.mStateListeners) {
306                     cb.mExecutor.execute(
307                             () -> cb.mListener.onClusterStateChanged(state, changes));
308                 }
309             }
310         }
311     }
312 
313     private static class IClusterNavigationStateListenerImpl extends
314             IClusterNavigationStateListener.Stub {
315         private final WeakReference<ClusterHomeManager> mManager;
316 
IClusterNavigationStateListenerImpl(ClusterHomeManager manager)317         private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) {
318             mManager = new WeakReference<>(manager);
319         }
320 
321         @Override
onNavigationStateChanged(@onNull byte[] navigationState)322         public void onNavigationStateChanged(@NonNull byte[] navigationState) {
323             ClusterHomeManager manager = mManager.get();
324             if (manager != null) {
325                 for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) {
326                     lr.mExecutor.execute(
327                             () -> lr.mListener.onNavigationStateChanged(navigationState));
328                 }
329             }
330         }
331     }
332 
333     /**
334      * Reports the current ClusterUI state.
335      * @param uiTypeMain uiType that ClusterHome tries to show in main area
336      * @param uiTypeSub uiType that ClusterHome tries to show in sub area
337      * @param uiAvailability the byte array to represent the availability of ClusterUI.
338      *    0 indicates non-available and 1 indicates available.
339      *    Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition.
340      *
341      * @hide
342      */
343     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability)344     public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) {
345         try {
346             mService.reportState(uiTypeMain, uiTypeSub, uiAvailability);
347         } catch (RemoteException e) {
348             handleRemoteExceptionFromCarService(e);
349         }
350     }
351 
352     /**
353      * Requests to turn the cluster display on to show some ClusterUI.
354      * @param uiType uiType that ClusterHome tries to show in main area
355      *
356      * @hide
357      */
358     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
requestDisplay(int uiType)359     public void requestDisplay(int uiType) {
360         try {
361             mService.requestDisplay(uiType);
362         } catch (RemoteException e) {
363             handleRemoteExceptionFromCarService(e);
364         }
365     }
366 
367     /**
368      * Returns the current {@code ClusterState}.
369      *
370      * @hide
371      */
372     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
373     @Nullable
getClusterState()374     public ClusterState getClusterState() {
375         ClusterState state = null;
376         try {
377             state = mService.getClusterState();
378         } catch (RemoteException e) {
379             handleRemoteExceptionFromCarService(e);
380         }
381         return state;
382     }
383 
384     /**
385      * Start an activity as specified user. The activity is considered as in fixed mode for
386      * the cluster display and will be re-launched if the activity crashes, the package
387      * is updated or goes to background for whatever reason.
388      * Only one activity can exist in fixed mode for the display and calling this multiple
389      * times with different {@code Intent} will lead into making all previous activities into
390      * non-fixed normal state (= will not be re-launched.)
391      * @param intent the Intent to start
392      * @param options additional options for how the Activity should be started
393      * @param userId the user the new activity should run as
394      * @return true if it launches the given Intent as FixedActivity successfully
395      *
396      * @hide
397      */
398     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
startFixedActivityModeAsUser( Intent intent, @Nullable Bundle options, int userId)399     public boolean startFixedActivityModeAsUser(
400             Intent intent, @Nullable Bundle options, int userId) {
401         try {
402             return mService.startFixedActivityModeAsUser(intent, options, userId);
403         } catch (RemoteException e) {
404             handleRemoteExceptionFromCarService(e);
405         }
406         return false;
407     }
408 
409     /**
410      * The activity launched on the cluster display is no longer in fixed mode. Re-launching or
411      * finishing should not trigger re-launching any more. Note that Activity for non-current user
412      * will be auto-stopped and there is no need to call this for user switching. Note that this
413      * does not stop the activity but it will not be re-launched any more.
414      *
415      * @hide
416      */
417     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
stopFixedActivityMode()418     public void stopFixedActivityMode() {
419         try {
420             mService.stopFixedActivityMode();
421         } catch (RemoteException e) {
422             handleRemoteExceptionFromCarService(e);
423         }
424     }
425 
426     /**
427      * Sends a heartbeat to ClusterOS.
428      * @param epochTimeNs the current time
429      * @param appMetadata the application specific metadata which will be delivered with
430      *                    the heartbeat.
431      */
432     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata)433     public void sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata) {
434         try {
435             mService.sendHeartbeat(epochTimeNs, appMetadata);
436         } catch (RemoteException e) {
437             handleRemoteExceptionFromCarService(e);
438         }
439     }
440 
441     /**
442      * Starts the visibility monitoring of given {@link Activity}.
443      *
444      * Note: This is supposed to be called in {@link Activity#onStart()} generally.
445      *
446      * @param activity               the {@link Activity} to track the visibility of its Window
447      */
448     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
startVisibilityMonitoring(@onNull Activity activity)449     public void startVisibilityMonitoring(@NonNull Activity activity) {
450         // We'd like to check the permission locally too, since the actual execution happens later.
451         if (getContext().checkCallingOrSelfPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
452                 != PERMISSION_GRANTED) {
453             throw new SecurityException(
454                     "requires permission " + Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
455         }
456         if (mVisibilityMonitoringStarted) {
457             Slogf.w(TAG, "startVisibilityMonitoring is already started");
458             return;
459         }
460         if (SurfaceControlHelper.getSurfaceControl(activity) != null) {
461             throw new IllegalStateException(
462                     "startVisibilityMonitoring is expected to be called before onAttachedToWindow");
463         }
464         mVisibilityMonitoringStarted = true;
465         ViewTreeObserver observer = getViewTreeObserver(activity);
466         // Can't use onWindowAttached, because SurfaceControl is available at the time, but invalid.
467         // TODO: b/286406553 - Move the callback below to onWindowAttached.
468         observer.addOnPreDrawListener(
469                 new OnPreDrawListener() {
470                     @Override
471                     public boolean onPreDraw() {
472                         // The existing 'observer' would be invalid, so gets it again.
473                         ViewTreeObserver observer = getViewTreeObserver(activity);
474                         observer.removeOnPreDrawListener(this);
475                         startVisibilityMonitoringInternal(activity);
476                         return true;
477                     }
478                 });
479         observer.addOnWindowAttachListener(
480                 new ViewTreeObserver.OnWindowAttachListener() {
481                     @Override
482                     public void onWindowAttached() {
483                         // Using onPreDraw instead, check b/286406553.
484                     }
485 
486                     @Override
487                     public void onWindowDetached() {
488                         ViewTreeObserver observer = getViewTreeObserver(activity);
489                         observer.removeOnWindowAttachListener(this);
490                         stopVisibilityMonitoringInternal();
491                     }
492                 }
493         );
494     }
495 
getViewTreeObserver(@onNull Activity activity)496     private static ViewTreeObserver getViewTreeObserver(@NonNull Activity activity) {
497         return activity.getWindow().getDecorView().getViewTreeObserver();
498     }
499 
startVisibilityMonitoringInternal(Activity activity)500     private void startVisibilityMonitoringInternal(Activity activity) {
501         SurfaceControl surfaceControl = SurfaceControlHelper.getSurfaceControl(activity);
502         try {
503             mService.startVisibilityMonitoring(surfaceControl);
504         } catch (RemoteException e) {
505             Slogf.e(TAG, "Failed to startVisibilityMonitoring", e);
506         }
507     }
508 
stopVisibilityMonitoringInternal()509     private void stopVisibilityMonitoringInternal() {
510         try {
511             mService.stopVisibilityMonitoring();
512             mVisibilityMonitoringStarted = false;
513         } catch (RemoteException e) {
514             Slogf.e(TAG, "Failed to stopVisibilityMonitoring", e);
515         }
516     }
517 
518     /** @hide */
519     @Override
onCarDisconnected()520     protected void onCarDisconnected() {
521         mStateListeners.clear();
522         mNavigationStateListeners.clear();
523     }
524 }
525