• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
20 import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
21 import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
22 import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_UNKNOWN;
23 import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_BASELINE;
24 
25 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.car.Car;
30 import android.car.drivingstate.CarDrivingStateEvent;
31 import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
32 import android.car.drivingstate.CarUxRestrictions;
33 import android.car.drivingstate.CarUxRestrictionsConfiguration;
34 import android.car.drivingstate.CarUxRestrictionsManager;
35 import android.car.drivingstate.ICarDrivingStateChangeListener;
36 import android.car.drivingstate.ICarUxRestrictionsChangeListener;
37 import android.car.drivingstate.ICarUxRestrictionsManager;
38 import android.car.hardware.CarPropertyValue;
39 import android.car.hardware.property.CarPropertyEvent;
40 import android.car.hardware.property.ICarPropertyEventListener;
41 import android.content.Context;
42 import android.content.pm.PackageManager;
43 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
44 import android.hardware.display.DisplayManager;
45 import android.os.Binder;
46 import android.os.Build;
47 import android.os.IBinder;
48 import android.os.Process;
49 import android.os.RemoteException;
50 import android.os.SystemClock;
51 import android.util.ArraySet;
52 import android.util.AtomicFile;
53 import android.util.JsonReader;
54 import android.util.JsonWriter;
55 import android.util.Log;
56 import android.util.Slog;
57 import android.view.Display;
58 import android.view.DisplayAddress;
59 
60 import com.android.car.systeminterface.SystemInterface;
61 import com.android.internal.annotations.GuardedBy;
62 import com.android.internal.annotations.VisibleForTesting;
63 
64 import org.xmlpull.v1.XmlPullParserException;
65 
66 import java.io.File;
67 import java.io.FileOutputStream;
68 import java.io.IOException;
69 import java.io.InputStreamReader;
70 import java.io.OutputStreamWriter;
71 import java.io.PrintWriter;
72 import java.nio.charset.StandardCharsets;
73 import java.nio.file.Files;
74 import java.nio.file.Path;
75 import java.util.ArrayList;
76 import java.util.HashMap;
77 import java.util.LinkedList;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.Set;
81 
82 /**
83  * A service that listens to current driving state of the vehicle and maps it to the
84  * appropriate UX restrictions for that driving state.
85  * <p>
86  * <h1>UX Restrictions Configuration</h1>
87  * When this service starts, it will first try reading the configuration set through
88  * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)}.
89  * If one is not available, it will try reading the configuration saved in
90  * {@code R.xml.car_ux_restrictions_map}. If XML is somehow unavailable, it will
91  * fall back to a hard-coded configuration.
92  * <p>
93  * <h1>Multi-Display</h1>
94  * Only physical displays that are available at service initialization are recognized.
95  * This service does not support pluggable displays.
96  */
97 public class CarUxRestrictionsManagerService extends ICarUxRestrictionsManager.Stub implements
98         CarServiceBase {
99     private static final String TAG = "CarUxR";
100     private static final boolean DBG = false;
101     private static final int MAX_TRANSITION_LOG_SIZE = 20;
102     private static final int PROPERTY_UPDATE_RATE = 5; // Update rate in Hz
103     private static final float SPEED_NOT_AVAILABLE = -1.0F;
104     private static final byte DEFAULT_PORT = 0;
105 
106     @VisibleForTesting
107     static final String CONFIG_FILENAME_PRODUCTION = "ux_restrictions_prod_config.json";
108     @VisibleForTesting
109     static final String CONFIG_FILENAME_STAGED = "ux_restrictions_staged_config.json";
110 
111     private final Context mContext;
112     private final DisplayManager mDisplayManager;
113     private final CarDrivingStateService mDrivingStateService;
114     private final CarPropertyService mCarPropertyService;
115     // List of clients listening to UX restriction events.
116     private final List<UxRestrictionsClient> mUxRClients = new ArrayList<>();
117 
118     private float mCurrentMovingSpeed;
119 
120     // Byte represents a physical port for display.
121     private byte mDefaultDisplayPhysicalPort;
122     private final List<Byte> mPhysicalPorts = new ArrayList<>();
123     // This lookup caches the mapping from an int display id
124     // to a byte that represents a physical port.
125     private final Map<Integer, Byte> mPortLookup = new HashMap<>();
126     private Map<Byte, CarUxRestrictionsConfiguration> mCarUxRestrictionsConfigurations;
127     private Map<Byte, CarUxRestrictions> mCurrentUxRestrictions;
128 
129     @CarUxRestrictionsManager.UxRestrictionMode
130     private int mRestrictionMode = UX_RESTRICTION_MODE_BASELINE;
131 
132     // Flag to disable broadcasting UXR changes - for development purposes
133     @GuardedBy("this")
134     private boolean mUxRChangeBroadcastEnabled = true;
135     // For dumpsys logging
136     private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>();
137 
CarUxRestrictionsManagerService(Context context, CarDrivingStateService drvService, CarPropertyService propertyService)138     public CarUxRestrictionsManagerService(Context context, CarDrivingStateService drvService,
139             CarPropertyService propertyService) {
140         mContext = context;
141         mDisplayManager = mContext.getSystemService(DisplayManager.class);
142         mDrivingStateService = drvService;
143         mCarPropertyService = propertyService;
144     }
145 
146     @Override
init()147     public synchronized void init() {
148         mDefaultDisplayPhysicalPort = getDefaultDisplayPhysicalPort();
149 
150         initPhysicalPort();
151 
152         // Unrestricted until driving state information is received. During boot up, we don't want
153         // everything to be blocked until data is available from CarPropertyManager.  If we start
154         // driving and we don't get speed or gear information, we have bigger problems.
155         mCurrentUxRestrictions = new HashMap<>();
156         for (byte port : mPhysicalPorts) {
157             mCurrentUxRestrictions.put(port, createUnrestrictedRestrictions());
158         }
159 
160         // subscribe to driving State
161         mDrivingStateService.registerDrivingStateChangeListener(
162                 mICarDrivingStateChangeEventListener);
163         // subscribe to property service for speed
164         mCarPropertyService.registerListener(VehicleProperty.PERF_VEHICLE_SPEED,
165                 PROPERTY_UPDATE_RATE, mICarPropertyEventListener);
166 
167         // At this point the driving state is known, which determines whether it's safe
168         // to promote staged new config.
169         mCarUxRestrictionsConfigurations = convertToMap(loadConfig());
170 
171         initializeUxRestrictions();
172     }
173 
174     @Override
getConfigs()175     public List<CarUxRestrictionsConfiguration> getConfigs() {
176         return new ArrayList<>(mCarUxRestrictionsConfigurations.values());
177     }
178 
179     /**
180      * Loads UX restrictions configurations and returns them.
181      *
182      * <p>Reads config from the following sources in order:
183      * <ol>
184      * <li>saved config set by
185      * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)};
186      * <li>XML resource config from {@code R.xml.car_ux_restrictions_map};
187      * <li>hardcoded default config.
188      * </ol>
189      *
190      * This method attempts to promote staged config file. Doing which depends on driving state.
191      */
192     @VisibleForTesting
loadConfig()193     synchronized List<CarUxRestrictionsConfiguration> loadConfig() {
194         promoteStagedConfig();
195         List<CarUxRestrictionsConfiguration> configs;
196 
197         // Production config, if available, is the first choice.
198         File prodConfig = getFile(CONFIG_FILENAME_PRODUCTION);
199         if (prodConfig.exists()) {
200             logd("Attempting to read production config");
201             configs = readPersistedConfig(prodConfig);
202             if (configs != null) {
203                 return configs;
204             }
205         }
206 
207         // XML config is the second choice.
208         logd("Attempting to read config from XML resource");
209         configs = readXmlConfig();
210         if (configs != null) {
211             return configs;
212         }
213 
214         // This should rarely happen.
215         Log.w(TAG, "Creating default config");
216 
217         configs = new ArrayList<>();
218         for (byte port : mPhysicalPorts) {
219             configs.add(createDefaultConfig(port));
220         }
221         return configs;
222     }
223 
getFile(String filename)224     private File getFile(String filename) {
225         SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
226         return new File(systemInterface.getSystemCarDir(), filename);
227     }
228 
229     @Nullable
readXmlConfig()230     private List<CarUxRestrictionsConfiguration> readXmlConfig() {
231         try {
232             return CarUxRestrictionsConfigurationXmlParser.parse(
233                     mContext, R.xml.car_ux_restrictions_map);
234         } catch (IOException | XmlPullParserException e) {
235             Log.e(TAG, "Could not read config from XML resource", e);
236         }
237         return null;
238     }
239 
promoteStagedConfig()240     private void promoteStagedConfig() {
241         Path stagedConfig = getFile(CONFIG_FILENAME_STAGED).toPath();
242 
243         CarDrivingStateEvent currentDrivingStateEvent =
244                 mDrivingStateService.getCurrentDrivingState();
245         // Only promote staged config when car is parked.
246         if (currentDrivingStateEvent != null
247                 && currentDrivingStateEvent.eventValue == DRIVING_STATE_PARKED
248                 && Files.exists(stagedConfig)) {
249 
250             Path prod = getFile(CONFIG_FILENAME_PRODUCTION).toPath();
251             try {
252                 logd("Attempting to promote stage config");
253                 Files.move(stagedConfig, prod, REPLACE_EXISTING);
254             } catch (IOException e) {
255                 Log.e(TAG, "Could not promote state config", e);
256             }
257         }
258     }
259 
260     // Update current restrictions by getting the current driving state and speed.
initializeUxRestrictions()261     private void initializeUxRestrictions() {
262         CarDrivingStateEvent currentDrivingStateEvent =
263                 mDrivingStateService.getCurrentDrivingState();
264         // if we don't have enough information from the CarPropertyService to compute the UX
265         // restrictions, then leave the UX restrictions unchanged from what it was initialized to
266         // in the constructor.
267         if (currentDrivingStateEvent == null
268                 || currentDrivingStateEvent.eventValue == DRIVING_STATE_UNKNOWN) {
269             return;
270         }
271         int currentDrivingState = currentDrivingStateEvent.eventValue;
272         Float currentSpeed = getCurrentSpeed();
273         if (currentSpeed == SPEED_NOT_AVAILABLE) {
274             return;
275         }
276         // At this point the underlying CarPropertyService has provided us enough information to
277         // compute the UX restrictions that could be potentially different from the initial UX
278         // restrictions.
279         handleDispatchUxRestrictions(currentDrivingState, currentSpeed);
280     }
281 
getCurrentSpeed()282     private Float getCurrentSpeed() {
283         CarPropertyValue value = mCarPropertyService.getProperty(VehicleProperty.PERF_VEHICLE_SPEED,
284                 0);
285         if (value != null) {
286             return (Float) value.getValue();
287         }
288         return SPEED_NOT_AVAILABLE;
289     }
290 
291     @Override
release()292     public synchronized void release() {
293         for (UxRestrictionsClient client : mUxRClients) {
294             client.listenerBinder.unlinkToDeath(client, 0);
295         }
296         mUxRClients.clear();
297         mDrivingStateService.unregisterDrivingStateChangeListener(
298                 mICarDrivingStateChangeEventListener);
299     }
300 
301     // Binder methods
302 
303     /**
304      * Registers a {@link ICarUxRestrictionsChangeListener} to be notified for changes to the UX
305      * restrictions.
306      *
307      * @param listener  Listener to register
308      * @param displayId UX restrictions on this display will be notified.
309      */
310     @Override
registerUxRestrictionsChangeListener( ICarUxRestrictionsChangeListener listener, int displayId)311     public synchronized void registerUxRestrictionsChangeListener(
312             ICarUxRestrictionsChangeListener listener, int displayId) {
313         if (listener == null) {
314             Log.e(TAG, "registerUxRestrictionsChangeListener(): listener null");
315             throw new IllegalArgumentException("Listener is null");
316         }
317         // If a new client is registering, create a new DrivingStateClient and add it to the list
318         // of listening clients.
319         UxRestrictionsClient client = findUxRestrictionsClient(listener);
320         if (client == null) {
321             client = new UxRestrictionsClient(listener, displayId);
322             try {
323                 listener.asBinder().linkToDeath(client, 0);
324             } catch (RemoteException e) {
325                 Log.e(TAG, "Cannot link death recipient to binder " + e);
326             }
327             mUxRClients.add(client);
328         }
329         return;
330     }
331 
332     /**
333      * Iterates through the list of registered UX Restrictions clients -
334      * {@link UxRestrictionsClient} and finds if the given client is already registered.
335      *
336      * @param listener Listener to look for.
337      * @return the {@link UxRestrictionsClient} if found, null if not
338      */
339     @Nullable
findUxRestrictionsClient( ICarUxRestrictionsChangeListener listener)340     private UxRestrictionsClient findUxRestrictionsClient(
341             ICarUxRestrictionsChangeListener listener) {
342         IBinder binder = listener.asBinder();
343         for (UxRestrictionsClient client : mUxRClients) {
344             if (client.isHoldingBinder(binder)) {
345                 return client;
346             }
347         }
348         return null;
349     }
350 
351     /**
352      * Unregister the given UX Restrictions listener
353      *
354      * @param listener client to unregister
355      */
356     @Override
unregisterUxRestrictionsChangeListener( ICarUxRestrictionsChangeListener listener)357     public synchronized void unregisterUxRestrictionsChangeListener(
358             ICarUxRestrictionsChangeListener listener) {
359         if (listener == null) {
360             Log.e(TAG, "unregisterUxRestrictionsChangeListener(): listener null");
361             throw new IllegalArgumentException("Listener is null");
362         }
363 
364         UxRestrictionsClient client = findUxRestrictionsClient(listener);
365         if (client == null) {
366             Log.e(TAG, "unregisterUxRestrictionsChangeListener(): listener was not previously "
367                     + "registered");
368             return;
369         }
370         listener.asBinder().unlinkToDeath(client, 0);
371         mUxRClients.remove(client);
372     }
373 
374     /**
375      * Gets the current UX restrictions for a display.
376      *
377      * @param displayId UX restrictions on this display will be returned.
378      */
379     @Override
getCurrentUxRestrictions(int displayId)380     public synchronized CarUxRestrictions getCurrentUxRestrictions(int displayId) {
381         CarUxRestrictions restrictions = mCurrentUxRestrictions.get(getPhysicalPort(displayId));
382         if (restrictions == null) {
383             Log.e(TAG, String.format(
384                     "Restrictions are null for displayId:%d. Returning full restrictions.",
385                     displayId));
386             restrictions = createFullyRestrictedRestrictions();
387         }
388         return restrictions;
389     }
390 
391     /**
392      * Convenience method to retrieve restrictions for default display.
393      */
394     @Nullable
getCurrentUxRestrictions()395     public synchronized CarUxRestrictions getCurrentUxRestrictions() {
396         return getCurrentUxRestrictions(Display.DEFAULT_DISPLAY);
397     }
398 
399     @Override
saveUxRestrictionsConfigurationForNextBoot( List<CarUxRestrictionsConfiguration> configs)400     public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
401             List<CarUxRestrictionsConfiguration> configs) {
402         ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION);
403 
404         validateConfigs(configs);
405 
406         return persistConfig(configs, CONFIG_FILENAME_STAGED);
407     }
408 
409     @Override
410     @Nullable
getStagedConfigs()411     public List<CarUxRestrictionsConfiguration> getStagedConfigs() {
412         File stagedConfig = getFile(CONFIG_FILENAME_STAGED);
413         if (stagedConfig.exists()) {
414             logd("Attempting to read staged config");
415             return readPersistedConfig(stagedConfig);
416         } else {
417             return null;
418         }
419     }
420 
421     /**
422      * Sets the restriction mode to use. Restriction mode allows a different set of restrictions to
423      * be applied in the same driving state. Restrictions for each mode can be configured through
424      * {@link CarUxRestrictionsConfiguration}.
425      *
426      * <p>Defaults to {@link CarUxRestrictionsManager#UX_RESTRICTION_MODE_BASELINE}.
427      *
428      * @param mode See values in {@link CarUxRestrictionsManager.UxRestrictionMode}.
429      * @return {@code true} if mode was successfully changed; {@code false} otherwise.
430      * @see CarUxRestrictionsConfiguration.DrivingStateRestrictions
431      * @see CarUxRestrictionsConfiguration.Builder
432      */
433     @Override
setRestrictionMode( @arUxRestrictionsManager.UxRestrictionMode int mode)434     public synchronized boolean setRestrictionMode(
435             @CarUxRestrictionsManager.UxRestrictionMode int mode) {
436         if (mRestrictionMode == mode) {
437             return true;
438         }
439 
440         addTransitionLog(TAG, mRestrictionMode, mode, System.currentTimeMillis(),
441                 "Restriction mode");
442         mRestrictionMode = mode;
443         logd("Set restriction mode to: " + CarUxRestrictionsManager.modeToString(mode));
444 
445         handleDispatchUxRestrictions(
446                 mDrivingStateService.getCurrentDrivingState().eventValue, getCurrentSpeed());
447         return true;
448     }
449 
450     @Override
451     @CarUxRestrictionsManager.UxRestrictionMode
getRestrictionMode()452     public synchronized int getRestrictionMode() {
453         return mRestrictionMode;
454     }
455 
456     /**
457      * Writes configuration into the specified file.
458      *
459      * IO access on file is not thread safe. Caller should ensure threading protection.
460      */
persistConfig(List<CarUxRestrictionsConfiguration> configs, String filename)461     private boolean persistConfig(List<CarUxRestrictionsConfiguration> configs, String filename) {
462         File file = getFile(filename);
463         AtomicFile stagedFile = new AtomicFile(file);
464         FileOutputStream fos;
465         try {
466             fos = stagedFile.startWrite();
467         } catch (IOException e) {
468             Log.e(TAG, "Could not open file to persist config", e);
469             return false;
470         }
471         try (JsonWriter jsonWriter = new JsonWriter(
472                 new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
473             jsonWriter.beginArray();
474             for (CarUxRestrictionsConfiguration config : configs) {
475                 config.writeJson(jsonWriter);
476             }
477             jsonWriter.endArray();
478         } catch (IOException e) {
479             Log.e(TAG, "Could not persist config", e);
480             stagedFile.failWrite(fos);
481             return false;
482         }
483         stagedFile.finishWrite(fos);
484         return true;
485     }
486 
487     @Nullable
readPersistedConfig(File file)488     private List<CarUxRestrictionsConfiguration> readPersistedConfig(File file) {
489         if (!file.exists()) {
490             Log.e(TAG, "Could not find config file: " + file.getName());
491             return null;
492         }
493 
494         AtomicFile configFile = new AtomicFile(file);
495         try (JsonReader reader = new JsonReader(
496                 new InputStreamReader(configFile.openRead(), StandardCharsets.UTF_8))) {
497             List<CarUxRestrictionsConfiguration> configs = new ArrayList<>();
498             reader.beginArray();
499             while (reader.hasNext()) {
500                 configs.add(CarUxRestrictionsConfiguration.readJson(reader));
501             }
502             reader.endArray();
503             return configs;
504         } catch (IOException e) {
505             Log.e(TAG, "Could not read persisted config file " + file.getName(), e);
506         }
507         return null;
508     }
509 
510     /**
511      * Enable/disable UX restrictions change broadcast blocking.
512      * Setting this to true will stop broadcasts of UX restriction change to listeners.
513      * This method works only on debug builds and the caller of this method needs to have the same
514      * signature of the car service.
515      */
setUxRChangeBroadcastEnabled(boolean enable)516     public synchronized void setUxRChangeBroadcastEnabled(boolean enable) {
517         if (!isDebugBuild()) {
518             Log.e(TAG, "Cannot set UX restriction change broadcast.");
519             return;
520         }
521         // Check if the caller has the same signature as that of the car service.
522         if (mContext.getPackageManager().checkSignatures(Process.myUid(), Binder.getCallingUid())
523                 != PackageManager.SIGNATURE_MATCH) {
524             throw new SecurityException(
525                     "Caller " + mContext.getPackageManager().getNameForUid(Binder.getCallingUid())
526                             + " does not have the right signature");
527         }
528         if (enable) {
529             // if enabling it back, send the current restrictions
530             mUxRChangeBroadcastEnabled = enable;
531             handleDispatchUxRestrictions(mDrivingStateService.getCurrentDrivingState().eventValue,
532                     getCurrentSpeed());
533         } else {
534             // fake parked state, so if the system is currently restricted, the restrictions are
535             // relaxed.
536             handleDispatchUxRestrictions(DRIVING_STATE_PARKED, 0);
537             mUxRChangeBroadcastEnabled = enable;
538         }
539     }
540 
isDebugBuild()541     private boolean isDebugBuild() {
542         return Build.IS_USERDEBUG || Build.IS_ENG;
543     }
544 
545     /**
546      * Class that holds onto client related information - listener interface, process that hosts the
547      * binder object etc.
548      * It also registers for death notifications of the host.
549      */
550     private class UxRestrictionsClient implements IBinder.DeathRecipient {
551         private final IBinder listenerBinder;
552         private final ICarUxRestrictionsChangeListener listener;
553         private final int mDisplayId;
554 
UxRestrictionsClient(ICarUxRestrictionsChangeListener l, int displayId)555         UxRestrictionsClient(ICarUxRestrictionsChangeListener l, int displayId) {
556             listener = l;
557             listenerBinder = l.asBinder();
558             mDisplayId = displayId;
559         }
560 
561         @Override
binderDied()562         public void binderDied() {
563             logd("Binder died " + listenerBinder);
564             listenerBinder.unlinkToDeath(this, 0);
565             synchronized (CarUxRestrictionsManagerService.this) {
566                 mUxRClients.remove(this);
567             }
568         }
569 
570         /**
571          * Returns if the given binder object matches to what this client info holds.
572          * Used to check if the listener asking to be registered is already registered.
573          *
574          * @return true if matches, false if not
575          */
isHoldingBinder(IBinder binder)576         public boolean isHoldingBinder(IBinder binder) {
577             return listenerBinder == binder;
578         }
579 
580         /**
581          * Dispatch the event to the listener
582          *
583          * @param event {@link CarUxRestrictions}.
584          */
dispatchEventToClients(CarUxRestrictions event)585         public void dispatchEventToClients(CarUxRestrictions event) {
586             if (event == null) {
587                 return;
588             }
589             try {
590                 listener.onUxRestrictionsChanged(event);
591             } catch (RemoteException e) {
592                 Log.e(TAG, "Dispatch to listener failed", e);
593             }
594         }
595     }
596 
597     @Override
dump(PrintWriter writer)598     public void dump(PrintWriter writer) {
599         writer.println("*CarUxRestrictionsManagerService*");
600         for (byte port : mCurrentUxRestrictions.keySet()) {
601             CarUxRestrictions restrictions = mCurrentUxRestrictions.get(port);
602             writer.printf("Port: 0x%02X UXR: %s\n", port, restrictions.toString());
603         }
604         if (isDebugBuild()) {
605             writer.println("mUxRChangeBroadcastEnabled? " + mUxRChangeBroadcastEnabled);
606         }
607 
608         writer.println("UX Restriction configurations:");
609         for (CarUxRestrictionsConfiguration config : mCarUxRestrictionsConfigurations.values()) {
610             config.dump(writer);
611         }
612         writer.println("UX Restriction change log:");
613         for (Utils.TransitionLog tlog : mTransitionLogs) {
614             writer.println(tlog);
615         }
616     }
617 
618     /**
619      * {@link CarDrivingStateEvent} listener registered with the {@link CarDrivingStateService}
620      * for getting driving state change notifications.
621      */
622     private final ICarDrivingStateChangeListener mICarDrivingStateChangeEventListener =
623             new ICarDrivingStateChangeListener.Stub() {
624                 @Override
625                 public void onDrivingStateChanged(CarDrivingStateEvent event) {
626                     logd("Driving State Changed:" + event.eventValue);
627                     handleDrivingStateEvent(event);
628                 }
629             };
630 
631     /**
632      * Handle the driving state change events coming from the {@link CarDrivingStateService}.
633      * Map the driving state to the corresponding UX Restrictions and dispatch the
634      * UX Restriction change to the registered clients.
635      */
handleDrivingStateEvent(CarDrivingStateEvent event)636     private synchronized void handleDrivingStateEvent(CarDrivingStateEvent event) {
637         if (event == null) {
638             return;
639         }
640         int drivingState = event.eventValue;
641         Float speed = getCurrentSpeed();
642 
643         if (speed != SPEED_NOT_AVAILABLE) {
644             mCurrentMovingSpeed = speed;
645         } else if (drivingState == DRIVING_STATE_PARKED
646                 || drivingState == DRIVING_STATE_UNKNOWN) {
647             // If speed is unavailable, but the driving state is parked or unknown, it can still be
648             // handled.
649             logd("Speed null when driving state is: " + drivingState);
650             mCurrentMovingSpeed = 0;
651         } else {
652             // If we get here with driving state != parked or unknown && speed == null,
653             // something is wrong.  CarDrivingStateService could not have inferred idling or moving
654             // when speed is not available
655             Log.e(TAG, "Unexpected:  Speed null when driving state is: " + drivingState);
656             return;
657         }
658         handleDispatchUxRestrictions(drivingState, mCurrentMovingSpeed);
659     }
660 
661     /**
662      * {@link CarPropertyEvent} listener registered with the {@link CarPropertyService} for getting
663      * speed change notifications.
664      */
665     private final ICarPropertyEventListener mICarPropertyEventListener =
666             new ICarPropertyEventListener.Stub() {
667                 @Override
668                 public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
669                     for (CarPropertyEvent event : events) {
670                         if ((event.getEventType()
671                                 == CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE)
672                                 && (event.getCarPropertyValue().getPropertyId()
673                                 == VehicleProperty.PERF_VEHICLE_SPEED)) {
674                             handleSpeedChange((Float) event.getCarPropertyValue().getValue());
675                         }
676                     }
677                 }
678             };
679 
handleSpeedChange(float newSpeed)680     private synchronized void handleSpeedChange(float newSpeed) {
681         if (newSpeed == mCurrentMovingSpeed) {
682             // Ignore if speed hasn't changed
683             return;
684         }
685         int currentDrivingState = mDrivingStateService.getCurrentDrivingState().eventValue;
686         if (currentDrivingState != DRIVING_STATE_MOVING) {
687             // Ignore speed changes if the vehicle is not moving
688             return;
689         }
690         mCurrentMovingSpeed = newSpeed;
691         handleDispatchUxRestrictions(currentDrivingState, newSpeed);
692     }
693 
694     /**
695      * Handle dispatching UX restrictions change.
696      *
697      * @param currentDrivingState driving state of the vehicle
698      * @param speed               speed of the vehicle
699      */
handleDispatchUxRestrictions(@arDrivingState int currentDrivingState, float speed)700     private synchronized void handleDispatchUxRestrictions(@CarDrivingState int currentDrivingState,
701             float speed) {
702         if (isDebugBuild() && !mUxRChangeBroadcastEnabled) {
703             Log.d(TAG, "Not dispatching UX Restriction due to setting");
704             return;
705         }
706 
707         Map<Byte, CarUxRestrictions> newUxRestrictions = new HashMap<>();
708         for (byte port : mPhysicalPorts) {
709             CarUxRestrictionsConfiguration config = mCarUxRestrictionsConfigurations.get(port);
710             if (config == null) {
711                 continue;
712             }
713 
714             CarUxRestrictions uxRestrictions = config.getUxRestrictions(
715                     currentDrivingState, speed, mRestrictionMode);
716             logd(String.format("Display port 0x%02x\tDO old->new: %b -> %b",
717                     port,
718                     mCurrentUxRestrictions.get(port).isRequiresDistractionOptimization(),
719                     uxRestrictions.isRequiresDistractionOptimization()));
720             logd(String.format("Display port 0x%02x\tUxR old->new: 0x%x -> 0x%x",
721                     port,
722                     mCurrentUxRestrictions.get(port).getActiveRestrictions(),
723                     uxRestrictions.getActiveRestrictions()));
724             newUxRestrictions.put(port, uxRestrictions);
725         }
726 
727         // Ignore dispatching if the restrictions has not changed.
728         Set<Byte> displayToDispatch = new ArraySet<>();
729         for (byte port : newUxRestrictions.keySet()) {
730             if (!mCurrentUxRestrictions.containsKey(port)) {
731                 // This should never happen.
732                 Log.wtf(TAG, "Unrecognized port:" + port);
733                 continue;
734             }
735             CarUxRestrictions uxRestrictions = newUxRestrictions.get(port);
736             if (!mCurrentUxRestrictions.get(port).isSameRestrictions(uxRestrictions)) {
737                 displayToDispatch.add(port);
738             }
739         }
740         if (displayToDispatch.isEmpty()) {
741             return;
742         }
743 
744         for (byte port : displayToDispatch) {
745             addTransitionLog(
746                     mCurrentUxRestrictions.get(port), newUxRestrictions.get(port));
747         }
748 
749         logd("dispatching to clients");
750         for (UxRestrictionsClient client : mUxRClients) {
751             Byte clientDisplayPort = getPhysicalPort(client.mDisplayId);
752             if (clientDisplayPort == null) {
753                 clientDisplayPort = mDefaultDisplayPhysicalPort;
754             }
755             if (displayToDispatch.contains(clientDisplayPort)) {
756                 client.dispatchEventToClients(newUxRestrictions.get(clientDisplayPort));
757             }
758         }
759 
760         mCurrentUxRestrictions = newUxRestrictions;
761     }
762 
getDefaultDisplayPhysicalPort()763     private byte getDefaultDisplayPhysicalPort() {
764         Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
765         DisplayAddress.Physical address = (DisplayAddress.Physical) defaultDisplay.getAddress();
766 
767         if (address == null) {
768             Log.w(TAG, "Default display does not have physical display port.");
769             return DEFAULT_PORT;
770         }
771         return address.getPort();
772     }
773 
initPhysicalPort()774     private void initPhysicalPort() {
775         for (Display display : mDisplayManager.getDisplays()) {
776             if (display.getType() == Display.TYPE_VIRTUAL) {
777                 continue;
778             }
779 
780             if (display.getDisplayId() == Display.DEFAULT_DISPLAY && display.getAddress() == null) {
781                 // Assume default display is a physical display so assign an address if it
782                 // does not have one (possibly due to lower graphic driver version).
783                 if (Log.isLoggable(TAG, Log.INFO)) {
784                     Log.i(TAG, "Default display does not have display address. Using default.");
785                 }
786                 mPhysicalPorts.add(mDefaultDisplayPhysicalPort);
787             } else if (display.getAddress() instanceof DisplayAddress.Physical) {
788                 byte port = ((DisplayAddress.Physical) display.getAddress()).getPort();
789                 if (Log.isLoggable(TAG, Log.INFO)) {
790                     Log.i(TAG, String.format(
791                             "Display %d uses port %d", display.getDisplayId(), port));
792                 }
793                 mPhysicalPorts.add(port);
794             } else {
795                 Log.w(TAG, "At init non-virtual display has a non-physical display address: "
796                         + display);
797             }
798         }
799     }
800 
convertToMap( List<CarUxRestrictionsConfiguration> configs)801     private Map<Byte, CarUxRestrictionsConfiguration> convertToMap(
802             List<CarUxRestrictionsConfiguration> configs) {
803         validateConfigs(configs);
804 
805         Map<Byte, CarUxRestrictionsConfiguration> result = new HashMap<>();
806         if (configs.size() == 1) {
807             CarUxRestrictionsConfiguration config = configs.get(0);
808             byte port = config.getPhysicalPort() == null
809                     ? mDefaultDisplayPhysicalPort
810                     : config.getPhysicalPort();
811             result.put(port, config);
812         } else {
813             for (CarUxRestrictionsConfiguration config : configs) {
814                 result.put(config.getPhysicalPort(), config);
815             }
816         }
817         return result;
818     }
819 
820     /**
821      * Validates configs for multi-display:
822      * - share the same restrictions parameters;
823      * - each sets display port;
824      * - each has unique display port.
825      */
826     @VisibleForTesting
validateConfigs(List<CarUxRestrictionsConfiguration> configs)827     void validateConfigs(List<CarUxRestrictionsConfiguration> configs) {
828         if (configs.size() == 0) {
829             throw new IllegalArgumentException("Empty configuration.");
830         }
831 
832         if (configs.size() == 1) {
833             return;
834         }
835 
836         CarUxRestrictionsConfiguration first = configs.get(0);
837         Set<Byte> existingPorts = new ArraySet<>();
838         for (CarUxRestrictionsConfiguration config : configs) {
839             if (!config.hasSameParameters(first)) {
840                 // Input should have the same restriction parameters because:
841                 // - it doesn't make sense otherwise; and
842                 // - in format it matches how xml can only specify one set of parameters.
843                 throw new IllegalArgumentException(
844                         "Configurations should have the same restrictions parameters.");
845             }
846 
847             Byte port = config.getPhysicalPort();
848             if (port == null) {
849                 // Size was checked above; safe to assume there are multiple configs.
850                 throw new IllegalArgumentException(
851                         "Input contains multiple configurations; each must set physical port.");
852             }
853             if (existingPorts.contains(port)) {
854                 throw new IllegalArgumentException("Multiple configurations for port " + port);
855             }
856 
857             existingPorts.add(port);
858         }
859     }
860 
861     /**
862      * Returns the physical port byte id for the display or {@code null} if {@link
863      * DisplayManager#getDisplay(int)} is not aware of the provided id.
864      */
865     @Nullable
getPhysicalPort(int displayId)866     private Byte getPhysicalPort(int displayId) {
867         if (!mPortLookup.containsKey(displayId)) {
868             Display display = mDisplayManager.getDisplay(displayId);
869             if (display == null) {
870                 Log.w(TAG, "Could not retrieve display for id: " + displayId);
871                 return null;
872             }
873             byte port = getPhysicalPort(display);
874             mPortLookup.put(displayId, port);
875         }
876         return mPortLookup.get(displayId);
877     }
878 
getPhysicalPort(@onNull Display display)879     private byte getPhysicalPort(@NonNull Display display) {
880         if (display.getType() == Display.TYPE_VIRTUAL) {
881             // We require all virtual displays to be launched on default display.
882             return mDefaultDisplayPhysicalPort;
883         }
884 
885         DisplayAddress address = display.getAddress();
886         if (address == null) {
887             Log.e(TAG, "Display " + display
888                     + " is not a virtual display but has null DisplayAddress.");
889             return mDefaultDisplayPhysicalPort;
890         } else if (!(address instanceof DisplayAddress.Physical)) {
891             Log.e(TAG, "Display " + display + " has non-physical address: " + address);
892             return mDefaultDisplayPhysicalPort;
893         } else {
894             return ((DisplayAddress.Physical) address).getPort();
895         }
896     }
897 
createUnrestrictedRestrictions()898     private CarUxRestrictions createUnrestrictedRestrictions() {
899         return new CarUxRestrictions.Builder(/* reqOpt= */ false,
900                 CarUxRestrictions.UX_RESTRICTIONS_BASELINE, SystemClock.elapsedRealtimeNanos())
901                 .build();
902     }
903 
createFullyRestrictedRestrictions()904     private CarUxRestrictions createFullyRestrictedRestrictions() {
905         return new CarUxRestrictions.Builder(
906                 /*reqOpt= */ true,
907                 CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED,
908                 SystemClock.elapsedRealtimeNanos()).build();
909     }
910 
createDefaultConfig(byte port)911     CarUxRestrictionsConfiguration createDefaultConfig(byte port) {
912         return new CarUxRestrictionsConfiguration.Builder()
913                 .setPhysicalPort(port)
914                 .setUxRestrictions(DRIVING_STATE_PARKED,
915                         false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
916                 .setUxRestrictions(DRIVING_STATE_IDLING,
917                         false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
918                 .setUxRestrictions(DRIVING_STATE_MOVING,
919                         true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
920                 .setUxRestrictions(DRIVING_STATE_UNKNOWN,
921                         true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
922                 .build();
923     }
924 
addTransitionLog(String name, int from, int to, long timestamp, String extra)925     private void addTransitionLog(String name, int from, int to, long timestamp, String extra) {
926         if (mTransitionLogs.size() >= MAX_TRANSITION_LOG_SIZE) {
927             mTransitionLogs.remove();
928         }
929 
930         Utils.TransitionLog tLog = new Utils.TransitionLog(name, from, to, timestamp, extra);
931         mTransitionLogs.add(tLog);
932     }
933 
addTransitionLog( CarUxRestrictions oldRestrictions, CarUxRestrictions newRestrictions)934     private void addTransitionLog(
935             CarUxRestrictions oldRestrictions, CarUxRestrictions newRestrictions) {
936         if (mTransitionLogs.size() >= MAX_TRANSITION_LOG_SIZE) {
937             mTransitionLogs.remove();
938         }
939         StringBuilder extra = new StringBuilder();
940         extra.append(oldRestrictions.isRequiresDistractionOptimization() ? "DO -> " : "No DO -> ");
941         extra.append(newRestrictions.isRequiresDistractionOptimization() ? "DO" : "No DO");
942 
943         Utils.TransitionLog tLog = new Utils.TransitionLog(TAG,
944                 oldRestrictions.getActiveRestrictions(), newRestrictions.getActiveRestrictions(),
945                 System.currentTimeMillis(), extra.toString());
946         mTransitionLogs.add(tLog);
947     }
948 
logd(String msg)949     private static void logd(String msg) {
950         if (DBG) {
951             Slog.d(TAG, msg);
952         }
953     }
954 }
955