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 android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.car.Car;
24 import android.car.CarManagerBase;
25 import android.content.Intent;
26 import android.os.Bundle;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.lang.ref.WeakReference;
35 import java.util.Objects;
36 import java.util.concurrent.CopyOnWriteArrayList;
37 import java.util.concurrent.Executor;
38 
39 /** @hide */
40 public class ClusterHomeManager extends CarManagerBase {
41     private static final String TAG = ClusterHomeManager.class.getSimpleName();
42     /**
43      * When the client reports ClusterHome state and if there is no UI in the sub area, it can
44      * reports UI_TYPE_CLUSTER_NONE instead.
45      */
46     public static final int UI_TYPE_CLUSTER_NONE = -1;
47     public static final int UI_TYPE_CLUSTER_HOME = 0;
48 
49     /** @hide */
50     @IntDef(flag = true, prefix = { "CONFIG_" }, value = {
51             CONFIG_DISPLAY_ON_OFF,
52             CONFIG_DISPLAY_BOUNDS,
53             CONFIG_DISPLAY_INSETS,
54             CONFIG_UI_TYPE,
55     })
56     @Retention(RetentionPolicy.SOURCE)
57     public @interface Config {}
58 
59     /** Bit fields indicates which fields of {@link ClusterState} are changed */
60     public static final int CONFIG_DISPLAY_ON_OFF = 0x01;
61     public static final int CONFIG_DISPLAY_BOUNDS = 0x02;
62     public static final int CONFIG_DISPLAY_INSETS = 0x04;
63     public static final int CONFIG_UI_TYPE = 0x08;
64     public static final int CONFIG_DISPLAY_ID = 0x10;
65 
66     /**
67      * Callback for ClusterHome to get notifications when cluster state changes.
68      */
69     public interface ClusterStateListener {
70         /**
71          * Called when ClusterOS changes the cluster display state, the geometry of cluster display,
72          * or the uiType.
73          * @param state newly updated {@link ClusterState}
74          * @param changes the flag indicates which fields are updated
75          */
onClusterStateChanged(ClusterState state, @Config int changes)76         void onClusterStateChanged(ClusterState state, @Config int changes);
77     }
78 
79     /**
80      * Callback for ClusterHome to get notifications when cluster navigation state changes.
81      */
82     public interface ClusterNavigationStateListener {
83         /** Called when the App who owns the navigation focus casts the new navigation state. */
onNavigationState(byte[] navigationState)84         void onNavigationState(byte[] navigationState);
85     }
86 
87     private static class ClusterStateListenerRecord {
88         final Executor mExecutor;
89         final ClusterStateListener mListener;
ClusterStateListenerRecord(Executor executor, ClusterStateListener listener)90         ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) {
91             mExecutor = executor;
92             mListener = listener;
93         }
94         @Override
equals(Object obj)95         public boolean equals(Object obj) {
96             if (this == obj) {
97                 return true;
98             }
99             if (!(obj instanceof ClusterStateListenerRecord)) {
100                 return false;
101             }
102             return mListener == ((ClusterStateListenerRecord) obj).mListener;
103         }
104         @Override
hashCode()105         public int hashCode() {
106             return mListener.hashCode();
107         }
108     }
109 
110     private static class ClusterNavigationStateListenerRecord {
111         final Executor mExecutor;
112         final ClusterNavigationStateListener mListener;
113 
ClusterNavigationStateListenerRecord(Executor executor, ClusterNavigationStateListener listener)114         ClusterNavigationStateListenerRecord(Executor executor,
115                 ClusterNavigationStateListener listener) {
116             mExecutor = executor;
117             mListener = listener;
118         }
119         @Override
equals(Object obj)120         public boolean equals(Object obj) {
121             if (this == obj) {
122                 return true;
123             }
124             if (!(obj instanceof ClusterNavigationStateListenerRecord)) {
125                 return false;
126             }
127             return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener;
128         }
129         @Override
hashCode()130         public int hashCode() {
131             return mListener.hashCode();
132         }
133     }
134 
135     private final IClusterHomeService mService;
136     private final IClusterStateListenerImpl mClusterStateListenerBinderCallback;
137     private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback;
138     private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners =
139             new CopyOnWriteArrayList<>();
140     private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord>
141             mNavigationStateListeners = new CopyOnWriteArrayList<>();
142 
143     /** @hide */
144     @VisibleForTesting
ClusterHomeManager(Car car, IBinder service)145     public ClusterHomeManager(Car car, IBinder service) {
146         super(car);
147         mService = IClusterHomeService.Stub.asInterface(service);
148         mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this);
149         mClusterNavigationStateListenerBinderCallback =
150                 new IClusterNavigationStateListenerImpl(this);
151     }
152 
153     /**
154      * Registers the callback for ClusterHome.
155      */
156     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
registerClusterStateListener( @onNull Executor executor, @NonNull ClusterStateListener callback)157     public void registerClusterStateListener(
158             @NonNull Executor executor, @NonNull ClusterStateListener callback) {
159         Objects.requireNonNull(executor, "executor cannot be null");
160         Objects.requireNonNull(callback, "callback cannot be null");
161         ClusterStateListenerRecord clusterStateListenerRecord =
162                 new ClusterStateListenerRecord(executor, callback);
163         if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) {
164             return;
165         }
166         if (mStateListeners.size() == 1) {
167             try {
168                 mService.registerClusterStateListener(mClusterStateListenerBinderCallback);
169             } catch (RemoteException e) {
170                 handleRemoteExceptionFromCarService(e);
171             }
172         }
173     }
174 
175     /**
176      * Registers the callback for ClusterHome.
177      */
178     @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
registerClusterNavigationStateListener( @onNull Executor executor, @NonNull ClusterNavigationStateListener callback)179     public void registerClusterNavigationStateListener(
180             @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) {
181         Objects.requireNonNull(executor, "executor cannot be null");
182         Objects.requireNonNull(callback, "callback cannot be null");
183         ClusterNavigationStateListenerRecord clusterStateListenerRecord =
184                 new ClusterNavigationStateListenerRecord(executor, callback);
185         if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) {
186             return;
187         }
188         if (mNavigationStateListeners.size() == 1) {
189             try {
190                 mService.registerClusterNavigationStateListener(
191                         mClusterNavigationStateListenerBinderCallback);
192             } catch (RemoteException e) {
193                 handleRemoteExceptionFromCarService(e);
194             }
195         }
196     }
197 
198     /**
199      * Unregisters the callback.
200      */
201     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
unregisterClusterStateListener(@onNull ClusterStateListener callback)202     public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) {
203         Objects.requireNonNull(callback, "callback cannot be null");
204         if (!mStateListeners
205                 .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) {
206             return;
207         }
208         if (mStateListeners.isEmpty()) {
209             try {
210                 mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback);
211             } catch (RemoteException ignored) {
212                 // ignore for unregistering
213             }
214         }
215     }
216 
217     /**
218      * Unregisters the callback.
219      */
220     @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
unregisterClusterNavigationStateListener( @onNull ClusterNavigationStateListener callback)221     public void unregisterClusterNavigationStateListener(
222             @NonNull ClusterNavigationStateListener callback) {
223         Objects.requireNonNull(callback, "callback cannot be null");
224         if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord(
225                 /* executor= */ null, callback))) {
226             return;
227         }
228         if (mNavigationStateListeners.isEmpty()) {
229             try {
230                 mService.unregisterClusterNavigationStateListener(
231                         mClusterNavigationStateListenerBinderCallback);
232             } catch (RemoteException ignored) {
233                 // ignore for unregistering
234             }
235         }
236     }
237 
238     private static class IClusterStateListenerImpl extends IClusterStateListener.Stub {
239         private final WeakReference<ClusterHomeManager> mManager;
240 
IClusterStateListenerImpl(ClusterHomeManager manager)241         private IClusterStateListenerImpl(ClusterHomeManager manager) {
242             mManager = new WeakReference<>(manager);
243         }
244 
245         @Override
onClusterStateChanged(@onNull ClusterState state, @Config int changes)246         public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) {
247             ClusterHomeManager manager = mManager.get();
248             if (manager != null) {
249                 for (ClusterStateListenerRecord cb : manager.mStateListeners) {
250                     cb.mExecutor.execute(
251                             () -> cb.mListener.onClusterStateChanged(state, changes));
252                 }
253             }
254         }
255     }
256 
257     private static class IClusterNavigationStateListenerImpl extends
258             IClusterNavigationStateListener.Stub {
259         private final WeakReference<ClusterHomeManager> mManager;
260 
IClusterNavigationStateListenerImpl(ClusterHomeManager manager)261         private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) {
262             mManager = new WeakReference<>(manager);
263         }
264 
265         @Override
onNavigationStateChanged(@onNull byte[] navigationState)266         public void onNavigationStateChanged(@NonNull byte[] navigationState) {
267             ClusterHomeManager manager = mManager.get();
268             if (manager != null) {
269                 for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) {
270                     lr.mExecutor.execute(() -> lr.mListener.onNavigationState(navigationState));
271                 }
272             }
273         }
274     }
275 
276     /**
277      * Reports the current ClusterUI state.
278      * @param uiTypeMain uiType that ClusterHome tries to show in main area
279      * @param uiTypeSub uiType that ClusterHome tries to show in sub area
280      * @param uiAvailability the byte array to represent the availability of ClusterUI.
281      *    0 indicates non-available and 1 indicates available.
282      *    Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition.
283      */
284     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability)285     public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) {
286         try {
287             mService.reportState(uiTypeMain, uiTypeSub, uiAvailability);
288         } catch (RemoteException e) {
289             handleRemoteExceptionFromCarService(e);
290         }
291     }
292 
293     /**
294      * Requests to turn the cluster display on to show some ClusterUI.
295      * @param uiType uiType that ClusterHome tries to show in main area
296      */
297     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
requestDisplay(int uiType)298     public void requestDisplay(int uiType) {
299         try {
300             mService.requestDisplay(uiType);
301         } catch (RemoteException e) {
302             handleRemoteExceptionFromCarService(e);
303         }
304     }
305 
306     /**
307      * Returns the current {@code ClusterState}.
308      */
309     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
310     @Nullable
getClusterState()311     public ClusterState getClusterState() {
312         ClusterState state = null;
313         try {
314             state = mService.getClusterState();
315         } catch (RemoteException e) {
316             handleRemoteExceptionFromCarService(e);
317         }
318         return state;
319     }
320 
321     /**
322      * Start an activity as specified user. The activity is considered as in fixed mode for
323      * the cluster display and will be re-launched if the activity crashes, the package
324      * is updated or goes to background for whatever reason.
325      * Only one activity can exist in fixed mode for the display and calling this multiple
326      * times with different {@code Intent} will lead into making all previous activities into
327      * non-fixed normal state (= will not be re-launched.)
328      * @param intent the Intent to start
329      * @param options additional options for how the Activity should be started
330      * @param userId the user the new activity should run as
331      * @return true if it launches the given Intent as FixedActivity successfully
332      */
333     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
startFixedActivityModeAsUser( Intent intent, @Nullable Bundle options, int userId)334     public boolean startFixedActivityModeAsUser(
335             Intent intent, @Nullable Bundle options, int userId) {
336         try {
337             return mService.startFixedActivityModeAsUser(intent, options, userId);
338         } catch (RemoteException e) {
339             handleRemoteExceptionFromCarService(e);
340         }
341         return false;
342     }
343 
344     /**
345      * The activity launched on the cluster display is no longer in fixed mode. Re-launching or
346      * finishing should not trigger re-launching any more. Note that Activity for non-current user
347      * will be auto-stopped and there is no need to call this for user switching. Note that this
348      * does not stop the activity but it will not be re-launched any more.
349      */
350     @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
stopFixedActivityMode()351     public void stopFixedActivityMode() {
352         try {
353             mService.stopFixedActivityMode();
354         } catch (RemoteException e) {
355             handleRemoteExceptionFromCarService(e);
356         }
357     }
358 
359     @Override
onCarDisconnected()360     protected void onCarDisconnected() {
361         mStateListeners.clear();
362         mNavigationStateListeners.clear();
363     }
364 }
365