| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.car; |
| |
| import android.support.annotation.IntDef; |
| import android.support.annotation.Nullable; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.support.car.content.pm.CarPackageManager; |
| import android.support.car.hardware.CarSensorManager; |
| import android.support.car.navigation.CarNavigationManager; |
| import android.util.Log; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| |
| /** |
| * Top level car API. |
| * This API works only for device with {@link PackageManager#FEATURE_AUTOMOTIVE} feature |
| * supported or device with Google play service. |
| * Calling this API with device with no such feature will lead into an exception. |
| * |
| */ |
| public class Car { |
| |
| /** Service name for {@link CarSensorManager}, to be used in {@link #getCarManager(String)}. */ |
| public static final String SENSOR_SERVICE = "sensor"; |
| |
| /** Service name for {@link CarInfoManager}, to be used in {@link #getCarManager(String)}. */ |
| public static final String INFO_SERVICE = "info"; |
| |
| /** Service name for {@link CarAppContextManager}. */ |
| public static final String APP_CONTEXT_SERVICE = "app_context"; |
| |
| /** Service name for {@link CarPackageManager} */ |
| public static final String PACKAGE_SERVICE = "package"; |
| |
| /** Service name for {@link CarAudioManager} */ |
| public static final String AUDIO_SERVICE = "audio"; |
| /** |
| * Service name for {@link CarNavigationManager} |
| * @hide |
| */ |
| public static final String CAR_NAVIGATION_SERVICE = "car_navigation_service"; |
| |
| /** Type of car connection: car emulator, not physical connection. */ |
| public static final int CONNECTION_TYPE_EMULATOR = 0; |
| /** Type of car connection: connected to a car via USB. */ |
| public static final int CONNECTION_TYPE_USB = 1; |
| /** Type of car connection: connected to a car via WIFI. */ |
| public static final int CONNECTION_TYPE_WIFI = 2; |
| /** Type of car connection: on-device car emulator, for development (e.g. Local Head Unit). */ |
| public static final int CONNECTION_TYPE_ON_DEVICE_EMULATOR = 3; |
| /** Type of car connection: car emulator, connected over ADB (e.g. Desktop Head Unit). */ |
| public static final int CONNECTION_TYPE_ADB_EMULATOR = 4; |
| /** Type of car connection: platform runs directly in car. */ |
| public static final int CONNECTION_TYPE_EMBEDDED = 5; |
| /** |
| * Type of car connection: platform runs directly in car but with mocked vehicle hal. |
| * This will only happen in testing environment. |
| * @hide |
| */ |
| public static final int CONNECTION_TYPE_EMBEDDED_MOCKING = 6; |
| |
| /** @hide */ |
| @IntDef({CONNECTION_TYPE_EMULATOR, CONNECTION_TYPE_USB, CONNECTION_TYPE_WIFI, |
| CONNECTION_TYPE_ON_DEVICE_EMULATOR, CONNECTION_TYPE_ADB_EMULATOR, CONNECTION_TYPE_EMBEDDED}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface ConnectionType {} |
| |
| /** Permission necessary to access car's mileage information. */ |
| public static final String PERMISSION_MILEAGE = "android.car.permission.CAR_MILEAGE"; |
| /** Permission necessary to access car's fuel level. */ |
| public static final String PERMISSION_FUEL = "android.car.permission.CAR_FUEL"; |
| /** Permission necessary to access car's speed. */ |
| public static final String PERMISSION_SPEED = "android.car.permission.CAR_SPEED"; |
| /** Permission necessary to access car specific communication channel. */ |
| public static final String PERMISSION_VENDOR_EXTENSION = |
| "android.car.permission.CAR_VENDOR_EXTENSION"; |
| /** |
| * Permission necessary to use {@link android.car.navigation.CarNavigationManager}. |
| * @hide |
| */ |
| public static final String PERMISSION_CAR_NAVIGATION_MANAGER = |
| "android.car.permission.PERMISSION_CAR_NAVIGATION_MANAGER"; |
| |
| |
| /** |
| * PackageManager.FEATURE_AUTOMOTIVE from M. But redefine here to support L. |
| * @hide |
| */ |
| private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive"; |
| |
| /** |
| * {@link CarServiceLoader} implementation for projected mode. Only available when projected |
| * client library is linked. |
| * @hide |
| */ |
| private static final String PROJECTED_CAR_SERVICE_LOADER = |
| "com.google.android.gms.car.CarServiceLoaderGms"; |
| |
| /** |
| * CarXyzService throws IllegalStateException with this message is re-thrown as |
| * {@link CarNotConnectedException}. |
| * |
| * @hide |
| */ |
| public static final String CAR_NOT_CONNECTED_EXCEPTION_MSG = "CarNotConnected"; |
| |
| /** @hide */ |
| public static final String CAR_SERVICE_INTERFACE_NAME = "android.car.ICar"; |
| |
| private final Context mContext; |
| private final Looper mLooper; |
| private static final int STATE_DISCONNECTED = 0; |
| private static final int STATE_CONNECTING = 1; |
| private static final int STATE_CONNECTED = 2; |
| // @GuardedBy("this") |
| private int mConnectionState; |
| |
| private final ServiceConnectionListener mServiceConnectionListener = |
| new ServiceConnectionListener () { |
| public void onServiceConnected(ComponentName name) { |
| synchronized (Car.this) { |
| mConnectionState = STATE_CONNECTED; |
| } |
| mServiceConnectionListenerClient.onServiceConnected(name); |
| } |
| |
| public void onServiceDisconnected(ComponentName name) { |
| synchronized (Car.this) { |
| if (mConnectionState == STATE_DISCONNECTED) { |
| return; |
| } |
| mConnectionState = STATE_DISCONNECTED; |
| } |
| mServiceConnectionListenerClient.onServiceDisconnected(name); |
| connect(); |
| } |
| |
| public void onServiceSuspended(int cause) { |
| mServiceConnectionListenerClient.onServiceSuspended(cause); |
| } |
| |
| public void onServiceConnectionFailed(int cause) { |
| mServiceConnectionListenerClient.onServiceConnectionFailed(cause); |
| } |
| }; |
| |
| private final ServiceConnectionListener mServiceConnectionListenerClient; |
| private final Object mCarManagerLock = new Object(); |
| //@GuardedBy("mCarManagerLock") |
| private final HashMap<String, CarManagerBase> mServiceMap = new HashMap<>(); |
| private final CarServiceLoader mCarServiceLoader; |
| |
| /** Handler for generic event dispatching. */ |
| private final Handler mEventHandler; |
| |
| /** |
| * A factory method that creates Car instance for all Car API access. |
| * @param context |
| * @param serviceConnectionListener listener for monitoring service connection. |
| * @param looper Looper to dispatch all listeners. If null, it will use main thread. Note that |
| * service connection listener will be always in main thread regardless of this Looper. |
| * @return Car instance if system is in car environment and returns {@code null} otherwise. |
| */ |
| public static Car createCar(Context context, |
| ServiceConnectionListener serviceConnectionListener, @Nullable Looper looper) { |
| try { |
| return new Car(context, serviceConnectionListener, looper); |
| } catch (IllegalArgumentException e) { |
| // Expected when car service loader is not available. |
| } |
| return null; |
| } |
| |
| /** |
| * A factory method that creates Car instance for all Car API access using main thread {@code |
| * Looper}. |
| * |
| * @see #createCar(Context, ServiceConnectionListener, Looper) |
| */ |
| public static Car createCar(Context context, |
| ServiceConnectionListener serviceConnectionListener) { |
| return createCar(context, serviceConnectionListener, null); |
| } |
| |
| private Car(Context context, ServiceConnectionListener serviceConnectionListener, |
| @Nullable Looper looper) { |
| mContext = context; |
| mServiceConnectionListenerClient = serviceConnectionListener; |
| if (looper == null) { |
| mLooper = Looper.getMainLooper(); |
| } else { |
| mLooper = looper; |
| } |
| mEventHandler = new Handler(mLooper); |
| if (mContext.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) { |
| mCarServiceLoader = new CarServiceLoaderEmbedded(context, mServiceConnectionListener, |
| mLooper); |
| } else { |
| mCarServiceLoader = loadCarServiceLoader(PROJECTED_CAR_SERVICE_LOADER, context, |
| mServiceConnectionListener, mLooper); |
| } |
| } |
| |
| private CarServiceLoader loadCarServiceLoader(String carServiceLoaderClassName, |
| Context context, ServiceConnectionListener serviceConnectionListener, Looper looper) |
| throws IllegalArgumentException { |
| Class carServiceLoaderClass = null; |
| try { |
| carServiceLoaderClass = Class.forName(carServiceLoaderClassName); |
| } catch (ClassNotFoundException e) { |
| throw new IllegalArgumentException("Cannot find CarServiceLoader implementation:" + |
| carServiceLoaderClassName, e); |
| } |
| Constructor<?> ctor; |
| try { |
| ctor = carServiceLoaderClass.getDeclaredConstructor(Context.class, |
| ServiceConnectionListener.class, Looper.class); |
| } catch (NoSuchMethodException e) { |
| throw new IllegalArgumentException("Cannot construct CarServiceLoader, no constructor: " |
| + carServiceLoaderClassName, e); |
| } |
| try { |
| return (CarServiceLoader) ctor.newInstance(context, |
| serviceConnectionListener, looper); |
| } catch (InstantiationException | IllegalAccessException | IllegalArgumentException |
| | InvocationTargetException e) { |
| throw new IllegalArgumentException( |
| "Cannot construct CarServiceLoader, constructor failed for " |
| + carServiceLoaderClass.getName(), e); |
| } |
| } |
| |
| /** |
| * Car constructor when CarServiceLoader is already available. |
| * @param context |
| * @param serviceLoader |
| * @param looper |
| * |
| * @hide |
| */ |
| public Car(Context context, CarServiceLoader serviceLoader, @Nullable Looper looper) { |
| mContext = context; |
| if (looper == null) { |
| mLooper = Looper.getMainLooper(); |
| } else { |
| mLooper = looper; |
| } |
| mEventHandler = new Handler(mLooper); |
| mConnectionState = STATE_CONNECTED; |
| mCarServiceLoader = serviceLoader; |
| mServiceConnectionListenerClient = null; |
| } |
| |
| /** |
| * Connect to car service. This can be called while it is disconnected. |
| * @throws IllegalStateException If connection is still on-going from previous |
| * connect call or it is already connected |
| */ |
| public void connect() throws IllegalStateException { |
| synchronized (this) { |
| if (mConnectionState != STATE_DISCONNECTED) { |
| throw new IllegalStateException("already connected or connecting"); |
| } |
| mConnectionState = STATE_CONNECTING; |
| mCarServiceLoader.connect(); |
| } |
| } |
| |
| /** |
| * Disconnect from car service. This can be called while disconnected. Once disconnect is |
| * called, all Car*Managers from this instance becomes invalid, and |
| * {@link Car#getCarManager(String)} will return different instance if it is connected again. |
| */ |
| public void disconnect() { |
| synchronized (this) { |
| if (mConnectionState == STATE_DISCONNECTED) { |
| return; |
| } |
| tearDownCarManagers(); |
| mConnectionState = STATE_DISCONNECTED; |
| mCarServiceLoader.disconnect(); |
| } |
| } |
| |
| /** |
| * Tells if it is connected to the service or not. This will return false if it is still |
| * connecting. |
| * @return |
| */ |
| public boolean isConnected() { |
| synchronized (this) { |
| return mConnectionState == STATE_CONNECTED; |
| } |
| } |
| |
| /** |
| * Tells if this instance is already connecting to car service or not. |
| * @return |
| */ |
| public boolean isConnecting() { |
| synchronized (this) { |
| return mConnectionState == STATE_CONNECTING; |
| } |
| } |
| |
| /** |
| * Tells if car is connected to car or not. In some car environments, being connected to service |
| * does not necessarily mean being connected to car. |
| */ |
| public boolean isConnectedToCar() { |
| return mCarServiceLoader.isConnectedToCar(); |
| } |
| |
| /** |
| * Get car specific service as in {@link Context#getSystemService(String)}. Returned |
| * {@link Object} should be type-casted to the desired service. |
| * For example, to get sensor service, |
| * SensorManagerService sensorManagerService = car.getCarManager(Car.SENSOR_SERVICE); |
| * @param serviceName Name of service that should be created like {@link #SENSOR_SERVICE}. |
| * @return Matching service manager or null if there is no such service. |
| */ |
| public Object getCarManager(String serviceName) |
| throws CarNotSupportedException, CarNotConnectedException { |
| Object manager = null; |
| synchronized (mCarManagerLock) { |
| manager = mServiceMap.get(serviceName); |
| if (manager == null) { |
| manager = mCarServiceLoader.getCarManager(serviceName); |
| } |
| // do not store if it is not CarManagerBase. This can happen when system version |
| // is retrieved from this call. |
| if (manager != null && manager instanceof CarManagerBase) { |
| mServiceMap.put(serviceName, (CarManagerBase) manager); |
| } |
| } |
| return manager; |
| } |
| |
| /** |
| * Return the type of currently connected car. |
| * @return |
| * @throws CarNotConnectedException |
| */ |
| @ConnectionType |
| public int getCarConnectionType() throws CarNotConnectedException { |
| return mCarServiceLoader.getCarConnectionType(); |
| } |
| |
| /** |
| * Registers a {@link CarConnectionListener}. |
| * |
| * Avoid reregistering unregistered listeners. If an unregistered listener is reregistered, |
| * it may receive duplicate calls to {@link CarConnectionListener#onConnected}. |
| * |
| * @throws IllegalStateException if service is not connected. |
| */ |
| public void registerCarConnectionListener(CarConnectionListener listener) |
| throws IllegalStateException, CarNotConnectedException { |
| assertCarConnection(); |
| mCarServiceLoader.registerCarConnectionListener(listener); |
| } |
| |
| /** |
| * Unregisters a {@link CarConnectionListener}. |
| * |
| * <b>Note:</b> If this method is called from a thread besides the client's looper thread, |
| * there is no guarantee that the unregistered listener will not receive callbacks after |
| * this method returns. |
| */ |
| public void unregisterCarConnectionListener(CarConnectionListener listener) { |
| mCarServiceLoader.unregisterCarConnectionListener(listener); |
| } |
| |
| /** |
| * IllegalStateException from XyzCarService with special message is re-thrown as a different |
| * exception. If the IllegalStateException is not understood then this message will throw the |
| * original exception. |
| * |
| * @param e exception from XyzCarService. |
| * @throws CarNotConnectedException |
| * @hide |
| */ |
| public static void checkCarNotConnectedExceptionFromCarService( |
| IllegalStateException e) throws CarNotConnectedException, IllegalStateException { |
| String message = e.getMessage(); |
| if (message.equals(CAR_NOT_CONNECTED_EXCEPTION_MSG)) { |
| throw new CarNotConnectedException(); |
| } else { |
| throw e; |
| } |
| } |
| |
| private synchronized void assertCarConnection() throws IllegalStateException { |
| if (!mCarServiceLoader.isConnectedToCar()) { |
| throw new IllegalStateException("not connected"); |
| } |
| } |
| |
| private void tearDownCarManagers() { |
| synchronized (mCarManagerLock) { |
| for (CarManagerBase manager: mServiceMap.values()) { |
| manager.onCarDisconnected(); |
| } |
| mServiceMap.clear(); |
| } |
| } |
| } |