1 /*
2  * Copyright (C) 2019 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;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.ActivityManager;
23 import android.car.Car;
24 import android.car.CarInfoManager;
25 import android.car.CarOccupantZoneManager;
26 import android.car.CarOccupantZoneManager.OccupantTypeEnum;
27 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
28 import android.car.ICarOccupantZone;
29 import android.car.ICarOccupantZoneCallback;
30 import android.car.VehicleAreaSeat;
31 import android.car.media.CarAudioManager;
32 import android.car.user.CarUserManager;
33 import android.car.user.CarUserManager.UserLifecycleListener;
34 import android.content.ComponentName;
35 import android.content.Context;
36 import android.content.pm.PackageManager;
37 import android.content.pm.UserInfo;
38 import android.content.res.Resources;
39 import android.hardware.display.DisplayManager;
40 import android.os.Handler;
41 import android.os.Looper;
42 import android.os.RemoteCallbackList;
43 import android.os.RemoteException;
44 import android.os.UserHandle;
45 import android.os.UserManager;
46 import android.util.ArrayMap;
47 import android.util.ArraySet;
48 import android.util.IntArray;
49 import android.util.Log;
50 import android.util.SparseIntArray;
51 import android.view.Display;
52 import android.view.DisplayAddress;
53 
54 import com.android.car.user.CarUserService;
55 import com.android.internal.annotations.GuardedBy;
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.internal.car.ICarServiceHelper;
58 
59 import java.io.PrintWriter;
60 import java.util.ArrayList;
61 import java.util.HashMap;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Objects;
65 
66 /**
67  * Service to implement CarOccupantZoneManager API.
68  */
69 public final class CarOccupantZoneService extends ICarOccupantZone.Stub
70         implements CarServiceBase {
71 
72     private static final String TAG = CarLog.TAG_OCCUPANT;
73     private static final String ALL_COMPONENTS = "*";
74 
75     private final Object mLock = new Object();
76     private final Context mContext;
77     private final DisplayManager mDisplayManager;
78     private final UserManager mUserManager;
79 
80     private final boolean mEnableProfileUserAssignmentForMultiDisplay;
81 
82     private boolean mEnableSourcePreferred;
83     private ArrayList<ComponentName> mSourcePreferredComponents;
84 
85     /**
86      * Stores android user id of profile users for the current user.
87      */
88     @GuardedBy("mLock")
89     private final ArraySet<Integer> mProfileUsers = new ArraySet<>();
90 
91     /** key: zone id */
92     @GuardedBy("mLock")
93     private final HashMap<Integer, OccupantZoneInfo> mOccupantsConfig = new HashMap<>();
94 
95     @VisibleForTesting
96     static class DisplayConfig {
97         public final int displayType;
98         public final int occupantZoneId;
99 
DisplayConfig(int displayType, int occupantZoneId)100         DisplayConfig(int displayType, int occupantZoneId) {
101             this.displayType = displayType;
102             this.occupantZoneId = occupantZoneId;
103         }
104 
105         @Override
toString()106         public String toString() {
107             // do not include type as this is only used for dump
108             StringBuilder b = new StringBuilder(64);
109             b.append("{displayType=");
110             b.append(Integer.toHexString(displayType));
111             b.append(" occupantZoneId=");
112             b.append(occupantZoneId);
113             b.append("}");
114             return b.toString();
115         }
116     }
117 
118     /** key: display port address */
119     @GuardedBy("mLock")
120     private final HashMap<Integer, DisplayConfig> mDisplayConfigs = new HashMap<>();
121 
122     /** key: audio zone id */
123     @GuardedBy("mLock")
124     private final SparseIntArray mAudioZoneIdToOccupantZoneIdMapping = new SparseIntArray();
125 
126     @VisibleForTesting
127     static class DisplayInfo {
128         public final Display display;
129         public final int displayType;
130 
DisplayInfo(Display display, int displayType)131         DisplayInfo(Display display, int displayType) {
132             this.display = display;
133             this.displayType = displayType;
134         }
135 
136         @Override
toString()137         public String toString() {
138             // do not include type as this is only used for dump
139             StringBuilder b = new StringBuilder(64);
140             b.append("{displayId=");
141             b.append(display.getDisplayId());
142             b.append(" displayType=");
143             b.append(displayType);
144             b.append("}");
145             return b.toString();
146         }
147     }
148 
149     @VisibleForTesting
150     static class OccupantConfig {
151         public int userId = UserHandle.USER_NULL;
152         public final ArrayList<DisplayInfo> displayInfos = new ArrayList<>();
153         public int audioZoneId = CarAudioManager.INVALID_AUDIO_ZONE;
154 
155         @Override
toString()156         public String toString() {
157             // do not include type as this is only used for dump
158             StringBuilder b = new StringBuilder(128);
159             b.append("{userId=");
160             b.append(userId);
161             b.append(" displays=");
162             for (int i = 0; i < displayInfos.size(); i++) {
163                 b.append(displayInfos.get(i).toString());
164             }
165             b.append(" audioZoneId=");
166             if (audioZoneId != CarAudioManager.INVALID_AUDIO_ZONE) {
167                 b.append(audioZoneId);
168             } else {
169                 b.append("none");
170             }
171             b.append("}");
172             return b.toString();
173         }
174     }
175 
176     /** key : zoneId */
177     @GuardedBy("mLock")
178     private final HashMap<Integer, OccupantConfig> mActiveOccupantConfigs = new HashMap<>();
179 
180     @GuardedBy("mLock")
181     private ICarServiceHelper mICarServiceHelper;
182 
183     @GuardedBy("mLock")
184     private int mDriverZoneId = OccupantZoneInfo.INVALID_ZONE_ID;
185 
186     @VisibleForTesting
187     final UserLifecycleListener mUserLifecycleListener = event -> {
188         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
189             Log.d(CarLog.TAG_MEDIA, "onEvent(" + event + ")");
190         }
191         if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
192             handleUserChange();
193         }
194     };
195 
196     final CarUserService.PassengerCallback mPassengerCallback =
197             new CarUserService.PassengerCallback() {
198                 @Override
199                 public void onPassengerStarted(@UserIdInt int passengerId, int zoneId) {
200                     handlePassengerStarted(passengerId, zoneId);
201                 }
202 
203                 @Override
204                 public void onPassengerStopped(@UserIdInt int passengerId) {
205                     handlePassengerStopped(passengerId);
206                 }
207             };
208 
209     @VisibleForTesting
210     final DisplayManager.DisplayListener mDisplayListener =
211             new DisplayManager.DisplayListener() {
212                 @Override
213                 public void onDisplayAdded(int displayId) {
214                     handleDisplayChange();
215                 }
216 
217                 @Override
218                 public void onDisplayRemoved(int displayId) {
219                     handleDisplayChange();
220                 }
221 
222                 @Override
223                 public void onDisplayChanged(int displayId) {
224                     // nothing to do
225                 }
226             };
227 
228     private final RemoteCallbackList<ICarOccupantZoneCallback> mClientCallbacks =
229             new RemoteCallbackList<>();
230 
231     @GuardedBy("mLock")
232     private int mDriverSeat = VehicleAreaSeat.SEAT_UNKNOWN;
233 
CarOccupantZoneService(Context context)234     public CarOccupantZoneService(Context context) {
235         this(context, context.getSystemService(DisplayManager.class),
236                 context.getSystemService(UserManager.class),
237                 context.getResources().getBoolean(
238                         R.bool.enableProfileUserAssignmentForMultiDisplay)
239                         && context.getPackageManager().hasSystemFeature(
240                         PackageManager.FEATURE_MANAGED_USERS));
241     }
242 
243     @VisibleForTesting
CarOccupantZoneService(Context context, DisplayManager displayManager, UserManager userManager, boolean enableProfileUserAssignmentForMultiDisplay)244     public CarOccupantZoneService(Context context, DisplayManager displayManager,
245             UserManager userManager, boolean enableProfileUserAssignmentForMultiDisplay) {
246         mContext = context;
247         mDisplayManager = displayManager;
248         mUserManager = userManager;
249         mEnableProfileUserAssignmentForMultiDisplay = enableProfileUserAssignmentForMultiDisplay;
250     }
251 
252     @Override
init()253     public void init() {
254         // This does not require connection as binder will be passed directly.
255         Car car = new Car(mContext, /* service= */null, /* handler= */ null);
256         CarInfoManager infoManager = new CarInfoManager(car, CarLocalServices.getService(
257                 CarPropertyService.class));
258         int driverSeat = infoManager.getDriverSeat();
259         synchronized (mLock) {
260             mDriverSeat = driverSeat;
261             parseOccupantZoneConfigsLocked();
262             parseDisplayConfigsLocked();
263             handleActiveDisplaysLocked();
264             handleAudioZoneChangesLocked();
265             handleUserChangesLocked();
266         }
267         CarUserService userService = CarLocalServices.getService(CarUserService.class);
268         userService.addUserLifecycleListener(mUserLifecycleListener);
269         userService.addPassengerCallback(mPassengerCallback);
270         mDisplayManager.registerDisplayListener(mDisplayListener,
271                 new Handler(Looper.getMainLooper()));
272         CarUserService.ZoneUserBindingHelper helper = new CarUserService.ZoneUserBindingHelper() {
273             @Override
274             @NonNull
275             public List<OccupantZoneInfo> getOccupantZones(@OccupantTypeEnum int occupantType) {
276                 List<OccupantZoneInfo> zones = new ArrayList<OccupantZoneInfo>();
277                 for (OccupantZoneInfo ozi : getAllOccupantZones()) {
278                     if (ozi.occupantType == occupantType) {
279                         zones.add(ozi);
280                     }
281                 }
282                 return zones;
283             }
284 
285             @Override
286             public boolean assignUserToOccupantZone(@UserIdInt int userId, int zoneId) {
287                 // Check if the user is already assigned to the other zone.
288                 synchronized (mLock) {
289                     for (Map.Entry<Integer, OccupantConfig> entry :
290                             mActiveOccupantConfigs.entrySet()) {
291                         OccupantConfig config = entry.getValue();
292                         if (config.userId == userId && zoneId != entry.getKey()) {
293                             Log.w(TAG, "cannot assign user to two different zone simultaneously");
294                             return false;
295                         }
296                     }
297                     OccupantConfig zoneConfig = mActiveOccupantConfigs.get(zoneId);
298                     if (zoneConfig == null) {
299                         Log.w(TAG, "cannot find the zone(" + zoneId + ")");
300                         return false;
301                     }
302                     if (zoneConfig.userId != UserHandle.USER_NULL && zoneConfig.userId != userId) {
303                         Log.w(TAG, "other user already occupies the zone(" + zoneId + ")");
304                         return false;
305                     }
306                     zoneConfig.userId = userId;
307                     return true;
308                 }
309             }
310 
311             @Override
312             public boolean unassignUserFromOccupantZone(@UserIdInt int userId) {
313                 synchronized (mLock) {
314                     for (OccupantConfig config : mActiveOccupantConfigs.values()) {
315                         if (config.userId == userId) {
316                             config.userId = UserHandle.USER_NULL;
317                             break;
318                         }
319                     }
320                     return true;
321                 }
322             }
323 
324             @Override
325             public boolean isPassengerDisplayAvailable() {
326                 for (OccupantZoneInfo ozi : getAllOccupantZones()) {
327                     if (getDisplayForOccupant(ozi.zoneId,
328                             CarOccupantZoneManager.DISPLAY_TYPE_MAIN) != Display.INVALID_DISPLAY
329                             && ozi.occupantType != CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
330                         return true;
331                     }
332                 }
333                 return false;
334             }
335         };
336         userService.setZoneUserBindingHelper(helper);
337     }
338 
339     @Override
release()340     public void release() {
341         mDisplayManager.unregisterDisplayListener(mDisplayListener);
342         CarUserService userService = CarLocalServices.getService(CarUserService.class);
343         userService.removeUserLifecycleListener(mUserLifecycleListener);
344         userService.removePassengerCallback(mPassengerCallback);
345         synchronized (mLock) {
346             mOccupantsConfig.clear();
347             mDisplayConfigs.clear();
348             mAudioZoneIdToOccupantZoneIdMapping.clear();
349             mActiveOccupantConfigs.clear();
350         }
351     }
352 
353     /** Return cloned mOccupantsConfig for testing */
354     @VisibleForTesting
355     @NonNull
getOccupantsConfig()356     public HashMap<Integer, OccupantZoneInfo> getOccupantsConfig() {
357         synchronized (mLock) {
358             return (HashMap<Integer, OccupantZoneInfo>) mOccupantsConfig.clone();
359         }
360     }
361 
362     /** Return cloned mDisplayConfigs for testing */
363     @VisibleForTesting
364     @NonNull
getDisplayConfigs()365     public HashMap<Integer, DisplayConfig> getDisplayConfigs() {
366         synchronized (mLock) {
367             return (HashMap<Integer, DisplayConfig>) mDisplayConfigs.clone();
368         }
369     }
370 
371     /** Return cloned mAudioConfigs for testing */
372     @VisibleForTesting
373     @NonNull
getAudioConfigs()374     SparseIntArray getAudioConfigs() {
375         synchronized (mLock) {
376             return mAudioZoneIdToOccupantZoneIdMapping.clone();
377         }
378     }
379 
380     /** Return cloned mActiveOccupantConfigs for testing */
381     @VisibleForTesting
382     @NonNull
getActiveOccupantConfigs()383     public HashMap<Integer, OccupantConfig> getActiveOccupantConfigs() {
384         synchronized (mLock) {
385             return (HashMap<Integer, OccupantConfig>) mActiveOccupantConfigs.clone();
386         }
387     }
388 
389     @Override
dump(PrintWriter writer)390     public void dump(PrintWriter writer) {
391         writer.println("*OccupantZoneService*");
392         synchronized (mLock) {
393             writer.println("**mOccupantsConfig**");
394             for (Map.Entry<Integer, OccupantZoneInfo> entry : mOccupantsConfig.entrySet()) {
395                 writer.println(" zoneId=" + entry.getKey()
396                         + " info=" + entry.getValue().toString());
397             }
398             writer.println("**mDisplayConfigs**");
399             for (Map.Entry<Integer, DisplayConfig> entry : mDisplayConfigs.entrySet()) {
400                 writer.println(" port=" + Integer.toHexString(entry.getKey())
401                         + " config=" + entry.getValue().toString());
402             }
403             writer.println("**mAudioZoneIdToOccupantZoneIdMapping**");
404             for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
405                 int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
406                 writer.println(" audioZoneId=" + Integer.toHexString(audioZoneId)
407                         + " zoneId=" + mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId));
408             }
409             writer.println("**mActiveOccupantConfigs**");
410             for (Map.Entry<Integer, OccupantConfig> entry : mActiveOccupantConfigs.entrySet()) {
411                 writer.println(" zoneId=" + entry.getKey()
412                         + " config=" + entry.getValue().toString());
413             }
414             writer.println("mEnableProfileUserAssignmentForMultiDisplay:"
415                     + mEnableProfileUserAssignmentForMultiDisplay);
416             writer.println("mEnableSourcePreferred:"
417                     + mEnableSourcePreferred);
418             writer.append("mSourcePreferredComponents: [");
419             if (mSourcePreferredComponents != null) {
420                 for (int i = 0; i < mSourcePreferredComponents.size(); ++i) {
421                     if (i > 0) writer.append(' ');
422                     writer.append(mSourcePreferredComponents.get(i).toString());
423                 }
424             }
425             writer.println(']');
426         }
427     }
428 
429     @Override
getAllOccupantZones()430     public List<OccupantZoneInfo> getAllOccupantZones() {
431         synchronized (mLock) {
432             List<OccupantZoneInfo> infos = new ArrayList<>();
433             for (Integer zoneId : mActiveOccupantConfigs.keySet()) {
434                 // no need for deep copy as OccupantZoneInfo itself is static.
435                 infos.add(mOccupantsConfig.get(zoneId));
436             }
437             return infos;
438         }
439     }
440 
441     @Override
getAllDisplaysForOccupantZone(int occupantZoneId)442     public int[] getAllDisplaysForOccupantZone(int occupantZoneId) {
443         synchronized (mLock) {
444             OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
445             if (config == null) {
446                 return new int[0];
447             }
448             int[] displayIds = new int[config.displayInfos.size()];
449             for (int i = 0; i < config.displayInfos.size(); i++) {
450                 displayIds[i] = config.displayInfos.get(i).display.getDisplayId();
451             }
452             return displayIds;
453         }
454     }
455 
456     @Override
getDisplayForOccupant(int occupantZoneId, int displayType)457     public int getDisplayForOccupant(int occupantZoneId, int displayType) {
458         synchronized (mLock) {
459             OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
460             if (config == null) {
461                 return Display.INVALID_DISPLAY;
462             }
463             for (int i = 0; i < config.displayInfos.size(); i++) {
464                 if (displayType == config.displayInfos.get(i).displayType) {
465                     return config.displayInfos.get(i).display.getDisplayId();
466                 }
467             }
468         }
469         return Display.INVALID_DISPLAY;
470     }
471 
472     @Override
getAudioZoneIdForOccupant(int occupantZoneId)473     public int getAudioZoneIdForOccupant(int occupantZoneId) {
474         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
475         synchronized (mLock) {
476             OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
477             if (config != null) {
478                 return config.audioZoneId;
479             }
480             // check if the occupant id exist at all
481             if (!mOccupantsConfig.containsKey(occupantZoneId)) {
482                 return CarAudioManager.INVALID_AUDIO_ZONE;
483             }
484             // Exist but not active
485             return getAudioZoneIdForOccupantLocked(occupantZoneId);
486         }
487     }
488 
getAudioZoneIdForOccupantLocked(int occupantZoneId)489     private int getAudioZoneIdForOccupantLocked(int occupantZoneId) {
490         for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
491             int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
492             if (occupantZoneId == mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId)) {
493                 return audioZoneId;
494             }
495         }
496         return CarAudioManager.INVALID_AUDIO_ZONE;
497     }
498 
499     @Override
getOccupantForAudioZoneId(int audioZoneId)500     public CarOccupantZoneManager.OccupantZoneInfo getOccupantForAudioZoneId(int audioZoneId) {
501         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
502         synchronized (mLock) {
503             int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId,
504                     OccupantZoneInfo.INVALID_ZONE_ID);
505             if (occupantZoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
506                 return null;
507             }
508             // To support headless zones return the occupant configuration.
509             return mOccupantsConfig.get(occupantZoneId);
510         }
511     }
512 
513     @Nullable
findDisplayConfigForDisplayLocked(int displayId)514     private DisplayConfig findDisplayConfigForDisplayLocked(int displayId) {
515         for (Map.Entry<Integer, DisplayConfig> entry : mDisplayConfigs.entrySet()) {
516             Display display = mDisplayManager.getDisplay(displayId);
517             if (display == null) {
518                 continue;
519             }
520             Byte portAddress = getPortAddress(display);
521             if (portAddress == null) {
522                 continue;
523             }
524             DisplayConfig config =
525                     mDisplayConfigs.get(Byte.toUnsignedInt(portAddress));
526             return config;
527         }
528         return null;
529     }
530 
531     @Override
getDisplayType(int displayId)532     public int getDisplayType(int displayId) {
533         synchronized (mLock) {
534             DisplayConfig config = findDisplayConfigForDisplayLocked(displayId);
535             if (config != null) {
536                 return config.displayType;
537             }
538         }
539         return CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN;
540     }
541 
542     @Override
getUserForOccupant(int occupantZoneId)543     public int getUserForOccupant(int occupantZoneId) {
544         synchronized (mLock) {
545             OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
546             if (config == null) {
547                 return UserHandle.USER_NULL;
548             }
549             return config.userId;
550         }
551     }
552 
553     @Override
getOccupantZoneIdForUserId(int userId)554     public int getOccupantZoneIdForUserId(int userId) {
555         synchronized (mLock) {
556             for (int occupantZoneId : mActiveOccupantConfigs.keySet()) {
557                 OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
558                 if (config.userId == userId) {
559                     return occupantZoneId;
560                 }
561             }
562             Log.w(TAG, "Could not find occupantZoneId for userId" + userId
563                     + " returning invalid occupant zone id " + OccupantZoneInfo.INVALID_ZONE_ID);
564             return OccupantZoneInfo.INVALID_ZONE_ID;
565         }
566     }
567 
568     /**
569      * returns the current driver user id.
570      */
getDriverUserId()571     public @UserIdInt int getDriverUserId() {
572         return getCurrentUser();
573     }
574 
575     /**
576      * Sets the mapping for audio zone id to occupant zone id.
577      *
578      * @param audioZoneIdToOccupantZoneMapping map for audio zone id, where key is the audio zone id
579      * and value is the occupant zone id.
580      */
setAudioZoneIdsForOccupantZoneIds( @onNull SparseIntArray audioZoneIdToOccupantZoneMapping)581     public void setAudioZoneIdsForOccupantZoneIds(
582             @NonNull SparseIntArray audioZoneIdToOccupantZoneMapping) {
583         Objects.requireNonNull(audioZoneIdToOccupantZoneMapping,
584                 "audioZoneIdToOccupantZoneMapping can not be null");
585         synchronized (mLock) {
586             validateOccupantZoneIdsLocked(audioZoneIdToOccupantZoneMapping);
587             mAudioZoneIdToOccupantZoneIdMapping.clear();
588             for (int index = 0; index < audioZoneIdToOccupantZoneMapping.size(); index++) {
589                 int audioZoneId = audioZoneIdToOccupantZoneMapping.keyAt(index);
590                 mAudioZoneIdToOccupantZoneIdMapping.put(audioZoneId,
591                         audioZoneIdToOccupantZoneMapping.get(audioZoneId));
592             }
593             //If there are any active displays for the zone send change event
594             handleAudioZoneChangesLocked();
595         }
596         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_AUDIO);
597     }
598 
validateOccupantZoneIdsLocked(SparseIntArray audioZoneIdToOccupantZoneMapping)599     private void validateOccupantZoneIdsLocked(SparseIntArray audioZoneIdToOccupantZoneMapping) {
600         for (int i = 0; i < audioZoneIdToOccupantZoneMapping.size(); i++) {
601             int occupantZoneId =
602                     audioZoneIdToOccupantZoneMapping.get(audioZoneIdToOccupantZoneMapping.keyAt(i));
603             if (!mOccupantsConfig.containsKey(occupantZoneId)) {
604                 throw new IllegalArgumentException("occupantZoneId " + occupantZoneId
605                         + " does not exist.");
606             }
607         }
608     }
609 
610     @Override
registerCallback(ICarOccupantZoneCallback callback)611     public void registerCallback(ICarOccupantZoneCallback callback) {
612         mClientCallbacks.register(callback);
613     }
614 
615     @Override
unregisterCallback(ICarOccupantZoneCallback callback)616     public void unregisterCallback(ICarOccupantZoneCallback callback) {
617         mClientCallbacks.unregister(callback);
618     }
619 
620     @Override
assignProfileUserToOccupantZone(int occupantZoneId, int userId)621     public boolean assignProfileUserToOccupantZone(int occupantZoneId, int userId) {
622         enforcePermission(android.Manifest.permission.MANAGE_USERS);
623         if (!mEnableProfileUserAssignmentForMultiDisplay) {
624             throw new IllegalStateException("feature not enabled");
625         }
626         int currentUser = getCurrentUser();
627 
628         synchronized (mLock) {
629             if (occupantZoneId == mDriverZoneId) {
630                 throw new IllegalArgumentException("Driver zone cannot have profile user");
631             }
632             updateEnabledProfilesLocked(currentUser);
633 
634             if (!mProfileUsers.contains(userId) && userId != UserHandle.USER_NULL) {
635                 // current user can change while this call is happening, so return false rather
636                 // than throwing exception
637                 Log.w(TAG, "Invalid profile user id:" + userId);
638                 return false;
639             }
640             if (!mUserManager.isUserRunning(userId)) {
641                 Log.w(TAG, "User is not running:" + userId);
642                 return false;
643             }
644             OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
645             if (config == null) {
646                 throw new IllegalArgumentException("Invalid occupantZoneId:" + occupantZoneId);
647             }
648             if (config.userId == userId && userId != UserHandle.USER_NULL) {
649                 Log.w(TAG, "assignProfileUserToOccupantZone zone:"
650                         + occupantZoneId + " already set to user:" + userId);
651                 return true;
652             }
653             if (userId == UserHandle.USER_NULL) {
654                 config.userId = currentUser;
655             } else {
656                 config.userId = userId;
657             }
658         }
659         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
660         return true;
661     }
662 
663     /**
664      * Sets {@code ICarServiceHelper}.
665      */
setCarServiceHelper(ICarServiceHelper helper)666     public void setCarServiceHelper(ICarServiceHelper helper) {
667         doSyncWithCarServiceHelper(helper, /* updateDisplay= */ true, /* updateUser= */ true,
668                 /* updateConfig= */ true);
669     }
670 
doSyncWithCarServiceHelper(@ullable ICarServiceHelper helper, boolean updateDisplay, boolean updateUser, boolean updateConfig)671     private void doSyncWithCarServiceHelper(@Nullable ICarServiceHelper helper,
672             boolean updateDisplay, boolean updateUser, boolean updateConfig) {
673         int[] passengerDisplays = null;
674         ArrayMap<Integer, IntArray> whitelists = null;
675         ICarServiceHelper helperToUse = helper;
676         synchronized (mLock) {
677             if (helper == null) {
678                 if (mICarServiceHelper == null) { // helper not set yet.
679                     return;
680                 }
681                 helperToUse = mICarServiceHelper;
682             } else {
683                 mICarServiceHelper = helper;
684             }
685             if (updateDisplay) {
686                 passengerDisplays = getAllActivePassengerDisplaysLocked();
687             }
688             if (updateUser) {
689                 whitelists = createDisplayWhitelistsLocked();
690             }
691         }
692         if (updateDisplay) {
693             updatePassengerDisplays(helperToUse, passengerDisplays);
694         }
695         if (updateUser) {
696             updateUserAssignmentForDisplays(helperToUse, whitelists);
697         }
698         if (updateConfig) {
699             Resources res = mContext.getResources();
700             String[] components = res.getStringArray(R.array.config_sourcePreferredComponents);
701             updateSourcePreferredComponents(helperToUse, components);
702         }
703     }
704 
getAllActivePassengerDisplaysLocked()705     private int[] getAllActivePassengerDisplaysLocked() {
706         IntArray displays = new IntArray();
707         for (Map.Entry<Integer, OccupantConfig> entry : mActiveOccupantConfigs.entrySet()) {
708             Integer zoneId = entry.getKey();
709             if (zoneId == mDriverZoneId) {
710                 continue;
711             }
712             OccupantConfig config = entry.getValue();
713             for (int i = 0; i < config.displayInfos.size(); i++) {
714                 displays.add(config.displayInfos.get(i).display.getDisplayId());
715             }
716         }
717         return displays.toArray();
718     }
719 
updatePassengerDisplays(ICarServiceHelper helper, int[] passengerDisplayIds)720     private void updatePassengerDisplays(ICarServiceHelper helper, int[] passengerDisplayIds) {
721         if (passengerDisplayIds == null) {
722             return;
723         }
724         try {
725             helper.setPassengerDisplays(passengerDisplayIds);
726         } catch (RemoteException e) {
727             Log.e(TAG, "ICarServiceHelper.setPassengerDisplays failed");
728         }
729     }
730 
updateSourcePreferredComponents(ICarServiceHelper helper, String[] components)731     private void updateSourcePreferredComponents(ICarServiceHelper helper, String[] components) {
732         boolean enableSourcePreferred;
733         ArrayList<ComponentName> componentNames = null;
734         if (components == null || components.length == 0) {
735             enableSourcePreferred = false;
736             Log.i(TAG, "CarLaunchParamsModifier: disable source-preferred");
737         } else if (components.length == 1 && components[0].equals(ALL_COMPONENTS)) {
738             enableSourcePreferred = true;
739             Log.i(TAG, "CarLaunchParamsModifier: enable source-preferred for all Components");
740         } else {
741             componentNames = new ArrayList<>((components.length));
742             for (String item : components) {
743                 ComponentName name = ComponentName.unflattenFromString(item);
744                 if (name == null) {
745                     Log.e(TAG, "CarLaunchParamsModifier: Wrong ComponentName=" + item);
746                     return;
747                 }
748                 componentNames.add(name);
749             }
750             enableSourcePreferred = true;
751         }
752         try {
753             helper.setSourcePreferredComponents(enableSourcePreferred, componentNames);
754             mEnableSourcePreferred = enableSourcePreferred;
755             mSourcePreferredComponents = componentNames;
756         } catch (RemoteException e) {
757             Log.e(TAG, "ICarServiceHelper.setSourcePreferredComponents failed");
758         }
759     }
760 
createDisplayWhitelistsLocked()761     private ArrayMap<Integer, IntArray> createDisplayWhitelistsLocked() {
762         ArrayMap<Integer, IntArray> whitelists = new ArrayMap<>();
763         for (Map.Entry<Integer, OccupantConfig> entry : mActiveOccupantConfigs.entrySet()) {
764             Integer zoneId = entry.getKey();
765             if (zoneId == mDriverZoneId) {
766                 continue;
767             }
768             OccupantConfig config = entry.getValue();
769             if (config.displayInfos.isEmpty()) {
770                 continue;
771             }
772             // user like driver can have multiple zones assigned, so add them all.
773             IntArray displays = whitelists.get(config.userId);
774             if (displays == null) {
775                 displays = new IntArray();
776                 whitelists.put(config.userId, displays);
777             }
778             for (int i = 0; i < config.displayInfos.size(); i++) {
779                 displays.add(config.displayInfos.get(i).display.getDisplayId());
780             }
781         }
782         return whitelists;
783     }
784 
updateUserAssignmentForDisplays(ICarServiceHelper helper, ArrayMap<Integer, IntArray> whitelists)785     private void updateUserAssignmentForDisplays(ICarServiceHelper helper,
786             ArrayMap<Integer, IntArray> whitelists) {
787         if (whitelists == null || whitelists.isEmpty()) {
788             return;
789         }
790         try {
791             for (int i = 0; i < whitelists.size(); i++) {
792                 int userId = whitelists.keyAt(i);
793                 helper.setDisplayWhitelistForUser(userId, whitelists.valueAt(i).toArray());
794             }
795         } catch (RemoteException e) {
796             Log.e(TAG, "ICarServiceHelper.setDisplayWhitelistForUser failed");
797         }
798     }
799 
throwFormatErrorInOccupantZones(String msg)800     private void throwFormatErrorInOccupantZones(String msg) {
801         throw new RuntimeException("Format error in config_occupant_zones resource:" + msg);
802     }
803 
804     // For overriding in test
805     @VisibleForTesting
getDriverSeat()806     int getDriverSeat() {
807         synchronized (mLock) {
808             return mDriverSeat;
809         }
810     }
811 
parseOccupantZoneConfigsLocked()812     private void parseOccupantZoneConfigsLocked() {
813         final Resources res = mContext.getResources();
814         // examples:
815         // <item>occupantZoneId=0,occupantType=DRIVER,seatRow=1,seatSide=driver</item>
816         // <item>occupantZoneId=1,occupantType=FRONT_PASSENGER,seatRow=1,
817         // searSide=oppositeDriver</item>
818         boolean hasDriver = false;
819         int driverSeat = getDriverSeat();
820         int driverSeatSide = VehicleAreaSeat.SIDE_LEFT; // default LHD : Left Hand Drive
821         if (driverSeat == VehicleAreaSeat.SEAT_ROW_1_RIGHT) {
822             driverSeatSide = VehicleAreaSeat.SIDE_RIGHT;
823         }
824         int maxZoneId = OccupantZoneInfo.INVALID_ZONE_ID;
825         for (String config : res.getStringArray(R.array.config_occupant_zones)) {
826             int zoneId = OccupantZoneInfo.INVALID_ZONE_ID;
827             int type = CarOccupantZoneManager.OCCUPANT_TYPE_INVALID;
828             int seatRow = 0; // invalid row
829             int seatSide = VehicleAreaSeat.SIDE_LEFT;
830             String[] entries = config.split(",");
831             for (String entry : entries) {
832                 String[] keyValuePair = entry.split("=");
833                 if (keyValuePair.length != 2) {
834                     throwFormatErrorInOccupantZones("No key/value pair:" + entry);
835                 }
836                 switch (keyValuePair[0]) {
837                     case "occupantZoneId":
838                         zoneId = Integer.parseInt(keyValuePair[1]);
839                         break;
840                     case "occupantType":
841                         switch (keyValuePair[1]) {
842                             case "DRIVER":
843                                 type = CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER;
844                                 break;
845                             case "FRONT_PASSENGER":
846                                 type = CarOccupantZoneManager.OCCUPANT_TYPE_FRONT_PASSENGER;
847                                 break;
848                             case "REAR_PASSENGER":
849                                 type = CarOccupantZoneManager.OCCUPANT_TYPE_REAR_PASSENGER;
850                                 break;
851                             default:
852                                 throwFormatErrorInOccupantZones("Unrecognized type:" + entry);
853                                 break;
854                         }
855                         break;
856                     case "seatRow":
857                         seatRow = Integer.parseInt(keyValuePair[1]);
858                         break;
859                     case "seatSide":
860                         switch (keyValuePair[1]) {
861                             case "driver":
862                                 seatSide = driverSeatSide;
863                                 break;
864                             case "oppositeDriver":
865                                 seatSide = -driverSeatSide;
866                                 break;
867                             case "left":
868                                 seatSide = VehicleAreaSeat.SIDE_LEFT;
869                                 break;
870                             case "center":
871                                 seatSide = VehicleAreaSeat.SIDE_CENTER;
872                                 break;
873                             case "right":
874                                 seatSide = VehicleAreaSeat.SIDE_RIGHT;
875                                 break;
876                             default:
877                                 throwFormatErrorInOccupantZones("Unregognized seatSide:" + entry);
878                                 break;
879 
880                         }
881                         break;
882                     default:
883                         throwFormatErrorInOccupantZones("Unrecognized key:" + entry);
884                         break;
885                 }
886             }
887             if (zoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
888                 throwFormatErrorInOccupantZones("Missing zone id:" + config);
889             }
890             if (zoneId > maxZoneId) {
891                 maxZoneId = zoneId;
892             }
893             if (type == CarOccupantZoneManager.OCCUPANT_TYPE_INVALID) {
894                 throwFormatErrorInOccupantZones("Missing type:" + config);
895             }
896             if (type == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
897                 if (hasDriver) {
898                     throwFormatErrorInOccupantZones("Multiple driver:" + config);
899                 } else {
900                     hasDriver = true;
901                     mDriverZoneId = zoneId;
902                 }
903             }
904             int seat = VehicleAreaSeat.fromRowAndSide(seatRow, seatSide);
905             if (seat == VehicleAreaSeat.SEAT_UNKNOWN) {
906                 throwFormatErrorInOccupantZones("Invalid seat:" + config);
907             }
908             OccupantZoneInfo info = new OccupantZoneInfo(zoneId, type, seat);
909             if (mOccupantsConfig.containsKey(zoneId)) {
910                 throwFormatErrorInOccupantZones("Duplicate zone id:" + config);
911             }
912             mOccupantsConfig.put(zoneId, info);
913         }
914         if (!hasDriver) {
915             maxZoneId++;
916             mDriverZoneId = maxZoneId;
917             Log.w(TAG, "No driver zone, add one:" + mDriverZoneId);
918             OccupantZoneInfo info = new OccupantZoneInfo(mDriverZoneId,
919                     CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER, getDriverSeat());
920             mOccupantsConfig.put(mDriverZoneId, info);
921         }
922     }
923 
throwFormatErrorInDisplayMapping(String msg)924     private void throwFormatErrorInDisplayMapping(String msg) {
925         throw new RuntimeException(
926                 "Format error in config_occupant_display_mapping resource:" + msg);
927     }
928 
parseDisplayConfigsLocked()929     private void parseDisplayConfigsLocked() {
930         final Resources res = mContext.getResources();
931         // examples:
932         // <item>displayPort=0,displayType=MAIN,occupantZoneId=0</item>
933         // <item>displayPort=1,displayType=INSTRUMENT_CLUSTER,occupantZoneId=0</item>
934         final int invalidPort = -1;
935         for (String config : res.getStringArray(R.array.config_occupant_display_mapping)) {
936             int port = invalidPort;
937             int type = CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN;
938             int zoneId = OccupantZoneInfo.INVALID_ZONE_ID;
939             String[] entries = config.split(",");
940             for (String entry : entries) {
941                 String[] keyValuePair = entry.split("=");
942                 if (keyValuePair.length != 2) {
943                     throwFormatErrorInDisplayMapping("No key/value pair:" + entry);
944                 }
945                 switch (keyValuePair[0]) {
946                     case "displayPort":
947                         port = Integer.parseInt(keyValuePair[1]);
948                         break;
949                     case "displayType":
950                         switch (keyValuePair[1]) {
951                             case "MAIN":
952                                 type = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
953                                 break;
954                             case "INSTRUMENT_CLUSTER":
955                                 type = CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER;
956                                 break;
957                             case "HUD":
958                                 type = CarOccupantZoneManager.DISPLAY_TYPE_HUD;
959                                 break;
960                             case "INPUT":
961                                 type = CarOccupantZoneManager.DISPLAY_TYPE_INPUT;
962                                 break;
963                             case "AUXILIARY":
964                                 type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY;
965                                 break;
966                             default:
967                                 throwFormatErrorInDisplayMapping(
968                                         "Unrecognized display type:" + entry);
969                                 break;
970                         }
971                         break;
972                     case "occupantZoneId":
973                         zoneId = Integer.parseInt(keyValuePair[1]);
974                         break;
975                     default:
976                         throwFormatErrorInDisplayMapping("Unrecognized key:" + entry);
977                         break;
978 
979                 }
980             }
981             // Now check validity
982             if (port == invalidPort) {
983                 throwFormatErrorInDisplayMapping("Missing or invalid displayPort:" + config);
984             }
985 
986             if (type == CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN) {
987                 throwFormatErrorInDisplayMapping("Missing or invalid displayType:" + config);
988             }
989             if (zoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
990                 throwFormatErrorInDisplayMapping("Missing or invalid occupantZoneId:" + config);
991             }
992             if (!mOccupantsConfig.containsKey(zoneId)) {
993                 throwFormatErrorInDisplayMapping(
994                         "Missing or invalid occupantZoneId:" + config);
995             }
996             if (mDisplayConfigs.containsKey(port)) {
997                 throwFormatErrorInDisplayMapping("Duplicate displayPort:" + config);
998             }
999             mDisplayConfigs.put(port, new DisplayConfig(type, zoneId));
1000         }
1001     }
1002 
getPortAddress(Display display)1003     private Byte getPortAddress(Display display) {
1004         DisplayAddress address = display.getAddress();
1005         if (address instanceof DisplayAddress.Physical) {
1006             DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) address;
1007             if (physicalAddress != null) {
1008                 return physicalAddress.getPort();
1009             }
1010         }
1011         return null;
1012     }
1013 
addDisplayInfoToOccupantZoneLocked(int zoneId, DisplayInfo info)1014     private void addDisplayInfoToOccupantZoneLocked(int zoneId, DisplayInfo info) {
1015         OccupantConfig occupantConfig = mActiveOccupantConfigs.get(zoneId);
1016         if (occupantConfig == null) {
1017             occupantConfig = new OccupantConfig();
1018             mActiveOccupantConfigs.put(zoneId, occupantConfig);
1019         }
1020         occupantConfig.displayInfos.add(info);
1021     }
1022 
handleActiveDisplaysLocked()1023     private void handleActiveDisplaysLocked() {
1024         mActiveOccupantConfigs.clear();
1025         boolean hasDefaultDisplayConfig = false;
1026         for (Display display : mDisplayManager.getDisplays()) {
1027             Byte rawPortAddress = getPortAddress(display);
1028             if (rawPortAddress == null) {
1029                 continue;
1030             }
1031 
1032             int portAddress = Byte.toUnsignedInt(rawPortAddress);
1033             DisplayConfig displayConfig = mDisplayConfigs.get(portAddress);
1034             if (displayConfig == null) {
1035                 Log.w(TAG,
1036                         "Display id:" + display.getDisplayId() + " port:" + portAddress
1037                                 + " does not have configurations");
1038                 continue;
1039             }
1040             if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
1041                 if (displayConfig.occupantZoneId != mDriverZoneId) {
1042                     throw new IllegalStateException(
1043                             "Default display should be only assigned to driver zone");
1044                 }
1045                 hasDefaultDisplayConfig = true;
1046             }
1047             addDisplayInfoToOccupantZoneLocked(displayConfig.occupantZoneId,
1048                     new DisplayInfo(display, displayConfig.displayType));
1049         }
1050         if (!hasDefaultDisplayConfig) {
1051             // Can reach here if default display has no port / no config
1052             Log.w(TAG, "Default display not assigned, will assign to driver zone");
1053             addDisplayInfoToOccupantZoneLocked(mDriverZoneId, new DisplayInfo(
1054                     mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY),
1055                     CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
1056         }
1057     }
1058 
1059     @VisibleForTesting
getCurrentUser()1060     int getCurrentUser() {
1061         return ActivityManager.getCurrentUser();
1062     }
1063 
updateEnabledProfilesLocked(int userId)1064     private void updateEnabledProfilesLocked(int userId) {
1065         mProfileUsers.clear();
1066         List<UserInfo> profileUsers = mUserManager.getEnabledProfiles(userId);
1067         for (UserInfo userInfo : profileUsers) {
1068             if (userInfo.id != userId) {
1069                 mProfileUsers.add(userInfo.id);
1070             }
1071         }
1072     }
1073 
handleUserChangesLocked()1074     private void handleUserChangesLocked() {
1075         int driverUserId = getCurrentUser();
1076 
1077         if (mEnableProfileUserAssignmentForMultiDisplay) {
1078             updateEnabledProfilesLocked(driverUserId);
1079         }
1080 
1081         for (Map.Entry<Integer, OccupantConfig> entry : mActiveOccupantConfigs.entrySet()) {
1082             Integer zoneId = entry.getKey();
1083             OccupantConfig config = entry.getValue();
1084             // mProfileUsers empty if not supported
1085             if (mProfileUsers.contains(config.userId)) {
1086                 Log.i(TAG, "Profile user:" + config.userId
1087                         + " already assigned for occupant zone:" + zoneId);
1088             } else {
1089                 config.userId = driverUserId;
1090             }
1091         }
1092     }
1093 
handleAudioZoneChangesLocked()1094     private void handleAudioZoneChangesLocked() {
1095         for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
1096             int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
1097             int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId);
1098             OccupantConfig occupantConfig =
1099                     mActiveOccupantConfigs.get(occupantZoneId);
1100             if (occupantConfig == null) {
1101                 //no active display for zone just continue
1102                 continue;
1103             }
1104             // Found an active configuration, add audio to it.
1105             occupantConfig.audioZoneId = audioZoneId;
1106         }
1107     }
1108 
sendConfigChangeEvent(int changeFlags)1109     private void sendConfigChangeEvent(int changeFlags) {
1110         boolean updateDisplay = false;
1111         boolean updateUser = false;
1112         if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0) {
1113             updateDisplay = true;
1114             updateUser = true;
1115         } else if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER) != 0) {
1116             updateUser = true;
1117         }
1118         doSyncWithCarServiceHelper(/* helper= */ null, updateDisplay, updateUser,
1119                 /* updateConfig= */ false);
1120 
1121         final int n = mClientCallbacks.beginBroadcast();
1122         for (int i = 0; i < n; i++) {
1123             ICarOccupantZoneCallback callback = mClientCallbacks.getBroadcastItem(i);
1124             try {
1125                 callback.onOccupantZoneConfigChanged(changeFlags);
1126             } catch (RemoteException ignores) {
1127                 // ignore
1128             }
1129         }
1130         mClientCallbacks.finishBroadcast();
1131     }
1132 
handleUserChange()1133     private void handleUserChange() {
1134         synchronized (mLock) {
1135             handleUserChangesLocked();
1136         }
1137         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
1138     }
1139 
handlePassengerStarted(@serIdInt int passengerId, int zoneId)1140     private void handlePassengerStarted(@UserIdInt int passengerId, int zoneId) {
1141         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
1142     }
1143 
handlePassengerStopped(@serIdInt int passengerId)1144     private void handlePassengerStopped(@UserIdInt int passengerId) {
1145         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
1146     }
1147 
handleDisplayChange()1148     private void handleDisplayChange() {
1149         synchronized (mLock) {
1150             handleActiveDisplaysLocked();
1151             //audio zones should be re-checked for changed display
1152             handleAudioZoneChangesLocked();
1153             // user should be re-checked for changed displays
1154             handleUserChangesLocked();
1155         }
1156         sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY);
1157     }
1158 
enforcePermission(String permissionName)1159     private void enforcePermission(String permissionName) {
1160         if (mContext.checkCallingOrSelfPermission(permissionName)
1161                 != PackageManager.PERMISSION_GRANTED) {
1162             throw new SecurityException("requires permission " + permissionName);
1163         }
1164     }
1165 }
1166