Merge "Make call button behave like actual incoming call with proper flags."
diff --git a/FrameworkPackageStubs/Android.mk b/FrameworkPackageStubs/Android.mk
new file mode 100644
index 0000000..c6a68c8
--- /dev/null
+++ b/FrameworkPackageStubs/Android.mk
@@ -0,0 +1,11 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CarFrameworkPackageStubs
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_PACKAGE)
diff --git a/FrameworkPackageStubs/AndroidManifest.xml b/FrameworkPackageStubs/AndroidManifest.xml
new file mode 100644
index 0000000..42a2317
--- /dev/null
+++ b/FrameworkPackageStubs/AndroidManifest.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.frameworkpackagestubs"
+ android:versionCode="1"
+ android:versionName="1.0.1">
+
+ <uses-sdk android:minSdkVersion="28"/>
+
+ <application android:label="@string/app_name"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
+
+ <!-- Music / media stubs. -->
+ <activity android:name=".Stubs$MediaStub"
+ android:label="@string/stub_name"
+ android:excludeFromRecents="true">
+ <intent-filter android:priority="-1">
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="file"/>
+ <data android:mimeType="audio/*"/>
+ <data android:mimeType="application/ogg"/>
+ <data android:mimeType="application/x-ogg"/>
+ </intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"/>
+ <data android:mimeType="audio/*"/>
+ <data android:mimeType="application/ogg"/>
+ <data android:mimeType="application/x-ogg"/>
+ </intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="content"/>
+ <data android:mimeType="audio/*"/>
+ <data android:mimeType="application/ogg"/>
+ <data android:mimeType="application/x-ogg"/>
+ </intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.intent.action.PICK" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.cursor.dir/audio"/>
+ </intent-filter>
+ </activity>
+
+ <!-- Settings package stubs -->
+ <activity android:name=".Stubs$SettingsStub"
+ android:label="@string/stub_name"
+ android:excludeFromRecents="true">
+ <intent-filter android:priority="-1">
+ <action android:name="android.app.action.ADD_DEVICE_ADMIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.settings.ACCESSIBILITY_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.settings.USER_DICTIONARY_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ </application>
+</manifest>
diff --git a/FrameworkPackageStubs/res/values/strings.xml b/FrameworkPackageStubs/res/values/strings.xml
new file mode 100644
index 0000000..1446a89
--- /dev/null
+++ b/FrameworkPackageStubs/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Package name [CHAR LIMIT=NONE]-->
+ <string name="app_name">Activity Stub</string>
+
+ <!-- Toast message displayed when a stub activity is launched. [CHAR LIMIT=NONE] -->
+ <string name="message_not_supported">No application can handle this action</string>
+
+ <!-- Stub name [CHAR LIMIT=NONE]-->
+ <string name="stub_name">None</string>
+
+</resources>
diff --git a/FrameworkPackageStubs/src/com/android/car/frameworkpackagestubs/Stubs.java b/FrameworkPackageStubs/src/com/android/car/frameworkpackagestubs/Stubs.java
new file mode 100644
index 0000000..5ebcabd
--- /dev/null
+++ b/FrameworkPackageStubs/src/com/android/car/frameworkpackagestubs/Stubs.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.frameworkpackagestubs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.Toast;
+
+/**
+ * Contains different stub classes.
+ *
+ * <p>These are broken out so that the intent filters are easier to track and so that
+ * individual ones can create more specific messages if desired.
+ */
+public final class Stubs {
+
+ /**
+ * Base class for stubs.
+ *
+ * <p>Shows a toast and finishes.
+ *
+ * <p>Subclasses can override {@link #getMessage()} to customize the message.
+ */
+ private static class BaseActivity extends Activity {
+
+ private Toast mToast;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ showToast();
+ finish();
+ }
+
+ protected CharSequence getMessage() {
+ return getResources().getString(R.string.message_not_supported);
+ }
+
+ private void showToast() {
+ cancelToast();
+ mToast = Toast.makeText(this, getMessage(), Toast.LENGTH_LONG);
+ mToast.show();
+ }
+
+ private void cancelToast() {
+ if (mToast != null) {
+ mToast.cancel();
+ }
+ }
+ }
+
+ /**
+ * Stub activity for media events.
+ */
+ public static class MediaStub extends BaseActivity { }
+
+ /**
+ * Stub activity for setting events.
+ */
+ public static class SettingsStub extends BaseActivity { }
+}
diff --git a/car-lib/Android.bp b/car-lib/Android.bp
index 4e03d8c..2c8858a 100644
--- a/car-lib/Android.bp
+++ b/car-lib/Android.bp
@@ -215,6 +215,9 @@
},
},
compile_dex: true,
+ dist: {
+ targets: ["dist_files"],
+ }
}
java_library_static {
@@ -231,6 +234,9 @@
},
},
compile_dex: true,
+ dist: {
+ targets: ["dist_files"],
+ }
}
java_library_static {
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index e81aa91..ac38b8b 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -51,7 +51,7 @@
field public static final int APP_FOCUS_REQUEST_FAILED = 0; // 0x0
field public static final int APP_FOCUS_REQUEST_SUCCEEDED = 1; // 0x1
field public static final int APP_FOCUS_TYPE_NAVIGATION = 1; // 0x1
- field public static final int APP_FOCUS_TYPE_VOICE_COMMAND = 2; // 0x2
+ field @Deprecated public static final int APP_FOCUS_TYPE_VOICE_COMMAND = 2; // 0x2
}
public static interface CarAppFocusManager.OnAppFocusChangedListener {
@@ -298,7 +298,6 @@
method public int getMaxContentDepth();
method public int getMaxCumulativeContentItems();
method public int getMaxRestrictedStringLength();
- method public long getTimeStamp();
method public boolean isRequiresDistractionOptimization();
method public boolean isSameRestrictions(android.car.drivingstate.CarUxRestrictions);
method public void writeToParcel(android.os.Parcel, int);
diff --git a/car-lib/api/system-baseline.txt b/car-lib/api/system-baseline.txt
index 3d4f02d..72d9c6a 100644
--- a/car-lib/api/system-baseline.txt
+++ b/car-lib/api/system-baseline.txt
@@ -53,7 +53,4 @@
Documentation mentions 'TODO'
Todo: android.car.cluster.renderer.InstrumentClusterRenderer:
Documentation mentions 'TODO'
-Todo: android.car.drivingstate.CarDrivingStateEvent#DRIVING_STATE_IDLING:
- Documentation mentions 'TODO'
-
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 4ac83df..0975e90 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -1,6 +1,16 @@
// Signature format: 2.0
package android.car {
+ public abstract class AoapService extends android.app.Service {
+ ctor public AoapService();
+ method @MainThread public int canSwitchToAoap(@NonNull android.hardware.usb.UsbDevice);
+ method @MainThread public abstract int isDeviceSupported(@NonNull android.hardware.usb.UsbDevice);
+ method public android.os.IBinder onBind(android.content.Intent);
+ field public static final int RESULT_DEVICE_NOT_SUPPORTED = 1; // 0x1
+ field public static final int RESULT_DO_NOT_SWITCH_TO_AOAP = 2; // 0x2
+ field public static final int RESULT_OK = 0; // 0x0
+ }
+
public final class Car {
field public static final String CABIN_SERVICE = "cabin";
field public static final String CAR_DRIVING_STATE_SERVICE = "drivingstate";
@@ -49,8 +59,8 @@
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionListener(@NonNull android.car.CarProjectionManager.CarProjectionListener, int);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionRunner(@NonNull android.content.Intent);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void registerProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
- method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
- method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean requestBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int, @NonNull android.os.IBinder);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean releaseBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean requestBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void startProjectionAccessPoint(@NonNull android.car.CarProjectionManager.ProjectionAccessPointCallback);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void stopProjectionAccessPoint();
method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionListener();
@@ -820,7 +830,7 @@
method public void onSnapshot(android.car.storagemonitoring.IoStats);
}
- public class IoStats implements android.os.Parcelable {
+ public final class IoStats implements android.os.Parcelable {
ctor public IoStats(java.util.List<android.car.storagemonitoring.IoStatsEntry>, long);
ctor public IoStats(android.os.Parcel);
ctor public IoStats(org.json.JSONObject) throws org.json.JSONException;
@@ -866,7 +876,7 @@
field public final long fsyncCalls;
}
- public class LifetimeWriteInfo implements android.os.Parcelable {
+ public final class LifetimeWriteInfo implements android.os.Parcelable {
ctor public LifetimeWriteInfo(String, String, long);
ctor public LifetimeWriteInfo(android.os.Parcel);
ctor public LifetimeWriteInfo(org.json.JSONObject) throws org.json.JSONException;
@@ -894,7 +904,7 @@
field public final int uid;
}
- public class WearEstimate implements android.os.Parcelable {
+ public final class WearEstimate implements android.os.Parcelable {
ctor public WearEstimate(int, int);
ctor public WearEstimate(android.os.Parcel);
ctor public WearEstimate(android.util.JsonReader) throws java.io.IOException;
@@ -907,7 +917,7 @@
field @IntRange(from=0xffffffff, to=100) public final int typeB;
}
- public class WearEstimateChange implements android.os.Parcelable {
+ public final class WearEstimateChange implements android.os.Parcelable {
ctor public WearEstimateChange(android.car.storagemonitoring.WearEstimate, android.car.storagemonitoring.WearEstimate, long, java.time.Instant, boolean);
ctor public WearEstimateChange(android.os.Parcel);
method public int describeContents();
@@ -935,7 +945,7 @@
package android.car.trust {
public final class CarTrustAgentEnrollmentManager {
- method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void enrollmentHandshakeAccepted();
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void enrollmentHandshakeAccepted(android.bluetooth.BluetoothDevice);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) @NonNull public java.util.List<java.lang.Long> getEnrollmentHandlesForUser(int);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public void initiateEnrollmentHandshake(android.bluetooth.BluetoothDevice);
method @RequiresPermission(android.car.Car.PERMISSION_CAR_ENROLL_TRUST) public boolean isEscrowTokenActive(long, int);
@@ -966,10 +976,19 @@
package android.car.vms {
- public final class VmsAvailableLayers implements android.os.Parcelable {
- ctor public VmsAvailableLayers(java.util.Set<android.car.vms.VmsAssociatedLayer>, int);
+ public final class VmsAssociatedLayer implements android.os.Parcelable {
+ ctor public VmsAssociatedLayer(@NonNull android.car.vms.VmsLayer, @NonNull java.util.Set<java.lang.Integer>);
method public int describeContents();
- method public java.util.Set<android.car.vms.VmsAssociatedLayer> getAssociatedLayers();
+ method @NonNull public java.util.Set<java.lang.Integer> getPublisherIds();
+ method @NonNull public android.car.vms.VmsLayer getVmsLayer();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.car.vms.VmsAssociatedLayer> CREATOR;
+ }
+
+ public final class VmsAvailableLayers implements android.os.Parcelable {
+ ctor public VmsAvailableLayers(@NonNull java.util.Set<android.car.vms.VmsAssociatedLayer>, int);
+ method public int describeContents();
+ method @NonNull public java.util.Set<android.car.vms.VmsAssociatedLayer> getAssociatedLayers();
method public int getSequence();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.car.vms.VmsAvailableLayers> CREATOR;
@@ -986,17 +1005,17 @@
}
public final class VmsLayerDependency implements android.os.Parcelable {
- ctor public VmsLayerDependency(android.car.vms.VmsLayer, java.util.Set<android.car.vms.VmsLayer>);
- ctor public VmsLayerDependency(android.car.vms.VmsLayer);
+ ctor public VmsLayerDependency(@NonNull android.car.vms.VmsLayer, @NonNull java.util.Set<android.car.vms.VmsLayer>);
+ ctor public VmsLayerDependency(@NonNull android.car.vms.VmsLayer);
method public int describeContents();
- method public java.util.Set<android.car.vms.VmsLayer> getDependencies();
- method public android.car.vms.VmsLayer getLayer();
+ method @NonNull public java.util.Set<android.car.vms.VmsLayer> getDependencies();
+ method @NonNull public android.car.vms.VmsLayer getLayer();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.car.vms.VmsLayerDependency> CREATOR;
}
public final class VmsLayersOffering implements android.os.Parcelable {
- ctor public VmsLayersOffering(java.util.Set<android.car.vms.VmsLayerDependency>, int);
+ ctor public VmsLayersOffering(@NonNull java.util.Set<android.car.vms.VmsLayerDependency>, int);
method public int describeContents();
method public java.util.Set<android.car.vms.VmsLayerDependency> getDependencies();
method public int getPublisherId();
@@ -1028,30 +1047,40 @@
public abstract class VmsPublisherClientService extends android.app.Service {
ctor public VmsPublisherClientService();
method public final int getPublisherId(byte[]);
- method @Nullable public final android.car.vms.VmsSubscriptionState getSubscriptions();
+ method public final android.car.vms.VmsSubscriptionState getSubscriptions();
method public android.os.IBinder onBind(android.content.Intent);
method protected abstract void onVmsPublisherServiceReady();
- method public abstract void onVmsSubscriptionChange(android.car.vms.VmsSubscriptionState);
- method public final void publish(android.car.vms.VmsLayer, int, byte[]);
- method public final void setLayersOffering(android.car.vms.VmsLayersOffering);
+ method public abstract void onVmsSubscriptionChange(@NonNull android.car.vms.VmsSubscriptionState);
+ method public final void publish(@NonNull android.car.vms.VmsLayer, int, byte[]);
+ method public final void setLayersOffering(@NonNull android.car.vms.VmsLayersOffering);
}
public final class VmsSubscriberManager {
method public void clearVmsSubscriberClientCallback();
- method public android.car.vms.VmsAvailableLayers getAvailableLayers();
- method public byte[] getPublisherInfo(int);
+ method @NonNull public android.car.vms.VmsAvailableLayers getAvailableLayers();
+ method @NonNull public byte[] getPublisherInfo(int);
method public void setVmsSubscriberClientCallback(@NonNull java.util.concurrent.Executor, @NonNull android.car.vms.VmsSubscriberManager.VmsSubscriberClientCallback);
method public void startMonitoring();
method public void stopMonitoring();
- method public void subscribe(android.car.vms.VmsLayer);
- method public void subscribe(android.car.vms.VmsLayer, int);
- method public void unsubscribe(android.car.vms.VmsLayer);
- method public void unsubscribe(android.car.vms.VmsLayer, int);
+ method public void subscribe(@NonNull android.car.vms.VmsLayer);
+ method public void subscribe(@NonNull android.car.vms.VmsLayer, int);
+ method public void unsubscribe(@NonNull android.car.vms.VmsLayer);
+ method public void unsubscribe(@NonNull android.car.vms.VmsLayer, int);
}
public static interface VmsSubscriberManager.VmsSubscriberClientCallback {
- method public void onLayersAvailabilityChanged(android.car.vms.VmsAvailableLayers);
- method public void onVmsMessageReceived(android.car.vms.VmsLayer, byte[]);
+ method public void onLayersAvailabilityChanged(@NonNull android.car.vms.VmsAvailableLayers);
+ method public void onVmsMessageReceived(@NonNull android.car.vms.VmsLayer, byte[]);
+ }
+
+ public final class VmsSubscriptionState implements android.os.Parcelable {
+ ctor public VmsSubscriptionState(int, @NonNull java.util.Set<android.car.vms.VmsLayer>, @NonNull java.util.Set<android.car.vms.VmsAssociatedLayer>);
+ method public int describeContents();
+ method @NonNull public java.util.Set<android.car.vms.VmsAssociatedLayer> getAssociatedLayers();
+ method @NonNull public java.util.Set<android.car.vms.VmsLayer> getLayers();
+ method public int getSequenceNumber();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.car.vms.VmsSubscriptionState> CREATOR;
}
}
diff --git a/car-lib/api/test-baseline.txt b/car-lib/api/test-baseline.txt
index d71a9e5..6cb7b91 100644
--- a/car-lib/api/test-baseline.txt
+++ b/car-lib/api/test-baseline.txt
@@ -1,8 +1,4 @@
// Baseline format: 1.0
-HiddenTypeParameter: android.car.drivingstate.CarUxRestrictionsManager#getConfig():
- Method android.car.drivingstate.CarUxRestrictionsManager.getConfig() references hidden type android.car.drivingstate.CarUxRestrictionsConfiguration.
-HiddenTypeParameter: android.car.drivingstate.CarUxRestrictionsManager#getStagedConfig():
- Method android.car.drivingstate.CarUxRestrictionsManager.getStagedConfig() references hidden type android.car.drivingstate.CarUxRestrictionsConfiguration.
HiddenTypeParameter: android.car.hardware.CarSensorManager#getPropertyList():
Method android.car.hardware.CarSensorManager.getPropertyList() references hidden type class android.car.hardware.CarPropertyConfig.
HiddenTypeParameter: android.car.navigation.CarNavigationStatusManager#getInstrumentClusterInfo():
@@ -25,16 +21,7 @@
Typedef references constant which isn't part of the API, skipping in documentation: android.car.hardware.CarSensorManager#SENSOR_TYPE_ENGINE_OIL_LEVEL
-MissingPermission: android.car.drivingstate.CarUxRestrictionsManager#getConfig():
- Permission Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION required by method android.car.drivingstate.CarUxRestrictionsManager.getConfig() is hidden or removed
-MissingPermission: android.car.drivingstate.CarUxRestrictionsManager#getStagedConfig():
- Permission Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION required by method android.car.drivingstate.CarUxRestrictionsManager.getStagedConfig() is hidden or removed
-
-ReferencesHidden: android.car.drivingstate.CarUxRestrictionsManager#getConfig():
- Class android.car.drivingstate.CarUxRestrictionsConfiguration is hidden but was referenced (as return type) from public method android.car.drivingstate.CarUxRestrictionsManager.getConfig()
-ReferencesHidden: android.car.drivingstate.CarUxRestrictionsManager#getStagedConfig():
- Class android.car.drivingstate.CarUxRestrictionsConfiguration is hidden but was referenced (as return type) from public method android.car.drivingstate.CarUxRestrictionsManager.getStagedConfig()
ReferencesHidden: android.car.hardware.CarSensorManager#getPropertyList():
Class android.car.hardware.CarPropertyConfig is hidden but was referenced (as return type parameter) from public method android.car.hardware.CarSensorManager.getPropertyList()
ReferencesHidden: android.car.navigation.CarNavigationStatusManager#getInstrumentClusterInfo():
diff --git a/car-lib/api/test-current.txt b/car-lib/api/test-current.txt
index 6658de9..d6b162e 100644
--- a/car-lib/api/test-current.txt
+++ b/car-lib/api/test-current.txt
@@ -7,15 +7,6 @@
}
-package android.car.drivingstate {
-
- public final class CarUxRestrictionsManager {
- method @RequiresPermission("android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION") public android.car.drivingstate.CarUxRestrictionsConfiguration getConfig();
- method @Nullable @RequiresPermission("android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION") public android.car.drivingstate.CarUxRestrictionsConfiguration getStagedConfig();
- }
-
-}
-
package android.car.media {
public final class CarAudioManager {
diff --git a/car-lib/src/android/car/AoapService.java b/car-lib/src/android/car/AoapService.java
new file mode 100644
index 0000000..20b7fa7
--- /dev/null
+++ b/car-lib/src/android/car/AoapService.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2019 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.car;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.IntDef;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+/**
+ * The service that must be implemented by USB AOAP handler system apps. The app must hold the
+ * following permission: {@code android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE}.
+ *
+ * <p>This service gets bound by the framework and the service needs to be protected by
+ * {@code android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE} permission to ensure nobody else can
+ * bind to the service. At most only one client should be bound at a time.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class AoapService extends Service {
+ private static final String TAG = AoapService.class.getSimpleName();
+
+ /** Indicates success or confirmation. */
+ public static final int RESULT_OK = 0;
+
+ /**
+ * Indicates that the device is not supported by this service and system shouldn't associate
+ * given device with this service.
+ */
+ public static final int RESULT_DEVICE_NOT_SUPPORTED = 1;
+
+ /**
+ * Indicates that device shouldn't be switch to AOAP mode at this time.
+ */
+ public static final int RESULT_DO_NOT_SWITCH_TO_AOAP = 2;
+
+ /** @hide */
+ @IntDef(value = {
+ RESULT_OK, RESULT_DEVICE_NOT_SUPPORTED, RESULT_DO_NOT_SWITCH_TO_AOAP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Result {}
+
+
+ /**
+ * A message sent from the system USB handler service to AOAP handler service to check if the
+ * device is supported. The message must have {@link #KEY_DEVICE} with {@link UsbDevice} object.
+ *
+ * @hide
+ */
+ public static final int MSG_NEW_DEVICE_ATTACHED = 1;
+
+ /**
+ * A response message for {@link #MSG_NEW_DEVICE_ATTACHED}. Must contain {@link #KEY_RESULT}
+ * with one of the {@code RESULT_*} constant.
+ *
+ * @hide */
+ public static final int MSG_NEW_DEVICE_ATTACHED_RESPONSE = 2;
+
+ /**
+ * A message sent from the system USB handler service to AOAP handler service to check if the
+ * device can be switched to AOAP mode. The message must have {@link #KEY_DEVICE} with
+ * {@link UsbDevice} object.
+ *
+ * @hide
+ */
+ public static final int MSG_CAN_SWITCH_TO_AOAP = 3;
+
+ /**
+ * A response message for {@link #MSG_CAN_SWITCH_TO_AOAP}. Must contain {@link #KEY_RESULT}
+ * with one of the {@code RESULT_*} constant.
+ *
+ * @hide */
+ public static final int MSG_CAN_SWITCH_TO_AOAP_RESPONSE = 4;
+
+ /** @hide */
+ public static final String KEY_DEVICE = "usb-device";
+
+ /** @hide */
+ public static final String KEY_RESULT = "result";
+
+
+ /**
+ * Returns {@code true} if the given USB device is supported by this service.
+ *
+ * <p>The device is not expected to be in AOAP mode when this method is called. The purpose of
+ * this method is just to give the service a chance to tell whether based on the information
+ * provided in {@link UsbDevice} class (e.g. PID/VID) this service supports or doesn't support
+ * given device.
+ *
+ * <p>The method must return one of the following status: {@link #RESULT_OK} or
+ * {@link #RESULT_DEVICE_NOT_SUPPORTED}
+ */
+ @MainThread
+ public abstract @Result int isDeviceSupported(@NonNull UsbDevice device);
+
+ /**
+ * This method will be called at least once per connection session before switching device into
+ * AOAP mode.
+ *
+ * <p>The device is connected, but not in AOAP mode yet. Implementors of this method may ask
+ * the framework to ignore this device for now and do not switch to AOAP. This may make sense if
+ * a connection to the device has been established through other means, and switching the device
+ * to AOAP would break that connection.
+ *
+ * <p>Note: the method may be called only if this device was claimed to be supported in
+ * {@link #isDeviceSupported(UsbDevice)} method, and this app has been chosen to handle the
+ * device.
+ *
+ * <p>The method must return one of the following status: {@link #RESULT_OK},
+ * {@link #RESULT_DEVICE_NOT_SUPPORTED} or {@link #RESULT_DO_NOT_SWITCH_TO_AOAP}
+ */
+ @MainThread
+ public @Result int canSwitchToAoap(@NonNull UsbDevice device) {
+ return RESULT_OK;
+ }
+
+ private Messenger mMessenger;
+ private boolean mBound;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mMessenger = new Messenger(new IncomingHandler(this));
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mBound) {
+ Log.w(TAG, "Received onBind event when the service was already bound");
+ }
+ mBound = true;
+ return mMessenger.getBinder();
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ mBound = false;
+ return super.onUnbind(intent);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.write("Bound: " + mBound);
+ }
+
+ private static class IncomingHandler extends Handler {
+ private final WeakReference<AoapService> mServiceRef;
+
+ IncomingHandler(AoapService service) {
+ super(Looper.getMainLooper());
+ mServiceRef = new WeakReference<>(service);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ AoapService service = mServiceRef.get();
+ if (service == null) {
+ return;
+ }
+ Bundle data = msg.getData();
+ if (data == null) {
+ Log.e(TAG, "Ignoring message " + msg.what + " without data");
+ return;
+ }
+
+ Log.i(TAG, "Message received: " + msg.what);
+
+ switch (msg.what) {
+ case MSG_NEW_DEVICE_ATTACHED: {
+ int res = service.isDeviceSupported(
+ checkNotNull(data.getParcelable(KEY_DEVICE)));
+ if (res != RESULT_OK && res != RESULT_DEVICE_NOT_SUPPORTED) {
+ throw new IllegalArgumentException("Result can not be " + res);
+ }
+ sendResponse(msg.replyTo, MSG_NEW_DEVICE_ATTACHED_RESPONSE, res);
+ break;
+ }
+
+ case MSG_CAN_SWITCH_TO_AOAP: {
+ int res = service.canSwitchToAoap(
+ checkNotNull(data.getParcelable(KEY_DEVICE)));
+ if (res != RESULT_OK && res != RESULT_DEVICE_NOT_SUPPORTED
+ && res != RESULT_DO_NOT_SWITCH_TO_AOAP) {
+ throw new IllegalArgumentException("Result can not be " + res);
+ }
+ sendResponse(msg.replyTo, MSG_CAN_SWITCH_TO_AOAP_RESPONSE, res);
+ break;
+ }
+
+ default:
+ Log.e(TAG, "Unknown message received: " + msg.what);
+ break;
+ }
+ }
+
+ private void sendResponse(Messenger messenger, int msg, int result) {
+ try {
+ messenger.send(createResponseMessage(msg, result));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send message", e);
+ }
+ }
+
+ private Message createResponseMessage(int msg, int result) {
+ Message response = Message.obtain(null, msg);
+ Bundle data = new Bundle();
+ data.putInt(KEY_RESULT, result);
+ response.setData(data);
+ return response;
+ }
+ }
+}
diff --git a/car-lib/src/android/car/CarAppFocusManager.java b/car-lib/src/android/car/CarAppFocusManager.java
index 57dc4c5..ff0c25d 100644
--- a/car-lib/src/android/car/CarAppFocusManager.java
+++ b/car-lib/src/android/car/CarAppFocusManager.java
@@ -78,7 +78,10 @@
public static final int APP_FOCUS_TYPE_NAVIGATION = 1;
/**
* Represents voice command focus.
+ *
+ * @deprecated use {@link android.service.voice.VoiceInteractionService} instead.
*/
+ @Deprecated
public static final int APP_FOCUS_TYPE_VOICE_COMMAND = 2;
/**
* Update this after adding a new app type.
@@ -89,7 +92,6 @@
/** @hide */
@IntDef({
APP_FOCUS_TYPE_NAVIGATION,
- APP_FOCUS_TYPE_VOICE_COMMAND
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppFocusType {}
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 7706bf7..8b5bc82 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -297,17 +297,14 @@
*
* @param device The device on which to inhibit a profile.
* @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit.
- * @param token A {@link IBinder} to be used as an identity for the request. If the process
- * owning the token dies, the request will automatically be released.
* @return True if the profile was successfully inhibited, false if an error occurred.
*/
@RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public boolean requestBluetoothProfileInhibit(
- @NonNull BluetoothDevice device, int profile, @NonNull IBinder token) {
+ @NonNull BluetoothDevice device, int profile) {
Preconditions.checkNotNull(device, "device cannot be null");
- Preconditions.checkNotNull(token, "token cannot be null");
try {
- return mService.requestBluetoothProfileInhibit(device, profile, token);
+ return mService.requestBluetoothProfileInhibit(device, profile, mToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -319,17 +316,13 @@
*
* @param device The device on which to release the inhibit request.
* @param profile The profile on which to release the inhibit request.
- * @param token The token provided in the original call to
- * {@link #requestBluetoothProfileInhibit}.
* @return True if the request was released, false if an error occurred.
*/
@RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
- public boolean releaseBluetoothProfileInhibit(
- BluetoothDevice device, int profile, IBinder token) {
+ public boolean releaseBluetoothProfileInhibit(@NonNull BluetoothDevice device, int profile) {
Preconditions.checkNotNull(device, "device cannot be null");
- Preconditions.checkNotNull(token, "token cannot be null");
try {
- return mService.releaseBluetoothProfileInhibit(device, profile, token);
+ return mService.releaseBluetoothProfileInhibit(device, profile, mToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -341,7 +334,7 @@
*
* @param status the reported status that will be distributed to the interested listeners
*
- * @see #registerProjectionListener(CarProjectionListener, int)
+ * @see #registerProjectionStatusListener(ProjectionStatusListener)
*/
@RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public void updateProjectionStatus(@NonNull ProjectionStatus status) {
diff --git a/car-lib/src/android/car/IUsbAoapSupportCheckService.aidl b/car-lib/src/android/car/IUsbAoapSupportCheckService.aidl
deleted file mode 100644
index 3e77357..0000000
--- a/car-lib/src/android/car/IUsbAoapSupportCheckService.aidl
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2016 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.car;
-
-import android.hardware.usb.UsbDevice;
-
-/**
- * Binder interface for service to check USB AOAP mode support.
- *
- * @hide
- */
-interface IUsbAoapSupportCheckService {
- boolean isDeviceSupported(in UsbDevice device) = 0;
-}
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java b/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
index 77dc08d..bd553ce 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
@@ -44,7 +44,6 @@
public static final int DRIVING_STATE_PARKED = 0;
/**
* Car is idling. Gear is not in Parked mode and Speed of the vehicle is zero.
- * TODO: (b/72157869) - Should speed that differentiates moving vs idling be configurable?
*/
public static final int DRIVING_STATE_IDLING = 1;
/**
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictions.java b/car-lib/src/android/car/drivingstate/CarUxRestrictions.java
index 149f1dc..72cd47c 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictions.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictions.java
@@ -220,6 +220,8 @@
* Time at which this UX restriction event was deduced based on the car's driving state.
*
* @return Elapsed time in nanoseconds since system boot.
+ *
+ * @hide
*/
public long getTimeStamp() {
return mTimeStamp;
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
index 06d19df..4ca54a7 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
@@ -15,9 +15,18 @@
*/
package android.car.drivingstate;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_UNKNOWN;
+import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_BASELINE;
+import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_PASSENGER;
+
import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
+import android.car.drivingstate.CarUxRestrictionsManager.UxRestrictionMode;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -27,10 +36,12 @@
import android.util.JsonWriter;
import android.util.Log;
+import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -51,6 +62,14 @@
private static final String JSON_NAME_IDLING_RESTRICTIONS = "idling_restrictions";
private static final String JSON_NAME_PARKED_RESTRICTIONS = "parked_restrictions";
private static final String JSON_NAME_UNKNOWN_RESTRICTIONS = "unknown_restrictions";
+ private static final String JSON_NAME_PASSENGER_MOVING_RESTRICTIONS =
+ "passenger_moving_restrictions";
+ private static final String JSON_NAME_PASSENGER_IDLING_RESTRICTIONS =
+ "passenger_idling_restrictions";
+ private static final String JSON_NAME_PASSENGER_PARKED_RESTRICTIONS =
+ "passenger_parked_restrictions";
+ private static final String JSON_NAME_PASSENGER_UNKNOWN_RESTRICTIONS =
+ "passenger_unknown_restrictions";
private static final String JSON_NAME_REQ_OPT = "req_opt";
private static final String JSON_NAME_RESTRICTIONS = "restrictions";
private static final String JSON_NAME_SPEED_RANGE = "speed_range";
@@ -60,7 +79,9 @@
private final int mMaxContentDepth;
private final int mMaxCumulativeContentItems;
private final int mMaxStringLength;
- private final Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+ private final Map<Integer, List<RestrictionsPerSpeedRange>> mPassengerUxRestrictions =
+ new ArrayMap<>(DRIVING_STATES.length);
+ private final Map<Integer, List<RestrictionsPerSpeedRange>> mBaselineUxRestrictions =
new ArrayMap<>(DRIVING_STATES.length);
private CarUxRestrictionsConfiguration(CarUxRestrictionsConfiguration.Builder builder) {
@@ -69,51 +90,89 @@
mMaxStringLength = builder.mMaxStringLength;
for (int drivingState : DRIVING_STATES) {
- List<RestrictionsPerSpeedRange> list = new ArrayList<>();
- for (RestrictionsPerSpeedRange r : builder.mUxRestrictions.get(drivingState)) {
- list.add(r);
+ List<RestrictionsPerSpeedRange> baseline = new ArrayList<>();
+ for (RestrictionsPerSpeedRange r : builder.mBaselineUxRestrictions.get(drivingState)) {
+ baseline.add(r);
}
- mUxRestrictions.put(drivingState, list);
+ mBaselineUxRestrictions.put(drivingState, baseline);
+
+ List<RestrictionsPerSpeedRange> passenger = new ArrayList<>();
+ for (RestrictionsPerSpeedRange r : builder.mPassengerUxRestrictions.get(drivingState)) {
+ passenger.add(r);
+ }
+ mPassengerUxRestrictions.put(drivingState, passenger);
}
}
/**
- * Returns the restrictions based on current driving state and speed.
+ * Returns the restrictions for
+ * {@link UxRestrictionMode#UX_RESTRICTION_MODE_BASELINE}
+ * based on current driving state.
+ *
+ * @param drivingState Driving state.
+ * See values in {@link CarDrivingStateEvent.CarDrivingState}.
+ * @param currentSpeed Current speed in meter per second.
+ */
+ public CarUxRestrictions getUxRestrictions(
+ @CarDrivingState int drivingState, float currentSpeed) {
+ return getUxRestrictions(drivingState, currentSpeed, UX_RESTRICTION_MODE_BASELINE);
+ }
+
+ /**
+ * Returns the restrictions based on current driving state and restriction mode.
+ *
+ * <p>Restriction mode allows a different set of restrictions to be applied in the same driving
+ * state. See values in {@link UxRestrictionMode}.
+ *
+ * @param drivingState Driving state.
+ * See values in {@link CarDrivingStateEvent.CarDrivingState}.
+ * @param currentSpeed Current speed in meter per second.
+ * @param mode Current UX Restriction mode.
*/
public CarUxRestrictions getUxRestrictions(@CarDrivingState int drivingState,
- float currentSpeed) {
- List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
- if (restrictions.isEmpty()) {
+ float currentSpeed, @UxRestrictionMode int mode) {
+ RestrictionsPerSpeedRange restriction = null;
+ if (mode == UX_RESTRICTION_MODE_PASSENGER) {
+ restriction = findUxRestrictionsInList(
+ currentSpeed, mPassengerUxRestrictions.get(drivingState));
+ }
+ if (restriction == null) {
+ // Mode is baseline, or passenger mode does not specify restrictions for current driving
+ // state.
+ restriction = findUxRestrictionsInList(
+ currentSpeed, mBaselineUxRestrictions.get(drivingState));
+ }
+
+ if (restriction == null) {
if (Build.IS_ENG || Build.IS_USERDEBUG) {
throw new IllegalStateException("No restrictions for driving state "
+ getDrivingStateName(drivingState));
}
return createDefaultUxRestrictionsEvent();
}
-
- RestrictionsPerSpeedRange restriction = null;
- if (restrictions.size() == 1) {
- restriction = restrictions.get(0);
- } else {
- for (RestrictionsPerSpeedRange r : restrictions) {
- if (r.mSpeedRange != null && r.mSpeedRange.includes(currentSpeed)) {
- restriction = r;
- break;
- }
- }
- }
-
- if (restriction == null) {
- if (Build.IS_ENG || Build.IS_USERDEBUG) {
- throw new IllegalStateException(
- "No restrictions found for driving state " + drivingState
- + " at speed " + currentSpeed);
- }
- return createDefaultUxRestrictionsEvent();
- }
return createUxRestrictionsEvent(restriction.mReqOpt, restriction.mRestrictions);
}
+ @Nullable
+ private static RestrictionsPerSpeedRange findUxRestrictionsInList(float currentSpeed,
+ List<RestrictionsPerSpeedRange> restrictions) {
+ if (restrictions.isEmpty()) {
+ return null;
+ }
+
+ if (restrictions.size() == 1 && restrictions.get(0).mSpeedRange == null) {
+ // Single restriction with no speed range implies it covers all.
+ return restrictions.get(0);
+ }
+
+ for (RestrictionsPerSpeedRange r : restrictions) {
+ if (r.mSpeedRange != null && r.mSpeedRange.includes(currentSpeed)) {
+ return r;
+ }
+ }
+ return null;
+ }
+
private CarUxRestrictions createDefaultUxRestrictionsEvent() {
return createUxRestrictionsEvent(true,
CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
@@ -162,19 +221,35 @@
writer.name(JSON_NAME_PARKED_RESTRICTIONS);
writeRestrictionsList(writer,
- mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_PARKED));
+ mBaselineUxRestrictions.get(DRIVING_STATE_PARKED));
writer.name(JSON_NAME_IDLING_RESTRICTIONS);
writeRestrictionsList(writer,
- mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_IDLING));
+ mBaselineUxRestrictions.get(DRIVING_STATE_IDLING));
writer.name(JSON_NAME_MOVING_RESTRICTIONS);
writeRestrictionsList(writer,
- mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_MOVING));
+ mBaselineUxRestrictions.get(DRIVING_STATE_MOVING));
writer.name(JSON_NAME_UNKNOWN_RESTRICTIONS);
writeRestrictionsList(writer,
- mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN));
+ mBaselineUxRestrictions.get(DRIVING_STATE_UNKNOWN));
+
+ writer.name(JSON_NAME_PASSENGER_PARKED_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mPassengerUxRestrictions.get(DRIVING_STATE_PARKED));
+
+ writer.name(JSON_NAME_PASSENGER_IDLING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mPassengerUxRestrictions.get(DRIVING_STATE_IDLING));
+
+ writer.name(JSON_NAME_PASSENGER_MOVING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mPassengerUxRestrictions.get(DRIVING_STATE_MOVING));
+
+ writer.name(JSON_NAME_PASSENGER_UNKNOWN_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mPassengerUxRestrictions.get(DRIVING_STATE_UNKNOWN));
writer.endObject();
}
@@ -203,6 +278,19 @@
writer.endObject();
}
+ @Override
+ public String toString() {
+ CharArrayWriter charWriter = new CharArrayWriter();
+ JsonWriter writer = new JsonWriter(charWriter);
+ writer.setIndent("\t");
+ try {
+ writeJson(writer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return charWriter.toString();
+ }
+
/**
* Reads Json as UX restriction configuration.
*/
@@ -214,23 +302,51 @@
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
- if (name.equals(JSON_NAME_MAX_CONTENT_DEPTH)) {
- builder.setMaxContentDepth(reader.nextInt());
- } else if (name.equals(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS)) {
- builder.setMaxCumulativeContentItems(reader.nextInt());
- } else if (name.equals(JSON_NAME_MAX_STRING_LENGTH)) {
- builder.setMaxStringLength(reader.nextInt());
- } else if (name.equals(JSON_NAME_PARKED_RESTRICTIONS)) {
- readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_PARKED, builder);
- } else if (name.equals(JSON_NAME_IDLING_RESTRICTIONS)) {
- readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_IDLING, builder);
- } else if (name.equals(JSON_NAME_MOVING_RESTRICTIONS)) {
- readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_MOVING, builder);
- } else if (name.equals(JSON_NAME_UNKNOWN_RESTRICTIONS)) {
- readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_UNKNOWN, builder);
- } else {
- Log.e(TAG, "Unknown name parsing json config: " + name);
- reader.skipValue();
+ switch (name) {
+ case JSON_NAME_MAX_CONTENT_DEPTH:
+ builder.setMaxContentDepth(reader.nextInt());
+ break;
+ case JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS:
+ builder.setMaxCumulativeContentItems(reader.nextInt());
+ break;
+ case JSON_NAME_MAX_STRING_LENGTH:
+ builder.setMaxStringLength(reader.nextInt());
+ break;
+ case JSON_NAME_PARKED_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_PARKED,
+ UX_RESTRICTION_MODE_BASELINE, builder);
+ break;
+ case JSON_NAME_IDLING_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_IDLING,
+ UX_RESTRICTION_MODE_BASELINE, builder);
+ break;
+ case JSON_NAME_MOVING_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_MOVING,
+ UX_RESTRICTION_MODE_BASELINE, builder);
+ break;
+ case JSON_NAME_UNKNOWN_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_UNKNOWN,
+ UX_RESTRICTION_MODE_BASELINE, builder);
+ break;
+ case JSON_NAME_PASSENGER_PARKED_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_PARKED,
+ UX_RESTRICTION_MODE_PASSENGER, builder);
+ break;
+ case JSON_NAME_PASSENGER_IDLING_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_IDLING,
+ UX_RESTRICTION_MODE_PASSENGER, builder);
+ break;
+ case JSON_NAME_PASSENGER_MOVING_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_MOVING,
+ UX_RESTRICTION_MODE_PASSENGER, builder);
+ break;
+ case JSON_NAME_PASSENGER_UNKNOWN_RESTRICTIONS:
+ readRestrictionsList(reader, DRIVING_STATE_UNKNOWN,
+ UX_RESTRICTION_MODE_PASSENGER, builder);
+ break;
+ default:
+ Log.e(TAG, "Unknown name parsing json config: " + name);
+ reader.skipValue();
}
}
reader.endObject();
@@ -238,16 +354,18 @@
}
private static void readRestrictionsList(JsonReader reader, @CarDrivingState int drivingState,
- Builder builder) throws IOException {
+ @UxRestrictionMode int mode, Builder builder) throws IOException {
reader.beginArray();
while (reader.hasNext()) {
- readRestrictions(reader, drivingState, builder);
+ DrivingStateRestrictions drivingStateRestrictions = readRestrictions(reader);
+ drivingStateRestrictions.setMode(mode);
+
+ builder.setUxRestrictions(drivingState, drivingStateRestrictions);
}
reader.endArray();
}
- private static void readRestrictions(JsonReader reader, @CarDrivingState int drivingState,
- Builder builder) throws IOException {
+ private static DrivingStateRestrictions readRestrictions(JsonReader reader) throws IOException {
reader.beginObject();
boolean reqOpt = false;
int restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
@@ -280,7 +398,13 @@
}
}
reader.endObject();
- builder.setUxRestrictions(drivingState, speedRange, reqOpt, restrictions);
+ DrivingStateRestrictions drivingStateRestrictions = new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(reqOpt)
+ .setRestrictions(restrictions);
+ if (speedRange != null) {
+ drivingStateRestrictions.setSpeedRange(speedRange);
+ }
+ return drivingStateRestrictions;
}
@Override
@@ -301,24 +425,27 @@
return false;
}
- // Compare UXR by driving state.
- if (!mUxRestrictions.keySet().equals(other.mUxRestrictions.keySet())) {
+ // Compare UXR for each restriction mode.
+ if (!areRestrictionsEqual(mBaselineUxRestrictions, other.mBaselineUxRestrictions)
+ || !areRestrictionsEqual(
+ mPassengerUxRestrictions, other.mPassengerUxRestrictions)) {
return false;
}
- for (int drivingState : mUxRestrictions.keySet()) {
- List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(
- drivingState);
- List<RestrictionsPerSpeedRange> otherRestrictions = other.mUxRestrictions.get(
- drivingState);
- if (restrictions.size() != otherRestrictions.size()) {
+ return true;
+ }
+
+ private boolean areRestrictionsEqual(Map<Integer, List<RestrictionsPerSpeedRange>> r1,
+ Map<Integer, List<RestrictionsPerSpeedRange>> r2) {
+ // Compare UXR by driving state.
+ if (!r1.keySet().equals(r2.keySet())) {
+ return false;
+ }
+ for (int drivingState : r1.keySet()) {
+ List<RestrictionsPerSpeedRange> restrictions = r1.get(drivingState);
+ List<RestrictionsPerSpeedRange> otherRestrictions = r2.get(drivingState);
+ if (!restrictions.equals(otherRestrictions)) {
return false;
}
- // Assuming the restrictions are sorted.
- for (int i = 0; i < restrictions.size(); i++) {
- if (!restrictions.get(i).equals(otherRestrictions.get(i))) {
- return false;
- }
- }
}
return true;
}
@@ -327,49 +454,61 @@
* Dump the driving state to UX restrictions mapping.
*/
public void dump(PrintWriter writer) {
- for (Integer state : mUxRestrictions.keySet()) {
- List<RestrictionsPerSpeedRange> list = mUxRestrictions.get(state);
- writer.println("===========================================");
- writer.println("Driving State to UXR");
- if (list != null) {
- writer.println("State:" + getDrivingStateName(state) + " num restrictions:"
- + list.size());
- for (RestrictionsPerSpeedRange r : list) {
- writer.println("Requires DO? " + r.mReqOpt
- + "\nRestrictions: 0x" + Integer.toHexString(r.mRestrictions)
- + "\nSpeed Range: " + (r.mSpeedRange == null
- ? "None"
- : r.mSpeedRange.mMinSpeed + " - " + r.mSpeedRange.mMaxSpeed));
- writer.println("===========================================");
- }
- }
- }
+ writer.println("===========================================");
+ writer.println("Baseline mode UXR:");
+ writer.println("-------------------------------------------");
+ dumpRestrictions(writer, mBaselineUxRestrictions);
+ writer.println("Passenger mode UXR:");
+ writer.println("-------------------------------------------");
+ dumpRestrictions(writer, mPassengerUxRestrictions);
+
writer.println("Max String length: " + mMaxStringLength);
writer.println("Max Cumulative Content Items: " + mMaxCumulativeContentItems);
writer.println("Max Content depth: " + mMaxContentDepth);
+ writer.println("===========================================");
+ }
+
+ private void dumpRestrictions(
+ PrintWriter writer, Map<Integer, List<RestrictionsPerSpeedRange>> restrictions) {
+ for (Integer state : restrictions.keySet()) {
+ List<RestrictionsPerSpeedRange> list = restrictions.get(state);
+ writer.println("State:" + getDrivingStateName(state)
+ + " num restrictions:" + list.size());
+ for (RestrictionsPerSpeedRange r : list) {
+ writer.println("Requires DO? " + r.mReqOpt
+ + "\nRestrictions: 0x" + Integer.toHexString(r.mRestrictions)
+ + "\nSpeed Range: "
+ + (r.mSpeedRange == null
+ ? "None"
+ : (r.mSpeedRange.mMinSpeed + " - " + r.mSpeedRange.mMaxSpeed)));
+ writer.println("-------------------------------------------");
+ }
+ }
}
private static String getDrivingStateName(@CarDrivingState int state) {
switch (state) {
- case 0:
+ case DRIVING_STATE_PARKED:
return "parked";
- case 1:
+ case DRIVING_STATE_IDLING:
return "idling";
- case 2:
+ case DRIVING_STATE_MOVING:
return "moving";
- default:
+ case DRIVING_STATE_UNKNOWN:
return "unknown";
+ default:
+ throw new IllegalArgumentException("Unrecognized state value: " + state);
}
}
// Parcelable methods/fields.
// Used by Parcel methods to ensure de/serialization order.
- private static final int[] DRIVING_STATES = new int[]{
- CarDrivingStateEvent.DRIVING_STATE_UNKNOWN,
- CarDrivingStateEvent.DRIVING_STATE_PARKED,
- CarDrivingStateEvent.DRIVING_STATE_IDLING,
- CarDrivingStateEvent.DRIVING_STATE_MOVING
+ private static final int[] DRIVING_STATES = new int[] {
+ DRIVING_STATE_UNKNOWN,
+ DRIVING_STATE_PARKED,
+ DRIVING_STATE_IDLING,
+ DRIVING_STATE_MOVING,
};
public static final Parcelable.Creator<CarUxRestrictionsConfiguration> CREATOR =
@@ -395,7 +534,12 @@
for (int drivingState : DRIVING_STATES) {
List<RestrictionsPerSpeedRange> restrictions = new ArrayList<>();
in.readTypedList(restrictions, RestrictionsPerSpeedRange.CREATOR);
- mUxRestrictions.put(drivingState, restrictions);
+ mBaselineUxRestrictions.put(drivingState, restrictions);
+ }
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = new ArrayList<>();
+ in.readTypedList(restrictions, RestrictionsPerSpeedRange.CREATOR);
+ mPassengerUxRestrictions.put(drivingState, restrictions);
}
mMaxContentDepth = in.readInt();
mMaxCumulativeContentItems = in.readInt();
@@ -405,7 +549,10 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
for (int drivingState : DRIVING_STATES) {
- dest.writeTypedList(mUxRestrictions.get(drivingState), 0);
+ dest.writeTypedList(mBaselineUxRestrictions.get(drivingState));
+ }
+ for (int drivingState : DRIVING_STATES) {
+ dest.writeTypedList(mPassengerUxRestrictions.get(drivingState));
}
dest.writeInt(mMaxContentDepth);
dest.writeInt(mMaxCumulativeContentItems);
@@ -423,26 +570,31 @@
private int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN;
private int mMaxStringLength = UX_RESTRICTIONS_UNKNOWN;
- private Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+ public Map<Integer, List<RestrictionsPerSpeedRange>> mPassengerUxRestrictions =
+ new ArrayMap<>(DRIVING_STATES.length);
+ public Map<Integer, List<RestrictionsPerSpeedRange>> mBaselineUxRestrictions =
new ArrayMap<>(DRIVING_STATES.length);
public Builder() {
for (int drivingState : DRIVING_STATES) {
- mUxRestrictions.put(drivingState, new ArrayList<>());
+ mBaselineUxRestrictions.put(drivingState, new ArrayList<>());
+ mPassengerUxRestrictions.put(drivingState, new ArrayList<>());
}
}
/**
- * Sets ux restrictions for driving state.
+ * Sets UX restrictions for driving state.
*/
public Builder setUxRestrictions(@CarDrivingState int drivingState,
boolean requiresOptimization,
@CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
- return this.setUxRestrictions(drivingState, null, requiresOptimization, restrictions);
+ return this.setUxRestrictions(drivingState, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(requiresOptimization)
+ .setRestrictions(restrictions));
}
/**
- * Sets ux restrictions with speed range.
+ * Sets UX restrictions with speed range.
*
* @param drivingState Restrictions will be set for this Driving state.
* See constants in {@link CarDrivingStateEvent}.
@@ -453,26 +605,57 @@
* @param requiresOptimization Whether distraction optimization (DO) is required for this
* driving state.
* @param restrictions See constants in {@link CarUxRestrictions}.
+ *
+ * @deprecated Use {@link #setUxRestrictions(int, DrivingStateRestrictions)} instead.
*/
+ @Deprecated
public Builder setUxRestrictions(@CarDrivingState int drivingState,
- SpeedRange speedRange, boolean requiresOptimization,
+ @NonNull SpeedRange speedRange, boolean requiresOptimization,
@CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
- if (drivingState != CarDrivingStateEvent.DRIVING_STATE_MOVING) {
- if (speedRange != null) {
- throw new IllegalArgumentException(
- "Non-moving driving state cannot specify speed range.");
- }
- if (mUxRestrictions.get(drivingState).size() > 0) {
- throw new IllegalArgumentException("Non-moving driving state cannot have "
- + "more than one set of restrictions.");
- }
+ return setUxRestrictions(drivingState, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(requiresOptimization)
+ .setRestrictions(restrictions)
+ .setSpeedRange(speedRange));
+ }
+
+ /**
+ * Sets UX restriction.
+ *
+ * @param drivingState Restrictions will be set for this Driving state.
+ * See constants in {@link CarDrivingStateEvent}.
+ * @param drivingStateRestrictions Restrictions to set.
+ *
+ * @return This builder object for method chaining.
+ */
+ public Builder setUxRestrictions(
+ int drivingState, DrivingStateRestrictions drivingStateRestrictions) {
+ SpeedRange speedRange = drivingStateRestrictions.mSpeedRange;
+
+ if (drivingState != DRIVING_STATE_MOVING && speedRange != null) {
+ throw new IllegalArgumentException(
+ "Non-moving driving state should not specify speed range.");
}
- mUxRestrictions.get(drivingState).add(
- new RestrictionsPerSpeedRange(requiresOptimization, restrictions, speedRange));
+ List<RestrictionsPerSpeedRange> restrictions;
+ switch (drivingStateRestrictions.mMode) {
+ case UX_RESTRICTION_MODE_BASELINE:
+ restrictions = mBaselineUxRestrictions.get(drivingState);
+ break;
+ case UX_RESTRICTION_MODE_PASSENGER:
+ restrictions = mPassengerUxRestrictions.get(drivingState);
+ break;
+ default:
+ String mode = CarUxRestrictionsManager.modeToString(
+ drivingStateRestrictions.mMode);
+ throw new IllegalArgumentException("Unrecognized restriction mode " + mode);
+ }
+ restrictions.add(new RestrictionsPerSpeedRange(
+ drivingStateRestrictions.mMode, drivingStateRestrictions.mReqOpt,
+ drivingStateRestrictions.mRestrictions, speedRange));
return this;
}
+
/**
* Sets max string length.
*/
@@ -501,9 +684,19 @@
* @return CarUxRestrictionsConfiguration based on builder configuration.
*/
public CarUxRestrictionsConfiguration build() {
- // Create default restriction for unspecified driving state.
+ // Unspecified driving state should be fully restricted to be safe.
+ addDefaultRestrictionsToBaseline();
+
+ validateBaselineModeRestrictions();
+ validatePassengerModeRestrictions();
+
+ return new CarUxRestrictionsConfiguration(this);
+ }
+
+ private void addDefaultRestrictionsToBaseline() {
for (int drivingState : DRIVING_STATES) {
- List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+ List<RestrictionsPerSpeedRange> restrictions =
+ mBaselineUxRestrictions.get(drivingState);
if (restrictions.size() == 0) {
Log.i(TAG, "Using default restrictions for driving state: "
+ getDrivingStateName(drivingState));
@@ -511,61 +704,107 @@
true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED));
}
}
+ }
- // Configuration validation.
+ private void validateBaselineModeRestrictions() {
for (int drivingState : DRIVING_STATES) {
- List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
-
- if (drivingState == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
- // Sort restrictions based on speed range.
- Collections.sort(restrictions,
- (r1, r2) -> r1.mSpeedRange.compareTo(r2.mSpeedRange));
-
- if (!isAllSpeedRangeCovered(restrictions)) {
- throw new IllegalStateException(
- "Moving state should cover full speed range.");
- }
- } else {
+ List<RestrictionsPerSpeedRange> restrictions =
+ mBaselineUxRestrictions.get(drivingState);
+ if (drivingState != DRIVING_STATE_MOVING) {
+ // Note: For non-moving state, setUxRestrictions() rejects UxRestriction with
+ // speed range, so we don't check here.
if (restrictions.size() != 1) {
- throw new IllegalStateException("Non-moving driving state should contain "
- + "one set of restriction rules.");
+ throw new IllegalStateException("Non-moving driving state should "
+ + "contain one set of restriction rules.");
}
}
+
+ // If there are multiple restrictions, each one should specify speed range.
+ if (restrictions.size() > 1 && restrictions.stream().anyMatch(
+ restriction -> restriction.mSpeedRange == null)) {
+ StringBuilder error = new StringBuilder();
+ for (RestrictionsPerSpeedRange restriction : restrictions) {
+ error.append(restriction.toString()).append('\n');
+ }
+ throw new IllegalStateException(
+ "Every restriction in MOVING state should contain driving state.\n"
+ + error.toString());
+ }
+
+ // Sort restrictions based on speed range.
+ Collections.sort(restrictions,
+ Comparator.comparing(RestrictionsPerSpeedRange::getSpeedRange));
+
+ validateRangeOfSpeed(restrictions);
+ validateContinuousSpeedRange(restrictions);
}
- return new CarUxRestrictionsConfiguration(this);
+ }
+
+ private void validatePassengerModeRestrictions() {
+ List<RestrictionsPerSpeedRange> passengerMovingRestrictions =
+ mPassengerUxRestrictions.get(DRIVING_STATE_MOVING);
+ Collections.sort(passengerMovingRestrictions,
+ Comparator.comparing(RestrictionsPerSpeedRange::getSpeedRange));
+
+ validateContinuousSpeedRange(passengerMovingRestrictions);
}
/**
- * restrictions should be sorted based on speed range.
+ * Validates if combined speed ranges of given restrictions.
+ *
+ * <p>Restrictions are considered to contain valid speed ranges if:
+ * <ul>
+ * <li>None contains a speed range - implies full range; or
+ * <li>Combination covers range [0 - MAX_SPEED]
+ * </ul>
+ *
+ * Throws exception on invalidate input.
+ *
+ * @param restrictions Restrictions to be checked. Must be sorted.
*/
- private boolean isAllSpeedRangeCovered(List<RestrictionsPerSpeedRange> restrictions) {
+ private void validateRangeOfSpeed(List<RestrictionsPerSpeedRange> restrictions) {
if (restrictions.size() == 1) {
- if (restrictions.get(0).mSpeedRange == null) {
+ SpeedRange speedRange = restrictions.get(0).mSpeedRange;
+ if (speedRange == null) {
// Single restriction with null speed range implies that
// it applies to the entire driving state.
- return true;
+ return;
}
- return restrictions.get(0).mSpeedRange.mMinSpeed == 0
- && Float.compare(restrictions.get(0).mSpeedRange.mMaxSpeed,
- SpeedRange.MAX_SPEED) == 0;
}
+ if (Float.compare(restrictions.get(0).mSpeedRange.mMinSpeed, 0) != 0) {
+ throw new IllegalStateException(
+ "Speed range min speed should start at 0.");
+ }
+ float lastMaxSpeed = restrictions.get(restrictions.size() - 1).mSpeedRange.mMaxSpeed;
+ if (Float.compare(lastMaxSpeed, SpeedRange.MAX_SPEED) != 0) {
+ throw new IllegalStateException(
+ "Max speed of last restriction should be MAX_SPEED.");
+ }
+ }
- if (restrictions.get(0).mSpeedRange.mMinSpeed != 0) {
- Log.e(TAG, "Speed range min speed should start at 0.");
- return false;
- }
+ /**
+ * Validates if combined speed ranges of given restrictions are continuous, meaning they:
+ * <ul>
+ * <li>Do not overlap; and
+ * <li>Do not contain gap
+ * </ul>
+ *
+ * <p>Namely the max speed of current range equals the min speed of next range.
+ *
+ * Throws exception on invalidate input.
+ *
+ * @param restrictions Restrictions to be checked. Must be sorted.
+ */
+ private void validateContinuousSpeedRange(List<RestrictionsPerSpeedRange> restrictions) {
for (int i = 1; i < restrictions.size(); i++) {
RestrictionsPerSpeedRange prev = restrictions.get(i - 1);
RestrictionsPerSpeedRange curr = restrictions.get(i);
// If current min != prev.max, there's either an overlap or a gap in speed range.
if (Float.compare(curr.mSpeedRange.mMinSpeed, prev.mSpeedRange.mMaxSpeed) != 0) {
- Log.e(TAG, "Mis-configured speed range. Possibly speed range overlap or gap.");
- return false;
+ throw new IllegalArgumentException(
+ "Mis-configured speed range. Possibly speed range overlap or gap.");
}
}
- // The last speed range should have max speed.
- float lastMaxSpeed = restrictions.get(restrictions.size() - 1).mSpeedRange.mMaxSpeed;
- return lastMaxSpeed == SpeedRange.MAX_SPEED;
}
/**
@@ -590,9 +829,6 @@
if (minSpeed == MAX_SPEED) {
throw new IllegalArgumentException("Min speed cannot be MAX_SPEED.");
}
- if (maxSpeed < 0) {
- throw new IllegalArgumentException("Max speed cannot be negative.");
- }
if (minSpeed > maxSpeed) {
throw new IllegalArgumentException("Min speed " + minSpeed
+ " should not be greater than max speed " + maxSpeed);
@@ -608,13 +844,7 @@
* @return {@code true} if in range; {@code false} otherwise.
*/
public boolean includes(float speed) {
- if (speed < mMinSpeed) {
- return false;
- }
- if (mMaxSpeed == MAX_SPEED) {
- return true;
- }
- return speed < mMaxSpeed;
+ return mMinSpeed <= speed && speed < mMaxSpeed;
}
@Override
@@ -640,35 +870,116 @@
return this.compareTo(other) == 0;
}
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("[min: ").append(mMinSpeed)
+ .append("; max: ").append(mMaxSpeed == MAX_SPEED ? "max_speed" : mMaxSpeed)
+ .append("]")
+ .toString();
+ }
+ }
+ }
+
+ /**
+ * UX restrictions to be applied to a driving state through {@link
+ * Builder#setUxRestrictions(int, CarUxRestrictionsConfiguration.DrivingStateRestrictions)}.
+ * These UX restrictions can also specified to be only applicable to certain speed range and
+ * restriction mode.
+ *
+ * @see UxRestrictionMode
+ * @see Builder.SpeedRange
+ *
+ * @hide
+ */
+ public static final class DrivingStateRestrictions {
+ private int mMode = UX_RESTRICTION_MODE_BASELINE;
+ private boolean mReqOpt = true;
+ private int mRestrictions = CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
+ @Nullable private Builder.SpeedRange mSpeedRange;
+
+ /**
+ * Sets whether Distraction Optimization (DO) is required. Defaults to {@code true}.
+ */
+ public DrivingStateRestrictions setDistractionOptimizationRequired(
+ boolean distractionOptimizationRequired) {
+ mReqOpt = distractionOptimizationRequired;
+ return this;
+ }
+
+ /**
+ * Sets active restrictions.
+ * Defaults to {@link CarUxRestrictions#UX_RESTRICTIONS_FULLY_RESTRICTED}.
+ */
+ public DrivingStateRestrictions setRestrictions(
+ @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+ mRestrictions = restrictions;
+ return this;
+ }
+
+ /**
+ * Sets restriction mode to apply to.
+ * Defaults to {@link CarUxRestrictionsManager#UX_RESTRICTION_MODE_BASELINE}.
+ */
+ public DrivingStateRestrictions setMode(@UxRestrictionMode int mode) {
+ mMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets speed range to apply to. Optional value. Not setting one means the restrictions
+ * apply to full speed range, namely {@code 0} to {@link Builder.SpeedRange#MAX_SPEED}.
+ */
+ public DrivingStateRestrictions setSpeedRange(@NonNull Builder.SpeedRange speedRange) {
+ mSpeedRange = speedRange;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("Mode: ").append(CarUxRestrictionsManager.modeToString(mMode))
+ .append(". Requires DO? ").append(mReqOpt)
+ .append(". Restrictions: ").append(Integer.toBinaryString(mRestrictions))
+ .append(". SpeedRange: ")
+ .append(mSpeedRange == null ? "null" : mSpeedRange.toString())
+ .toString();
}
}
/**
* Container for UX restrictions for a speed range.
* Speed range is valid only for the {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}.
- * @hide
*/
- public static final class RestrictionsPerSpeedRange implements Parcelable {
+ private static final class RestrictionsPerSpeedRange implements Parcelable {
+ @UxRestrictionMode
+ final int mMode;
final boolean mReqOpt;
final int mRestrictions;
@Nullable
final Builder.SpeedRange mSpeedRange;
- public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions) {
- this(reqOpt, restrictions, null);
+ RestrictionsPerSpeedRange(boolean reqOpt, int restrictions) {
+ this(UX_RESTRICTION_MODE_BASELINE, reqOpt, restrictions, null);
}
- public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions,
+ RestrictionsPerSpeedRange(@UxRestrictionMode int mode, boolean reqOpt, int restrictions,
@Nullable Builder.SpeedRange speedRange) {
if (!reqOpt && restrictions != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
throw new IllegalArgumentException(
"Driving optimization is not required but UX restrictions is required.");
}
+ mMode = mode;
mReqOpt = reqOpt;
mRestrictions = restrictions;
mSpeedRange = speedRange;
}
+ public Builder.SpeedRange getSpeedRange() {
+ return mSpeedRange;
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
@@ -678,12 +989,25 @@
return false;
}
RestrictionsPerSpeedRange other = (RestrictionsPerSpeedRange) obj;
- return mReqOpt == other.mReqOpt
+ return mMode == other.mMode
+ && mReqOpt == other.mReqOpt
&& mRestrictions == other.mRestrictions
&& ((mSpeedRange == null && other.mSpeedRange == null) || mSpeedRange.equals(
other.mSpeedRange));
}
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("[Mode is ").append(CarUxRestrictionsManager.modeToString(mMode))
+ .append("; Requires DO? ").append(mReqOpt)
+ .append("; Restrictions: ").append(Integer.toBinaryString(mRestrictions))
+ .append("; Speed range: ")
+ .append(mSpeedRange == null ? "null" : mSpeedRange.toString())
+ .append(']')
+ .toString();
+ }
+
// Parcelable methods/fields.
public static final Creator<RestrictionsPerSpeedRange> CREATOR =
@@ -705,6 +1029,7 @@
}
protected RestrictionsPerSpeedRange(Parcel in) {
+ mMode = in.readInt();
mReqOpt = in.readBoolean();
mRestrictions = in.readInt();
// Whether speed range is specified.
@@ -719,6 +1044,7 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mMode);
dest.writeBoolean(mReqOpt);
dest.writeInt(mRestrictions);
// Whether speed range is specified.
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 06875dd..4848e99 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -16,10 +16,10 @@
package android.car.drivingstate;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
-import android.annotation.TestApi;
import android.car.Car;
import android.car.CarManagerBase;
import android.content.Context;
@@ -30,6 +30,8 @@
import android.os.RemoteException;
import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
/**
@@ -42,6 +44,37 @@
private static final boolean VDBG = false;
private static final int MSG_HANDLE_UX_RESTRICTIONS_CHANGE = 0;
+ /**
+ * Baseline restriction mode is the default UX restrictions used for driving state.
+ *
+ * @hide
+ */
+ public static final int UX_RESTRICTION_MODE_BASELINE = 0;
+ /**
+ * Passenger restriction mode uses UX restrictions for {@link #UX_RESTRICTION_MODE_PASSENGER},
+ * set through {@link CarUxRestrictionsConfiguration.Builder.UxRestrictions#setMode(int)}.
+ *
+ * <p>If a new {@link CarUxRestrictions} is available upon mode transition, it'll be immediately
+ * dispatched to listeners.
+ *
+ * <p>If passenger mode restrictions is not configured for current driving state, it will fall
+ * back to {@link #UX_RESTRICTION_MODE_BASELINE}.
+ *
+ * <p>Caller are responsible for determining and executing the criteria for entering and exiting
+ * this mode. Exiting by setting mode to {@link #UX_RESTRICTION_MODE_BASELINE}.
+ *
+ * @hide
+ */
+ public static final int UX_RESTRICTION_MODE_PASSENGER = 1;
+
+ /** @hide */
+ @IntDef(prefix = { "UX_RESTRICTION_MODE_" }, value = {
+ UX_RESTRICTION_MODE_BASELINE,
+ UX_RESTRICTION_MODE_PASSENGER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UxRestrictionMode {}
+
private final Context mContext;
private final ICarUxRestrictionsManager mUxRService;
private final EventCallbackHandler mEventCallbackHandler;
@@ -112,30 +145,6 @@
}
/**
- * Set a new {@link CarUxRestrictionsConfiguration} for next trip.
- * <p>
- * Saving a new configuration does not affect current configuration. The new configuration will
- * only be used after UX Restrictions service restarts when the vehicle is parked.
- * <p>
- * Requires Permission:
- * {@link android.car.Manifest.permission#CAR_UX_RESTRICTIONS_CONFIGURATION}.
- *
- * @param config UX restrictions configuration to be persisted.
- * @return {@code true} if input config was successfully saved; {@code false} otherwise.
- *
- * @hide
- */
- @RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
- public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
- CarUxRestrictionsConfiguration config) {
- try {
- return mUxRService.saveUxRestrictionsConfigurationForNextBoot(config);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Unregister the registered {@link OnUxRestrictionsChangedListener}
*/
public synchronized void unregisterListener() {
@@ -168,6 +177,59 @@
}
/**
+ * Sets restriction mode. Returns {@code true} if the operation succeeds.
+ *
+ * @hide
+ */
+ @RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
+ public boolean setRestrictionMode(@UxRestrictionMode int mode) {
+ try {
+ return mUxRService.setRestrictionMode(mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current restriction mode.
+ *
+ * @hide
+ */
+ @RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
+ @UxRestrictionMode
+ public int getRestrictionMode() {
+ try {
+ return mUxRService.getRestrictionMode();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set a new {@link CarUxRestrictionsConfiguration} for next trip.
+ * <p>
+ * Saving a new configuration does not affect current configuration. The new configuration will
+ * only be used after UX Restrictions service restarts when the vehicle is parked.
+ * <p>
+ * Requires Permission:
+ * {@link android.car.Manifest.permission#CAR_UX_RESTRICTIONS_CONFIGURATION}.
+ *
+ * @param config UX restrictions configuration to be persisted.
+ * @return {@code true} if input config was successfully saved; {@code false} otherwise.
+ *
+ * @hide
+ */
+ @RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
+ public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
+ CarUxRestrictionsConfiguration config) {
+ try {
+ return mUxRService.saveUxRestrictionsConfigurationForNextBoot(config);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Get the current staged configuration, staged config file will only be accessible after
* the boot up completed or user has been switched.
* This methods is only for test purpose, please do not use in production.
@@ -175,9 +237,7 @@
* @return current staged configuration, {@code null} if it's not available
*
* @hide
- *
*/
- @TestApi
@Nullable
@RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
public synchronized CarUxRestrictionsConfiguration getStagedConfig() {
@@ -194,9 +254,7 @@
* @return current prod configuration that is in effect.
*
* @hide
- *
*/
- @TestApi
@RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
public synchronized CarUxRestrictionsConfiguration getConfig() {
try {
@@ -207,6 +265,20 @@
}
/**
+ * @hide
+ */
+ public static String modeToString(@UxRestrictionMode int mode) {
+ switch (mode) {
+ case UX_RESTRICTION_MODE_BASELINE:
+ return "baseline";
+ case UX_RESTRICTION_MODE_PASSENGER:
+ return "passenger";
+ default:
+ throw new IllegalArgumentException("Unrecognized restriction mode " + mode);
+ }
+ }
+
+ /**
* Class that implements the listener interface and gets called back from the
* {@link com.android.car.CarDrivingStateService} across the binder interface.
*/
diff --git a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
index 8f48c5e..76d5a3b 100644
--- a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
+++ b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
@@ -34,4 +34,6 @@
boolean saveUxRestrictionsConfigurationForNextBoot(in CarUxRestrictionsConfiguration config) = 3;
CarUxRestrictionsConfiguration getStagedConfig() = 4;
CarUxRestrictionsConfiguration getConfig() = 5;
+ boolean setRestrictionMode(int mode) = 6;
+ int getRestrictionMode() = 7;
}
diff --git a/car-lib/src/android/car/storagemonitoring/IoStats.java b/car-lib/src/android/car/storagemonitoring/IoStats.java
index 3fc03c6..0c2b662 100644
--- a/car-lib/src/android/car/storagemonitoring/IoStats.java
+++ b/car-lib/src/android/car/storagemonitoring/IoStats.java
@@ -35,7 +35,7 @@
* @hide
*/
@SystemApi
-public class IoStats implements Parcelable {
+public final class IoStats implements Parcelable {
public static final Creator<IoStats> CREATOR = new Creator<IoStats>() {
@Override
public IoStats createFromParcel(Parcel in) {
diff --git a/car-lib/src/android/car/storagemonitoring/LifetimeWriteInfo.java b/car-lib/src/android/car/storagemonitoring/LifetimeWriteInfo.java
index 6313e46..01dc933 100644
--- a/car-lib/src/android/car/storagemonitoring/LifetimeWriteInfo.java
+++ b/car-lib/src/android/car/storagemonitoring/LifetimeWriteInfo.java
@@ -30,7 +30,7 @@
* @hide
*/
@SystemApi
-public class LifetimeWriteInfo implements Parcelable {
+public final class LifetimeWriteInfo implements Parcelable {
public static final Creator<IoStats> CREATOR = new Creator<IoStats>() {
@Override
public IoStats createFromParcel(Parcel in) {
diff --git a/car-lib/src/android/car/storagemonitoring/WearEstimate.java b/car-lib/src/android/car/storagemonitoring/WearEstimate.java
index 507e6ed..10ae8a1 100644
--- a/car-lib/src/android/car/storagemonitoring/WearEstimate.java
+++ b/car-lib/src/android/car/storagemonitoring/WearEstimate.java
@@ -38,7 +38,7 @@
* @hide
*/
@SystemApi
-public class WearEstimate implements Parcelable {
+public final class WearEstimate implements Parcelable {
public static final int UNKNOWN = -1;
/** @hide */
diff --git a/car-lib/src/android/car/storagemonitoring/WearEstimateChange.java b/car-lib/src/android/car/storagemonitoring/WearEstimateChange.java
index 5c80a31..2d454b4 100644
--- a/car-lib/src/android/car/storagemonitoring/WearEstimateChange.java
+++ b/car-lib/src/android/car/storagemonitoring/WearEstimateChange.java
@@ -33,7 +33,7 @@
* @hide
*/
@SystemApi
-public class WearEstimateChange implements Parcelable {
+public final class WearEstimateChange implements Parcelable {
public static final Parcelable.Creator<WearEstimateChange> CREATOR =
new Parcelable.Creator<WearEstimateChange>() {
public WearEstimateChange createFromParcel(Parcel in) {
diff --git a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
index e169a17..8b01d87 100644
--- a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
+++ b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
@@ -147,13 +147,15 @@
}
/**
- * Confirms that the enrollment handshake has been accepted by the user. This should be called
+ * Confirms that the enrollment handshake has been accepted by the user. This should be called
* after the user has confirmed the verification code displayed on the UI.
+ *
+ * @param device the remote Bluetooth device that will receive the signal.
*/
@RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
- public void enrollmentHandshakeAccepted() {
+ public void enrollmentHandshakeAccepted(BluetoothDevice device) {
try {
- mEnrollmentService.enrollmentHandshakeAccepted();
+ mEnrollmentService.enrollmentHandshakeAccepted(device);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -200,7 +202,11 @@
*/
@RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void removeEscrowToken(long handle, int uid) {
- //TODO:(b/128857992) need to call method in CarTrustAgentEnrollmentService
+ try {
+ mEnrollmentService.removeEscrowToken(handle, uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
diff --git a/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl b/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
index 5606c06..bcdf1c9 100644
--- a/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
+++ b/car-lib/src/android/car/trust/ICarTrustAgentEnrollment.aidl
@@ -30,10 +30,10 @@
void startEnrollmentAdvertising();
void stopEnrollmentAdvertising();
void initiateEnrollmentHandshake(in BluetoothDevice device);
- void enrollmentHandshakeAccepted();
+ void enrollmentHandshakeAccepted(in BluetoothDevice device);
void terminateEnrollmentHandshake();
boolean isEscrowTokenActive(in long handle, int uid);
- void revokeTrust(in long handle);
+ void removeEscrowToken(in long handle, int uid);
long[] getEnrollmentHandlesForUser(in int uid);
void registerEnrollmentCallback(in ICarTrustAgentEnrollmentCallback callback);
void unregisterEnrollmentCallback(in ICarTrustAgentEnrollmentCallback callback);
diff --git a/car-lib/src/android/car/vms/VmsAssociatedLayer.java b/car-lib/src/android/car/vms/VmsAssociatedLayer.java
index d3f0e18..e522faa 100644
--- a/car-lib/src/android/car/vms/VmsAssociatedLayer.java
+++ b/car-lib/src/android/car/vms/VmsAssociatedLayer.java
@@ -16,33 +16,48 @@
package android.car.vms;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.util.Preconditions;
+
import java.util.*;
/**
- * A VMS Layer with a list of publisher IDs it is associated with.
+ * A Vehicle Map Service layer with a list of publisher IDs it is associated with.
*
* @hide
*/
+@SystemApi
public final class VmsAssociatedLayer implements Parcelable {
-
- // The VmsLayer.
private final VmsLayer mLayer;
-
- // The IDs of the publishers that can publish this VmsLayer.
private final Set<Integer> mPublisherIds;
- public VmsAssociatedLayer(VmsLayer layer, Set<Integer> publisherIds) {
- mLayer = layer;
+ /**
+ * Constructs a new layer offering.
+ *
+ * @param layer layer being offered
+ * @param publisherIds IDs of publishers associated with the layer
+ */
+ public VmsAssociatedLayer(@NonNull VmsLayer layer, @NonNull Set<Integer> publisherIds) {
+ mLayer = Preconditions.checkNotNull(layer, "layer cannot be null");
mPublisherIds = Collections.unmodifiableSet(publisherIds);
}
+ /**
+ * @return layer being offered
+ */
+ @NonNull
public VmsLayer getVmsLayer() {
return mLayer;
}
+ /**
+ * @return IDs of publishers associated with the layer
+ */
+ @NonNull
public Set<Integer> getPublisherIds() {
return mPublisherIds;
}
@@ -52,7 +67,6 @@
return "VmsAssociatedLayer{ VmsLayer: " + mLayer + ", Publishers: " + mPublisherIds + "}";
}
- // Parcelable related methods.
public static final Parcelable.Creator<VmsAssociatedLayer> CREATOR =
new Parcelable.Creator<VmsAssociatedLayer>() {
public VmsAssociatedLayer createFromParcel(Parcel in) {
diff --git a/car-lib/src/android/car/vms/VmsAvailableLayers.java b/car-lib/src/android/car/vms/VmsAvailableLayers.java
index b0324f6..3852a7f 100644
--- a/car-lib/src/android/car/vms/VmsAvailableLayers.java
+++ b/car-lib/src/android/car/vms/VmsAvailableLayers.java
@@ -16,21 +16,28 @@
package android.car.vms;
+import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-
-import java.util.Set;
import java.util.ArrayList;
-import java.util.List;
-import java.util.HashSet;
import java.util.Collections;
-
+import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
- * VMS Layers that can be subscribed to by VMS clients.
+ * Availability of Vehicle Map Service layers.
+ *
+ * The layer availability is used by subscribers to determine which {@link VmsLayer}s are available
+ * for subscription and which publishers are offering to publish data for those layers. However,
+ * the Vehicle Map Service will allow subscription requests for unavailable layers.
+ *
+ * Sequence numbers are used to indicate the succession of availability states, and increase
+ * monotonically with each change in layer availability. They must be used by clients to ignore
+ * states that are received out-of-order.
*
* @hide
*/
@@ -43,16 +50,28 @@
// The list of AssociatedLayers
private final Set<VmsAssociatedLayer> mAssociatedLayers;
- public VmsAvailableLayers(Set<VmsAssociatedLayer> associatedLayers, int sequence) {
+ /**
+ * Constructs a new layer availability.
+ *
+ * @param associatedLayers set of layers available for subscription
+ * @param sequence sequence number of the availability state
+ */
+ public VmsAvailableLayers(@NonNull Set<VmsAssociatedLayer> associatedLayers, int sequence) {
mSeq = sequence;
-
mAssociatedLayers = Collections.unmodifiableSet(associatedLayers);
}
+ /**
+ * @return sequence number of the availability state
+ */
public int getSequence() {
return mSeq;
}
+ /**
+ * @return set of layers available for subscription
+ */
+ @NonNull
public Set<VmsAssociatedLayer> getAssociatedLayers() {
return mAssociatedLayers;
}
diff --git a/car-lib/src/android/car/vms/VmsLayer.java b/car-lib/src/android/car/vms/VmsLayer.java
index b921230..fb811fa 100644
--- a/car-lib/src/android/car/vms/VmsLayer.java
+++ b/car-lib/src/android/car/vms/VmsLayer.java
@@ -23,62 +23,73 @@
import java.util.Objects;
/**
- * A VMS Layer which can be subscribed to by VMS clients.
+ * A Vehicle Map Service layer, which can be offered or subscribed to by clients.
+ *
+ * The full layer definition is used when routing packets, with each layer having the following
+ * properties:
+ *
+ * <ul>
+ * <li>Type: Type of data being published.</li>
+ * <li>Subtype: Type of packet being published.</li>
+ * <li>Version: Major version of the packet format. Different versions are not guaranteed to be
+ * compatible.</li>
+ * </ul>
+ *
+ * See the Vehicle Maps Service partner documentation for the set of valid types and subtypes.
*
* @hide
*/
@SystemApi
public final class VmsLayer implements Parcelable {
-
- // The layer Type.
private int mType;
-
- // The layer Subtype.
private int mSubtype;
-
- // The layer version.
private int mVersion;
+ /**
+ * Constructs a new layer definition.
+ *
+ * @param type type of data published on the layer
+ * @param subtype type of packet published on the layer
+ * @param version major version of layer packet format
+ */
public VmsLayer(int type, int subtype, int version) {
mType = type;
mSubtype = subtype;
mVersion = version;
}
+ /**
+ * @return type of data published on the layer
+ */
public int getType() {
return mType;
}
+ /**
+ * @return type of packet published on the layer
+ */
public int getSubtype() {
return mSubtype;
}
+ /**
+ * @return major version of layer packet format
+ */
public int getVersion() {
return mVersion;
}
- /**
- * Checks the two objects for equality by comparing their IDs and Versions.
- *
- * @param o the {@link VmsLayer} to which this one is to be checked for equality
- * @return true if the underlying objects of the VmsLayer are both considered equal
- */
@Override
public boolean equals(Object o) {
if (!(o instanceof VmsLayer)) {
return false;
}
VmsLayer p = (VmsLayer) o;
- return Objects.equals(p.mType, mType) &&
- Objects.equals(p.mSubtype, mSubtype) &&
- Objects.equals(p.mVersion, mVersion);
+ return Objects.equals(p.mType, mType)
+ && Objects.equals(p.mSubtype, mSubtype)
+ && Objects.equals(p.mVersion, mVersion);
}
- /**
- * Compute a hash code similarly tp {@link android.util.Pair}
- *
- * @return a hashcode of the Pair
- */
@Override
public int hashCode() {
return Objects.hash(mType, mSubtype, mVersion);
@@ -86,11 +97,10 @@
@Override
public String toString() {
- return "VmsLayer{ Type: " + mType + ", Sub type: " + mSubtype + ", Version: " + mVersion + "}";
+ return "VmsLayer{ Type: " + mType + ", Sub type: " + mSubtype + ", Version: " + mVersion
+ + "}";
}
-
- // Parcelable related methods.
public static final Parcelable.Creator<VmsLayer> CREATOR = new
Parcelable.Creator<VmsLayer>() {
public VmsLayer createFromParcel(Parcel in) {
diff --git a/car-lib/src/android/car/vms/VmsLayerDependency.java b/car-lib/src/android/car/vms/VmsLayerDependency.java
index f5b6007..8cf8722 100644
--- a/car-lib/src/android/car/vms/VmsLayerDependency.java
+++ b/car-lib/src/android/car/vms/VmsLayerDependency.java
@@ -16,12 +16,14 @@
package android.car.vms;
+import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.util.Preconditions;
+
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -29,7 +31,10 @@
import java.util.Set;
/**
- * A dependency for a VMS layer on other VMS layers.
+ * Layer dependencies for single Vehicle Map Service layer.
+ *
+ * Dependencies are treated as <b>hard</b> dependencies, meaning that an offered layer will not be
+ * reported as available until all dependent layers are also available.
*
* @hide
*/
@@ -39,28 +44,37 @@
private final Set<VmsLayer> mDependency;
/**
- * Construct a dependency for layer on other layers.
+ * Constructs a layer with a dependency on other layers.
+ *
+ * @param layer layer that has dependencies
+ * @param dependencies layers that the given layer depends on
*/
- public VmsLayerDependency(VmsLayer layer, Set<VmsLayer> dependencies) {
- mLayer = layer;
+ public VmsLayerDependency(@NonNull VmsLayer layer, @NonNull Set<VmsLayer> dependencies) {
+ mLayer = Preconditions.checkNotNull(layer, "layer cannot be null");
mDependency = Collections.unmodifiableSet(dependencies);
}
/**
- * Constructs a layer without a dependency.
+ * Constructs a layer without dependencies.
+ *
+ * @param layer layer that has no dependencies
*/
- public VmsLayerDependency(VmsLayer layer) {
- mLayer = layer;
- mDependency = Collections.emptySet();
+ public VmsLayerDependency(@NonNull VmsLayer layer) {
+ this(layer, Collections.emptySet());
}
+ /**
+ * @return layer that has zero or more dependencies
+ */
+ @NonNull
public VmsLayer getLayer() {
return mLayer;
}
/**
- * Returns the dependencies.
+ * @return all layers that the layer depends on
*/
+ @NonNull
public Set<VmsLayer> getDependencies() {
return mDependency;
}
@@ -76,6 +90,7 @@
}
};
+ @Override
public String toString() {
return "VmsLayerDependency{ Layer: " + mLayer + " Dependency: " + mDependency + "}";
}
diff --git a/car-lib/src/android/car/vms/VmsLayersOffering.java b/car-lib/src/android/car/vms/VmsLayersOffering.java
index f2f4b73..9a79cdb 100644
--- a/car-lib/src/android/car/vms/VmsLayersOffering.java
+++ b/car-lib/src/android/car/vms/VmsLayersOffering.java
@@ -16,6 +16,7 @@
package android.car.vms;
+import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -28,7 +29,11 @@
import java.util.Set;
/**
- * The state of dependencies for a single publisher.
+ * A Vehicle Map Service layer offering for a single publisher.
+ *
+ * Contains all layers the publisher can offer, and the layers that offering depends on.
+ *
+ * A layer will not be advertised to subscribers unless all of its dependencies are met.
*
* @hide
*/
@@ -39,18 +44,26 @@
private final int mPublisherId;
- public VmsLayersOffering(Set<VmsLayerDependency> dependencies, int publisherId) {
+ /**
+ *
+ * @param dependencies
+ * @param publisherId
+ */
+ public VmsLayersOffering(@NonNull Set<VmsLayerDependency> dependencies, int publisherId) {
mDependencies = Collections.unmodifiableSet(dependencies);
mPublisherId = publisherId;
}
/**
- * Returns the dependencies.
+ * @return set of layer and dependencies in the offering
*/
public Set<VmsLayerDependency> getDependencies() {
return mDependencies;
}
+ /**
+ * @return ID of the publisher making the offering
+ */
public int getPublisherId() {
return mPublisherId;
}
@@ -77,8 +90,7 @@
@Override
public void writeToParcel(Parcel out, int flags) {
-
- out.writeParcelableList(new ArrayList(mDependencies), flags);
+ out.writeParcelableList(new ArrayList<>(mDependencies), flags);
out.writeInt(mPublisherId);
}
@@ -88,7 +100,8 @@
return false;
}
VmsLayersOffering p = (VmsLayersOffering) o;
- return Objects.equals(p.mPublisherId, mPublisherId) && p.mDependencies.equals(mDependencies);
+ return Objects.equals(p.mPublisherId, mPublisherId)
+ && p.mDependencies.equals(mDependencies);
}
@Override
diff --git a/car-lib/src/android/car/vms/VmsPublisherClientService.java b/car-lib/src/android/car/vms/VmsPublisherClientService.java
index bdd2510..b89c558 100644
--- a/car-lib/src/android/car/vms/VmsPublisherClientService.java
+++ b/car-lib/src/android/car/vms/VmsPublisherClientService.java
@@ -17,7 +17,7 @@
package android.car.vms;
-import android.annotation.Nullable;
+import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
@@ -29,27 +29,29 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
import java.lang.ref.WeakReference;
/**
- * Services that need VMS publisher services need to inherit from this class and also need to be
- * declared in the array vmsPublisherClients located in
- * packages/services/Car/service/res/values/config.xml (most likely, this file will be in an overlay
- * of the target product.
+ * API implementation of a Vehicle Map Service publisher client.
*
- * The {@link com.android.car.VmsPublisherService} will start this service. The callback
- * {@link #onVmsPublisherServiceReady()} notifies when VMS publisher services can be used, and the
- * publisher can request a publisher ID in order to start publishing.
+ * All publisher clients must inherit from this class and export it as a service, and the service
+ * be added to either the {@code vmsPublisherSystemClients} or {@code vmsPublisherUserClients}
+ * arrays in the Car service configuration, depending on which user the client will run as.
*
- * SystemApi candidate.
+ * The {@link com.android.car.VmsPublisherService} will then bind to this service, with the
+ * {@link #onVmsPublisherServiceReady()} callback notifying the client implementation when the
+ * connection is established and publisher operations can be called.
+ *
+ * Publishers must also register a publisher ID by calling {@link #getPublisherId(byte[])}.
*
* @hide
*/
@SystemApi
public abstract class VmsPublisherClientService extends Service {
private static final boolean DBG = true;
- private static final String TAG = "VmsPublisherClient";
+ private static final String TAG = "VmsPublisherClientService";
private final Object mLock = new Object();
@@ -83,27 +85,29 @@
}
/**
- * Notifies that the publisher services are ready.
+ * Notifies the client that publisher services are ready.
*/
protected abstract void onVmsPublisherServiceReady();
/**
- * Publishers need to implement this method to receive notifications of subscription changes.
+ * Notifies the client of changes in layer subscriptions.
*
- * @param subscriptionState the state of the subscriptions.
+ * @param subscriptionState state of layer subscriptions
*/
- public abstract void onVmsSubscriptionChange(VmsSubscriptionState subscriptionState);
+ public abstract void onVmsSubscriptionChange(@NonNull VmsSubscriptionState subscriptionState);
/**
- * Uses the VmsPublisherService binder to publish messages.
+ * Publishes a data packet to subscribers.
*
- * @param layer the layer to publish to.
- * @param payload the message to be sent.
- * @param publisherId the ID that got assigned to the publisher that published the message by
- * VMS core.
- * @return if the call to the method VmsPublisherService.publish was successful.
+ * Publishers must only publish packets for the layers that they have made offerings for.
+ *
+ * @param layer layer to publish to
+ * @param publisherId ID of the publisher publishing the message
+ * @param payload data packet to be sent
+ * @throws IllegalStateException if publisher services are not available
*/
- public final void publish(VmsLayer layer, int publisherId, byte[] payload) {
+ public final void publish(@NonNull VmsLayer layer, int publisherId, byte[] payload) {
+ Preconditions.checkNotNull(layer, "layer cannot be null");
if (DBG) {
Log.d(TAG, "Publishing for layer : " + layer);
}
@@ -118,12 +122,13 @@
}
/**
- * Uses the VmsPublisherService binder to set the layers offering.
+ * Sets the layers offered by a specific publisher.
*
- * @param offering the layers that the publisher may publish.
- * @return if the call to VmsPublisherService.setLayersOffering was successful.
+ * @param offering layers being offered for subscription by the publisher
+ * @throws IllegalStateException if publisher services are not available
*/
- public final void setLayersOffering(VmsLayersOffering offering) {
+ public final void setLayersOffering(@NonNull VmsLayersOffering offering) {
+ Preconditions.checkNotNull(offering, "offering cannot be null");
if (DBG) {
Log.d(TAG, "Setting layers offering : " + offering);
}
@@ -153,31 +158,38 @@
return token;
}
+ /**
+ * Acquires a publisher ID for a serialized publisher description.
+ *
+ * Multiple calls to this method with the same information will return the same publisher ID.
+ *
+ * @param publisherInfo serialized publisher description information, in a vendor-specific
+ * format
+ * @return a publisher ID for the given publisher description
+ * @throws IllegalStateException if publisher services are not available
+ */
public final int getPublisherId(byte[] publisherInfo) {
if (mVmsPublisherService == null) {
throw new IllegalStateException("VmsPublisherService not set.");
}
- Integer publisherId = null;
+ int publisherId;
try {
Log.i(TAG, "Getting publisher static ID");
publisherId = mVmsPublisherService.getPublisherId(publisherInfo);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- if (publisherId == null) {
- throw new IllegalStateException("VmsPublisherService cannot get a publisher static ID.");
- } else {
- VmsOperationRecorder.get().getPublisherId(publisherId);
- }
+ VmsOperationRecorder.get().getPublisherId(publisherId);
return publisherId;
}
/**
- * Uses the VmsPublisherService binder to get the state of the subscriptions.
+ * Gets the state of layer subscriptions.
*
- * @return list of layer/version or null in case of error.
+ * @return state of layer subscriptions
+ * @throws IllegalStateException if publisher services are not available
*/
- public final @Nullable VmsSubscriptionState getSubscriptions() {
+ public final VmsSubscriptionState getSubscriptions() {
if (mVmsPublisherService == null) {
throw new IllegalStateException("VmsPublisherService not set.");
}
@@ -202,7 +214,7 @@
private long mSequence = -1;
private final Object mSequenceLock = new Object();
- public VmsPublisherClientBinder(VmsPublisherClientService vmsPublisherClientService) {
+ VmsPublisherClientBinder(VmsPublisherClientService vmsPublisherClientService) {
mVmsPublisherClientService = new WeakReference<>(vmsPublisherClientService);
}
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index 8ca0878..06a06ae 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -31,10 +31,10 @@
import java.util.concurrent.Executor;
/**
- * API for interfacing with the VmsSubscriberService. It supports a single client callback that can
- * (un)subscribe to different layers. Getting notifactions and managing subscriptions is enabled
- * after setting the client callback with #setVmsSubscriberClientCallback.
- * SystemApi candidate
+ * API implementation for use by Vehicle Map Service subscribers.
+ *
+ * Supports a single client callback that can subscribe and unsubscribe to different data layers.
+ * {@link #setVmsSubscriberClientCallback} must be called before any subscription operations.
*
* @hide
*/
@@ -52,22 +52,28 @@
private Executor mExecutor;
/**
- * Interface exposed to VMS subscribers: it is a wrapper of IVmsSubscriberClient.
+ * Callback interface for Vehicle Map Service subscribers.
*/
public interface VmsSubscriberClientCallback {
/**
- * Called when the property is updated
+ * Called when a data packet is received.
+ *
+ * @param layer subscribed layer that packet was received for
+ * @param payload data packet that was received
*/
- void onVmsMessageReceived(VmsLayer layer, byte[] payload);
+ void onVmsMessageReceived(@NonNull VmsLayer layer, byte[] payload);
/**
- * Called when layers availability change
+ * Called when set of available data layers changes.
+ *
+ * @param availableLayers set of available data layers
*/
- void onLayersAvailabilityChanged(VmsAvailableLayers availableLayers);
+ void onLayersAvailabilityChanged(@NonNull VmsAvailableLayers availableLayers);
}
/**
* Hidden constructor - can only be used internally.
+ *
* @hide
*/
public VmsSubscriberManager(IBinder service) {
@@ -86,7 +92,9 @@
return;
}
Binder.clearCallingIdentity();
- executor.execute(() -> {dispatchOnReceiveMessage(layer, payload);});
+ executor.execute(() -> {
+ dispatchOnReceiveMessage(layer, payload);
+ });
}
@Override
@@ -102,28 +110,30 @@
return;
}
Binder.clearCallingIdentity();
- executor.execute(() -> {dispatchOnAvailabilityChangeMessage(availableLayers);});
+ executor.execute(() -> {
+ dispatchOnAvailabilityChangeMessage(availableLayers);
+ });
}
};
}
/**
- * Sets the callback for the notification of onVmsMessageReceived events.
- * @param executor {@link Executor} to handle the callbacks
- * @param clientCallback subscriber callback that will handle onVmsMessageReceived events.
- * @throws IllegalStateException if the client callback was already set.
+ * Sets the subscriber client's callback, for receiving layer availability and data events.
+ *
+ * @param executor {@link Executor} to handle the callbacks
+ * @param clientCallback subscriber callback that will handle events
+ * @throws IllegalStateException if the client callback was already set
*/
public void setVmsSubscriberClientCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull VmsSubscriberClientCallback clientCallback) {
- Preconditions.checkNotNull(clientCallback);
- Preconditions.checkNotNull(executor);
synchronized (mClientCallbackLock) {
if (mClientCallback != null) {
throw new IllegalStateException("Client callback is already configured.");
}
- mClientCallback = clientCallback;
- mExecutor = executor;
+ mClientCallback = Preconditions.checkNotNull(clientCallback,
+ "clientCallback cannot be null");
+ mExecutor = Preconditions.checkNotNull(executor, "executor cannot be null");
}
try {
mVmsSubscriberService.addVmsSubscriberToNotifications(mSubscriberManagerClient);
@@ -134,7 +144,7 @@
/**
- * Clears the client callback which disables communication with the client.
+ * Clears the subscriber client's callback.
*/
public void clearVmsSubscriberClientCallback() {
synchronized (mClientCallbackLock) {
@@ -153,8 +163,12 @@
}
/**
- * Returns a serialized publisher information for a publisher ID.
+ * Gets a publisher's self-reported description information.
+ *
+ * @param publisherId publisher ID to retrieve information for
+ * @return serialized publisher information, in a vendor-specific format
*/
+ @NonNull
public byte[] getPublisherInfo(int publisherId) {
try {
return mVmsSubscriberService.getPublisherInfo(publisherId);
@@ -164,8 +178,11 @@
}
/**
- * Returns the available layers.
+ * Gets all layers available for subscription.
+ *
+ * @return available layers
*/
+ @NonNull
public VmsAvailableLayers getAvailableLayers() {
try {
return mVmsSubscriberService.getAvailableLayers();
@@ -175,13 +192,13 @@
}
/**
- * Subscribes to listen to the layer specified.
+ * Subscribes to data packets for a specific layer.
*
- * @param layer the layer to subscribe to.
+ * @param layer layer to subscribe to
* @throws IllegalStateException if the client callback was not set via
* {@link #setVmsSubscriberClientCallback}.
*/
- public void subscribe(VmsLayer layer) {
+ public void subscribe(@NonNull VmsLayer layer) {
verifySubscriptionIsAllowed();
try {
mVmsSubscriberService.addVmsSubscriber(mSubscriberManagerClient, layer);
@@ -192,14 +209,14 @@
}
/**
- * Subscribes to listen to the layer specified from the publisher specified.
+ * Subscribes to data packets for a specific layer from a specific publisher.
*
- * @param layer the layer to subscribe to.
- * @param publisherId the publisher of the layer.
+ * @param layer layer to subscribe to
+ * @param publisherId a publisher of the layer
* @throws IllegalStateException if the client callback was not set via
* {@link #setVmsSubscriberClientCallback}.
*/
- public void subscribe(VmsLayer layer, int publisherId) {
+ public void subscribe(@NonNull VmsLayer layer, int publisherId) {
verifySubscriptionIsAllowed();
try {
mVmsSubscriberService.addVmsSubscriberToPublisher(
@@ -210,7 +227,9 @@
}
}
- /** Starts monitoring. */
+ /**
+ * Start monitoring all messages for all layers, regardless of subscriptions.
+ */
public void startMonitoring() {
verifySubscriptionIsAllowed();
try {
@@ -222,13 +241,13 @@
}
/**
- * Unsubscribes from the layer/version specified.
+ * Unsubscribes from data packets for a specific layer.
*
- * @param layer the layer to unsubscribe from.
+ * @param layer layer to unsubscribe from
* @throws IllegalStateException if the client callback was not set via
* {@link #setVmsSubscriberClientCallback}.
*/
- public void unsubscribe(VmsLayer layer) {
+ public void unsubscribe(@NonNull VmsLayer layer) {
verifySubscriptionIsAllowed();
try {
mVmsSubscriberService.removeVmsSubscriber(mSubscriberManagerClient, layer);
@@ -239,14 +258,14 @@
}
/**
- * Unsubscribes from the layer/version specified.
+ * Unsubscribes from data packets for a specific layer from a specific publisher.
*
- * @param layer the layer to unsubscribe from.
- * @param publisherId the pubisher of the layer.
+ * @param layer layer to unsubscribe from
+ * @param publisherId a publisher of the layer
* @throws IllegalStateException if the client callback was not set via
* {@link #setVmsSubscriberClientCallback}.
*/
- public void unsubscribe(VmsLayer layer, int publisherId) {
+ public void unsubscribe(@NonNull VmsLayer layer, int publisherId) {
try {
mVmsSubscriberService.removeVmsSubscriberToPublisher(
mSubscriberManagerClient, layer, publisherId);
@@ -256,6 +275,9 @@
}
}
+ /**
+ * Stop monitoring. Only receive messages for layers which have been subscribed to."
+ */
public void stopMonitoring() {
try {
mVmsSubscriberService.removeVmsSubscriberPassive(mSubscriberManagerClient);
@@ -310,22 +332,4 @@
@Override
public void onCarDisconnected() {
}
-
- private static final class VmsDataMessage {
- private final VmsLayer mLayer;
- private final byte[] mPayload;
-
- public VmsDataMessage(VmsLayer layer, byte[] payload) {
- mLayer = layer;
- mPayload = payload;
- }
-
- public VmsLayer getLayer() {
- return mLayer;
- }
-
- public byte[] getPayload() {
- return mPayload;
- }
- }
}
diff --git a/car-lib/src/android/car/vms/VmsSubscriptionState.java b/car-lib/src/android/car/vms/VmsSubscriptionState.java
index 7b8ce81..08e72ba 100644
--- a/car-lib/src/android/car/vms/VmsSubscriptionState.java
+++ b/car-lib/src/android/car/vms/VmsSubscriptionState.java
@@ -16,6 +16,8 @@
package android.car.vms;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -27,39 +29,54 @@
import java.util.Set;
/**
- * The list of layers with subscribers.
+ * Subscription state of Vehicle Map Service layers.
+ *
+ * The subscription state is used by publishers to determine which layers to publish data for, as
+ * any data published to a layer without subscribers will be dropped by the Vehicle Map Service.
+ *
+ * Sequence numbers are used to indicate the succession of subscription states, and increase
+ * monotonically with each change in subscriptions. They must be used by clients to ignore states
+ * that are received out-of-order.
*
* @hide
*/
+@SystemApi
public final class VmsSubscriptionState implements Parcelable {
private final int mSequenceNumber;
private final Set<VmsLayer> mLayers;
private final Set<VmsAssociatedLayer> mSubscribedLayersFromPublishers;
/**
- * Construcs a summary of the state of the current subscriptions for publishers to consume
+ * Constructs a summary of the state of the current subscriptions for publishers to consume
* and adjust which layers that the are publishing.
*/
public VmsSubscriptionState(int sequenceNumber,
- Set<VmsLayer> subscribedLayers,
- Set<VmsAssociatedLayer> layersFromPublishers) {
+ @NonNull Set<VmsLayer> subscribedLayers,
+ @NonNull Set<VmsAssociatedLayer> layersFromPublishers) {
mSequenceNumber = sequenceNumber;
mLayers = Collections.unmodifiableSet(subscribedLayers);
mSubscribedLayersFromPublishers = Collections.unmodifiableSet(layersFromPublishers);
}
/**
- * Returns the sequence number assigned by the VMS service. Sequence numbers are
- * monotonically increasing and help clients ignore potential out-of-order states.
+ * @return sequence number of the subscription state
*/
public int getSequenceNumber() {
return mSequenceNumber;
}
+ /**
+ * @return set of layers with subscriptions to all publishers
+ */
+ @NonNull
public Set<VmsLayer> getLayers() {
return mLayers;
}
+ /**
+ * @return set of layers with subscriptions to a subset of publishers
+ */
+ @NonNull
public Set<VmsAssociatedLayer> getAssociatedLayers() {
return mSubscribedLayersFromPublishers;
}
diff --git a/car-usb-handler/AndroidManifest.xml b/car-usb-handler/AndroidManifest.xml
index 7803318..91730a6 100644
--- a/car-usb-handler/AndroidManifest.xml
+++ b/car-usb-handler/AndroidManifest.xml
@@ -15,12 +15,12 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- package="android.car.usb.handler" >
+ package="android.car.usb.handler">
<uses-sdk android:minSdkVersion="25" />
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.MANAGE_USB" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application android:label="@string/app_name" android:icon="@drawable/ic_launcher"
- android:directBootAware="true" >
+ android:directBootAware="true">
<activity android:name=".UsbHostManagementActivity"
android:theme="@android:style/Theme.Material.Light.Dialog"
android:launchMode="singleTop">
@@ -28,9 +28,10 @@
android:name="distractionOptimized"
android:value="true" />
</activity>
- <receiver android:name=".BootUsbScanner" >
+ <receiver android:name=".BootUsbScanner"
+ android:directBootAware="true">
<intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
diff --git a/car-usb-handler/src/android/car/usb/handler/AoapServiceManager.java b/car-usb-handler/src/android/car/usb/handler/AoapServiceManager.java
new file mode 100644
index 0000000..04bef87
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/AoapServiceManager.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2019 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.car.usb.handler;
+
+import static android.car.AoapService.KEY_DEVICE;
+import static android.car.AoapService.KEY_RESULT;
+import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP;
+import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP_RESPONSE;
+import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED;
+import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED_RESPONSE;
+
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.car.AoapService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Manages connections to {@link android.car.AoapService} (AOAP handler apps). */
+public class AoapServiceManager {
+ private static final String TAG = AoapServiceManager.class.getSimpleName();
+
+ private static final int MSG_DISCONNECT = 1;
+ private static final int DISCONNECT_DELAY_MS = 30000;
+
+ private static final int INVOCATION_TIMEOUT_MS = 5000;
+
+
+ private final HashMap<ComponentName, AoapServiceConnection> mConnections = new HashMap<>();
+ private Context mContext;
+ private final Object mLock = new Object();
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+
+ public AoapServiceManager(Context context) {
+ mContext = context;
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mHandler = new Handler(mHandlerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_DISCONNECT) {
+ removeConnection((AoapServiceConnection) msg.obj);
+ } else {
+ Log.e(TAG, "Unexpected message " + msg.what);
+ }
+ }
+ };
+ }
+
+ /**
+ * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
+ * if it supports the device.
+ */
+ @WorkerThread
+ public boolean isDeviceSupported(UsbDevice device, ComponentName serviceName) {
+ final AoapServiceConnection connection = getConnectionOrNull(serviceName);
+ if (connection == null) {
+ return false;
+ }
+
+ try {
+ return connection.isDeviceSupported(device)
+ .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ Log.w(TAG, "Failed to get response isDeviceSupported from " + serviceName, e);
+ return false;
+ }
+ }
+
+ /**
+ * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
+ * if the device can be switched to AOAP mode now.
+ */
+ @WorkerThread
+ public boolean canSwitchDeviceToAoap(UsbDevice device, ComponentName serviceName) {
+ final AoapServiceConnection connection = getConnectionOrNull(serviceName);
+ if (connection == null) {
+ return false;
+ }
+
+ try {
+ return connection.canSwitchDeviceToAoap(device)
+ .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ Log.w(TAG, "Failed to get response of canSwitchDeviceToAoap from " + serviceName, e);
+ return false;
+ }
+ }
+
+ @Nullable
+ private AoapServiceConnection getConnectionOrNull(ComponentName name) {
+ AoapServiceConnection connection;
+ synchronized (mLock) {
+ connection = mConnections.get(name);
+ if (connection != null) {
+ postponeServiceDisconnection(connection);
+ return connection;
+ }
+
+ connection = new AoapServiceConnection(name, this, mHandlerThread.getLooper());
+ boolean bound = mContext.bindService(
+ createIntent(name), connection, Context.BIND_AUTO_CREATE);
+ if (bound) {
+ mConnections.put(name, connection);
+ postponeServiceDisconnection(connection);
+ } else {
+ Log.w(TAG, "Failed to bind to service " + name);
+ return null;
+ }
+ }
+ return connection;
+ }
+
+ private void postponeServiceDisconnection(AoapServiceConnection connection) {
+ mHandler.removeMessages(MSG_DISCONNECT, connection);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISCONNECT, connection),
+ DISCONNECT_DELAY_MS);
+ }
+
+ private static Intent createIntent(ComponentName name) {
+ Intent intent = new Intent();
+ intent.setComponent(name);
+ return intent;
+ }
+
+ private void removeConnection(AoapServiceConnection connection) {
+ Log.i(TAG, "Removing connection to " + connection);
+ synchronized (mLock) {
+ mConnections.remove(connection.mComponentName);
+ if (connection.mBound) {
+ mContext.unbindService(connection);
+ connection.mBound = false;
+ }
+ }
+ }
+
+ private static class AoapServiceConnection implements ServiceConnection {
+ private Messenger mOutgoingMessenger;
+ private boolean mBound;
+ private final CompletableFuture<Void> mConnected = new CompletableFuture<>();
+ private final SparseArray<CompletableFuture<Bundle>> mExpectedResponses =
+ new SparseArray<>();
+ private final ComponentName mComponentName;
+ private final WeakReference<AoapServiceManager> mManagerRef;
+ private final Messenger mIncomingMessenger;
+ private final Object mLock = new Object();
+
+ private AoapServiceConnection(ComponentName name, AoapServiceManager manager,
+ Looper looper) {
+ mComponentName = name;
+ mManagerRef = new WeakReference<>(manager);
+ mIncomingMessenger = new Messenger(new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ onResponse(msg);
+ }
+ });
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (service == null) {
+ Log.e(TAG, "Binder object was not provided on service connection to " + name);
+ return;
+ }
+
+ synchronized (mLock) {
+ mBound = true;
+ mOutgoingMessenger = new Messenger(service);
+ }
+ mConnected.complete(null);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (mLock) {
+ mOutgoingMessenger = null;
+ mBound = false;
+ }
+
+ final AoapServiceManager mgr = mManagerRef.get();
+ if (mgr != null) {
+ mgr.removeConnection(this);
+ }
+ }
+
+ private void onResponse(Message message) {
+ final CompletableFuture<Bundle> response;
+ synchronized (mLock) {
+ response = mExpectedResponses.removeReturnOld(message.what);
+ }
+ if (response == null) {
+ Log.e(TAG, "Received unexpected response " + message.what + ", expected: "
+ + mExpectedResponses);
+ return;
+ }
+
+ if (message.getData() == null) {
+ throw new IllegalArgumentException("Received response msg " + message.what
+ + " without data");
+ }
+ Log.i(TAG, "onResponse msg: " + message.what + ", data: " + message.getData());
+ boolean res = response.complete(message.getData());
+ if (!res) {
+ Log.w(TAG, "Failed to complete future " + response);
+ }
+ }
+
+ CompletableFuture<Boolean> isDeviceSupported(UsbDevice device) {
+ return sendMessageForResult(
+ MSG_NEW_DEVICE_ATTACHED,
+ MSG_NEW_DEVICE_ATTACHED_RESPONSE,
+ createUsbDeviceData(device))
+ .thenApply(this::isResultOk);
+
+ }
+
+ CompletableFuture<Boolean> canSwitchDeviceToAoap(UsbDevice device) {
+ return sendMessageForResult(
+ MSG_CAN_SWITCH_TO_AOAP,
+ MSG_CAN_SWITCH_TO_AOAP_RESPONSE,
+ createUsbDeviceData(device))
+ .thenApply(this::isResultOk);
+ }
+
+ private boolean isResultOk(Bundle data) {
+ int result = data.getInt(KEY_RESULT);
+ Log.i(TAG, "Got result: " + data);
+ return AoapService.RESULT_OK == result;
+ }
+
+ private static Bundle createUsbDeviceData(UsbDevice device) {
+ Bundle data = new Bundle(1);
+ data.putParcelable(KEY_DEVICE, device);
+ return data;
+ }
+
+ private CompletableFuture<Bundle> sendMessageForResult(
+ int msgRequest, int msgResponse, Bundle data) {
+ return mConnected.thenCompose(x -> {
+ CompletableFuture<Bundle> responseFuture = new CompletableFuture<>();
+ Messenger messenger;
+ synchronized (mLock) {
+ mExpectedResponses.put(msgResponse, responseFuture);
+ messenger = mOutgoingMessenger;
+ }
+ send(messenger, msgRequest, data);
+
+ return responseFuture;
+ });
+ }
+
+ private void send(Messenger messenger, int req, Bundle data) {
+ Message msg = Message.obtain(null, req, null);
+ msg.replyTo = mIncomingMessenger;
+ msg.setData(data);
+ try {
+ messenger.send(msg);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Connection broken with " + mComponentName, e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AoapServiceConnection{"
+ + "mBound=" + mBound
+ + ", mConnected=" + mConnected
+ + ", mExpectedResponses=" + mExpectedResponses
+ + ", mComponentName=" + mComponentName
+ + '}';
+ }
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbDeviceFilter.java b/car-usb-handler/src/android/car/usb/handler/UsbDeviceFilter.java
new file mode 100644
index 0000000..c698290
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbDeviceFilter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2019 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.car.usb.handler;
+
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbInterface;
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * This class is used to describe a USB device. When used in HashMaps all values must be specified,
+ * but wildcards can be used for any of the fields in the package meta-data.
+ */
+class UsbDeviceFilter {
+ private static final String TAG = UsbDeviceFilter.class.getSimpleName();
+
+ // USB Vendor ID (or -1 for unspecified)
+ public final int mVendorId;
+ // USB Product ID (or -1 for unspecified)
+ public final int mProductId;
+ // USB device or interface class (or -1 for unspecified)
+ public final int mClass;
+ // USB device subclass (or -1 for unspecified)
+ public final int mSubclass;
+ // USB device protocol (or -1 for unspecified)
+ public final int mProtocol;
+ // USB device manufacturer name string (or null for unspecified)
+ public final String mManufacturerName;
+ // USB device product name string (or null for unspecified)
+ public final String mProductName;
+ // USB device serial number string (or null for unspecified)
+ public final String mSerialNumber;
+
+ // USB device in AOAP mode manufacturer
+ public final String mAoapManufacturer;
+ // USB device in AOAP mode model
+ public final String mAoapModel;
+ // USB device in AOAP mode description string
+ public final String mAoapDescription;
+ // USB device in AOAP mode version
+ public final String mAoapVersion;
+ // USB device in AOAP mode URI
+ public final String mAoapUri;
+ // USB device in AOAP mode serial
+ public final String mAoapSerial;
+ // USB device in AOAP mode verification service
+ public final String mAoapService;
+
+ UsbDeviceFilter(int vid, int pid, int clasz, int subclass, int protocol,
+ String manufacturer, String product, String serialnum,
+ String aoapManufacturer, String aoapModel, String aoapDescription,
+ String aoapVersion, String aoapUri, String aoapSerial,
+ String aoapService) {
+ mVendorId = vid;
+ mProductId = pid;
+ mClass = clasz;
+ mSubclass = subclass;
+ mProtocol = protocol;
+ mManufacturerName = manufacturer;
+ mProductName = product;
+ mSerialNumber = serialnum;
+
+ mAoapManufacturer = aoapManufacturer;
+ mAoapModel = aoapModel;
+ mAoapDescription = aoapDescription;
+ mAoapVersion = aoapVersion;
+ mAoapUri = aoapUri;
+ mAoapSerial = aoapSerial;
+ mAoapService = aoapService;
+ }
+
+ public static UsbDeviceFilter read(XmlPullParser parser, boolean aoapData) {
+ int vendorId = -1;
+ int productId = -1;
+ int deviceClass = -1;
+ int deviceSubclass = -1;
+ int deviceProtocol = -1;
+ String manufacturerName = null;
+ String productName = null;
+ String serialNumber = null;
+
+ String aoapManufacturer = null;
+ String aoapModel = null;
+ String aoapDescription = null;
+ String aoapVersion = null;
+ String aoapUri = null;
+ String aoapSerial = null;
+ String aoapService = null;
+
+ int count = parser.getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String name = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ // Attribute values are ints or strings
+ if (!aoapData && "manufacturer-name".equals(name)) {
+ manufacturerName = value;
+ } else if (!aoapData && "product-name".equals(name)) {
+ productName = value;
+ } else if (!aoapData && "serial-number".equals(name)) {
+ serialNumber = value;
+ } else if (aoapData && "manufacturer".equals(name)) {
+ aoapManufacturer = value;
+ } else if (aoapData && "model".equals(name)) {
+ aoapModel = value;
+ } else if (aoapData && "description".equals(name)) {
+ aoapDescription = value;
+ } else if (aoapData && "version".equals(name)) {
+ aoapVersion = value;
+ } else if (aoapData && "uri".equals(name)) {
+ aoapUri = value;
+ } else if (aoapData && "serial".equals(name)) {
+ aoapSerial = value;
+ } else if (aoapData && "service".equals(name)) {
+ aoapService = value;
+ } else if (!aoapData) {
+ int intValue = -1;
+ int radix = 10;
+ if (value != null && value.length() > 2 && value.charAt(0) == '0'
+ && (value.charAt(1) == 'x' || value.charAt(1) == 'X')) {
+ // allow hex values starting with 0x or 0X
+ radix = 16;
+ value = value.substring(2);
+ }
+ try {
+ intValue = Integer.parseInt(value, radix);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "invalid number for field " + name, e);
+ continue;
+ }
+ if ("vendor-id".equals(name)) {
+ vendorId = intValue;
+ } else if ("product-id".equals(name)) {
+ productId = intValue;
+ } else if ("class".equals(name)) {
+ deviceClass = intValue;
+ } else if ("subclass".equals(name)) {
+ deviceSubclass = intValue;
+ } else if ("protocol".equals(name)) {
+ deviceProtocol = intValue;
+ }
+ }
+ }
+ return new UsbDeviceFilter(vendorId, productId,
+ deviceClass, deviceSubclass, deviceProtocol,
+ manufacturerName, productName, serialNumber, aoapManufacturer,
+ aoapModel, aoapDescription, aoapVersion, aoapUri, aoapSerial,
+ aoapService);
+ }
+
+ private boolean matches(int clasz, int subclass, int protocol) {
+ return ((mClass == -1 || clasz == mClass)
+ && (mSubclass == -1 || subclass == mSubclass)
+ && (mProtocol == -1 || protocol == mProtocol));
+ }
+
+ public boolean matches(UsbDevice device) {
+ if (mVendorId != -1 && device.getVendorId() != mVendorId) {
+ return false;
+ }
+ if (mProductId != -1 && device.getProductId() != mProductId) {
+ return false;
+ }
+ if (mManufacturerName != null && device.getManufacturerName() == null) {
+ return false;
+ }
+ if (mProductName != null && device.getProductName() == null) {
+ return false;
+ }
+ if (mSerialNumber != null && device.getSerialNumber() == null) {
+ return false;
+ }
+ if (mManufacturerName != null && device.getManufacturerName() != null
+ && !mManufacturerName.equals(device.getManufacturerName())) {
+ return false;
+ }
+ if (mProductName != null && device.getProductName() != null
+ && !mProductName.equals(device.getProductName())) {
+ return false;
+ }
+ if (mSerialNumber != null && device.getSerialNumber() != null
+ && !mSerialNumber.equals(device.getSerialNumber())) {
+ return false;
+ }
+
+ // check device class/subclass/protocol
+ if (matches(device.getDeviceClass(), device.getDeviceSubclass(),
+ device.getDeviceProtocol())) {
+ return true;
+ }
+
+ // if device doesn't match, check the interfaces
+ int count = device.getInterfaceCount();
+ for (int i = 0; i < count; i++) {
+ UsbInterface intf = device.getInterface(i);
+ if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(),
+ intf.getInterfaceProtocol())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // can't compare if we have wildcard strings
+ if (mVendorId == -1 || mProductId == -1
+ || mClass == -1 || mSubclass == -1 || mProtocol == -1) {
+ return false;
+ }
+ if (obj instanceof UsbDeviceFilter) {
+ UsbDeviceFilter filter = (UsbDeviceFilter) obj;
+
+ if (filter.mVendorId != mVendorId
+ || filter.mProductId != mProductId
+ || filter.mClass != mClass
+ || filter.mSubclass != mSubclass
+ || filter.mProtocol != mProtocol) {
+ return false;
+ }
+ if ((filter.mManufacturerName != null && mManufacturerName == null)
+ || (filter.mManufacturerName == null && mManufacturerName != null)
+ || (filter.mProductName != null && mProductName == null)
+ || (filter.mProductName == null && mProductName != null)
+ || (filter.mSerialNumber != null && mSerialNumber == null)
+ || (filter.mSerialNumber == null && mSerialNumber != null)) {
+ return false;
+ }
+ if ((filter.mManufacturerName != null && mManufacturerName != null
+ && !mManufacturerName.equals(filter.mManufacturerName))
+ || (filter.mProductName != null && mProductName != null
+ && !mProductName.equals(filter.mProductName))
+ || (filter.mSerialNumber != null && mSerialNumber != null
+ && !mSerialNumber.equals(filter.mSerialNumber))) {
+ return false;
+ }
+ return true;
+ }
+ if (obj instanceof UsbDevice) {
+ UsbDevice device = (UsbDevice) obj;
+ if (device.getVendorId() != mVendorId
+ || device.getProductId() != mProductId
+ || device.getDeviceClass() != mClass
+ || device.getDeviceSubclass() != mSubclass
+ || device.getDeviceProtocol() != mProtocol) {
+ return false;
+ }
+ if ((mManufacturerName != null && device.getManufacturerName() == null)
+ || (mManufacturerName == null && device.getManufacturerName() != null)
+ || (mProductName != null && device.getProductName() == null)
+ || (mProductName == null && device.getProductName() != null)
+ || (mSerialNumber != null && device.getSerialNumber() == null)
+ || (mSerialNumber == null && device.getSerialNumber() != null)) {
+ return false;
+ }
+ if ((device.getManufacturerName() != null
+ && !mManufacturerName.equals(device.getManufacturerName()))
+ || (device.getProductName() != null
+ && !mProductName.equals(device.getProductName()))
+ || (device.getSerialNumber() != null
+ && !mSerialNumber.equals(device.getSerialNumber()))) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (((mVendorId << 16) | mProductId)
+ ^ ((mClass << 16) | (mSubclass << 8) | mProtocol));
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId=" + mProductId
+ + ",mClass=" + mClass + ",mSubclass=" + mSubclass
+ + ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName
+ + ",mProductName=" + mProductName + ",mSerialNumber=" + mSerialNumber + "]";
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java b/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java
index b5abc58..96f12cf 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java
@@ -15,12 +15,13 @@
*/
package android.car.usb.handler;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.Manifest;
import android.annotation.Nullable;
-import android.car.IUsbAoapSupportCheckService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -28,16 +29,12 @@
import android.content.res.XmlResourceParser;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
-import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
import android.util.Log;
-import android.util.Pair;
import com.android.internal.util.XmlUtils;
@@ -45,15 +42,17 @@
import java.io.IOException;
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Queue;
/** Resolves supported handlers for USB device. */
public final class UsbDeviceHandlerResolver {
+
private static final String TAG = UsbDeviceHandlerResolver.class.getSimpleName();
private static final boolean LOCAL_LOGD = true;
+ private static final String AOAP_HANDLE_PERMISSION =
+ "android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE";
+
/**
* Callbacks for device resolver.
*/
@@ -71,330 +70,7 @@
private final Context mContext;
private final HandlerThread mHandlerThread;
private final UsbDeviceResolverHandler mHandler;
-
- private class DeviceContext {
- public final UsbDevice usbDevice;
- @Nullable public final UsbDeviceConnection connection;
- public final UsbDeviceSettings settings;
- public final List<UsbDeviceSettings> activeDeviceSettings;
- public final Queue<Pair<ResolveInfo, DeviceFilter>> mActiveDeviceOptions =
- new LinkedList<>();
-
- private volatile IUsbAoapSupportCheckService mUsbAoapSupportCheckService;
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
- Log.i(TAG, "onServiceConnected: " + className);
- mUsbAoapSupportCheckService = IUsbAoapSupportCheckService.Stub.asInterface(service);
- mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName className) {
- Log.i(TAG, "onServiceDisconnected: " + className);
- mUsbAoapSupportCheckService = null;
- mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
- }
- };
-
- public DeviceContext(UsbDevice usbDevice, UsbDeviceSettings settings,
- List<UsbDeviceSettings> activeDeviceSettings) {
- this.usbDevice = usbDevice;
- this.settings = settings;
- this.activeDeviceSettings = activeDeviceSettings;
- connection = UsbUtil.openConnection(mUsbManager, usbDevice);
- }
- }
-
- // This class is used to describe a USB device.
- // When used in HashMaps all values must be specified,
- // but wildcards can be used for any of the fields in
- // the package meta-data.
- private static class DeviceFilter {
- // USB Vendor ID (or -1 for unspecified)
- public final int mVendorId;
- // USB Product ID (or -1 for unspecified)
- public final int mProductId;
- // USB device or interface class (or -1 for unspecified)
- public final int mClass;
- // USB device subclass (or -1 for unspecified)
- public final int mSubclass;
- // USB device protocol (or -1 for unspecified)
- public final int mProtocol;
- // USB device manufacturer name string (or null for unspecified)
- public final String mManufacturerName;
- // USB device product name string (or null for unspecified)
- public final String mProductName;
- // USB device serial number string (or null for unspecified)
- public final String mSerialNumber;
-
- // USB device in AOAP mode manufacturer
- public final String mAoapManufacturer;
- // USB device in AOAP mode model
- public final String mAoapModel;
- // USB device in AOAP mode description string
- public final String mAoapDescription;
- // USB device in AOAP mode version
- public final String mAoapVersion;
- // USB device in AOAP mode URI
- public final String mAoapUri;
- // USB device in AOAP mode serial
- public final String mAoapSerial;
- // USB device in AOAP mode verification service
- public final String mAoapService;
-
- DeviceFilter(int vid, int pid, int clasz, int subclass, int protocol,
- String manufacturer, String product, String serialnum,
- String aoapManufacturer, String aoapModel, String aoapDescription,
- String aoapVersion, String aoapUri, String aoapSerial,
- String aoapService) {
- mVendorId = vid;
- mProductId = pid;
- mClass = clasz;
- mSubclass = subclass;
- mProtocol = protocol;
- mManufacturerName = manufacturer;
- mProductName = product;
- mSerialNumber = serialnum;
-
- mAoapManufacturer = aoapManufacturer;
- mAoapModel = aoapModel;
- mAoapDescription = aoapDescription;
- mAoapVersion = aoapVersion;
- mAoapUri = aoapUri;
- mAoapSerial = aoapSerial;
- mAoapService = aoapService;
- }
-
- DeviceFilter(UsbDevice device) {
- mVendorId = device.getVendorId();
- mProductId = device.getProductId();
- mClass = device.getDeviceClass();
- mSubclass = device.getDeviceSubclass();
- mProtocol = device.getDeviceProtocol();
- mManufacturerName = device.getManufacturerName();
- mProductName = device.getProductName();
- mSerialNumber = device.getSerialNumber();
- mAoapManufacturer = null;
- mAoapModel = null;
- mAoapDescription = null;
- mAoapVersion = null;
- mAoapUri = null;
- mAoapSerial = null;
- mAoapService = null;
- }
-
- public static DeviceFilter read(XmlPullParser parser, boolean aoapData) {
- int vendorId = -1;
- int productId = -1;
- int deviceClass = -1;
- int deviceSubclass = -1;
- int deviceProtocol = -1;
- String manufacturerName = null;
- String productName = null;
- String serialNumber = null;
-
- String aoapManufacturer = null;
- String aoapModel = null;
- String aoapDescription = null;
- String aoapVersion = null;
- String aoapUri = null;
- String aoapSerial = null;
- String aoapService = null;
-
- int count = parser.getAttributeCount();
- for (int i = 0; i < count; i++) {
- String name = parser.getAttributeName(i);
- String value = parser.getAttributeValue(i);
- // Attribute values are ints or strings
- if (!aoapData && "manufacturer-name".equals(name)) {
- manufacturerName = value;
- } else if (!aoapData && "product-name".equals(name)) {
- productName = value;
- } else if (!aoapData && "serial-number".equals(name)) {
- serialNumber = value;
- } else if (aoapData && "manufacturer".equals(name)) {
- aoapManufacturer = value;
- } else if (aoapData && "model".equals(name)) {
- aoapModel = value;
- } else if (aoapData && "description".equals(name)) {
- aoapDescription = value;
- } else if (aoapData && "version".equals(name)) {
- aoapVersion = value;
- } else if (aoapData && "uri".equals(name)) {
- aoapUri = value;
- } else if (aoapData && "serial".equals(name)) {
- aoapSerial = value;
- } else if (aoapData && "service".equals(name)) {
- aoapService = value;
- } else if (!aoapData) {
- int intValue = -1;
- int radix = 10;
- if (value != null && value.length() > 2 && value.charAt(0) == '0'
- && (value.charAt(1) == 'x' || value.charAt(1) == 'X')) {
- // allow hex values starting with 0x or 0X
- radix = 16;
- value = value.substring(2);
- }
- try {
- intValue = Integer.parseInt(value, radix);
- } catch (NumberFormatException e) {
- Log.e(TAG, "invalid number for field " + name, e);
- continue;
- }
- if ("vendor-id".equals(name)) {
- vendorId = intValue;
- } else if ("product-id".equals(name)) {
- productId = intValue;
- } else if ("class".equals(name)) {
- deviceClass = intValue;
- } else if ("subclass".equals(name)) {
- deviceSubclass = intValue;
- } else if ("protocol".equals(name)) {
- deviceProtocol = intValue;
- }
- }
- }
- return new DeviceFilter(vendorId, productId,
- deviceClass, deviceSubclass, deviceProtocol,
- manufacturerName, productName, serialNumber, aoapManufacturer,
- aoapModel, aoapDescription, aoapVersion, aoapUri, aoapSerial,
- aoapService);
- }
-
- private boolean matches(int clasz, int subclass, int protocol) {
- return ((mClass == -1 || clasz == mClass)
- && (mSubclass == -1 || subclass == mSubclass)
- && (mProtocol == -1 || protocol == mProtocol));
- }
-
- public boolean matches(UsbDevice device) {
- if (mVendorId != -1 && device.getVendorId() != mVendorId) {
- return false;
- }
- if (mProductId != -1 && device.getProductId() != mProductId) {
- return false;
- }
- if (mManufacturerName != null && device.getManufacturerName() == null) {
- return false;
- }
- if (mProductName != null && device.getProductName() == null) {
- return false;
- }
- if (mSerialNumber != null && device.getSerialNumber() == null) {
- return false;
- }
- if (mManufacturerName != null && device.getManufacturerName() != null
- && !mManufacturerName.equals(device.getManufacturerName())) {
- return false;
- }
- if (mProductName != null && device.getProductName() != null
- && !mProductName.equals(device.getProductName())) {
- return false;
- }
- if (mSerialNumber != null && device.getSerialNumber() != null
- && !mSerialNumber.equals(device.getSerialNumber())) {
- return false;
- }
-
- // check device class/subclass/protocol
- if (matches(device.getDeviceClass(), device.getDeviceSubclass(),
- device.getDeviceProtocol())) {
- return true;
- }
-
- // if device doesn't match, check the interfaces
- int count = device.getInterfaceCount();
- for (int i = 0; i < count; i++) {
- UsbInterface intf = device.getInterface(i);
- if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(),
- intf.getInterfaceProtocol())) {
- return true;
- }
- }
-
- return false;
- }
-
- @Override
- public boolean equals(Object obj) {
- // can't compare if we have wildcard strings
- if (mVendorId == -1 || mProductId == -1
- || mClass == -1 || mSubclass == -1 || mProtocol == -1) {
- return false;
- }
- if (obj instanceof DeviceFilter) {
- DeviceFilter filter = (DeviceFilter) obj;
-
- if (filter.mVendorId != mVendorId
- || filter.mProductId != mProductId
- || filter.mClass != mClass
- || filter.mSubclass != mSubclass
- || filter.mProtocol != mProtocol) {
- return false;
- }
- if ((filter.mManufacturerName != null && mManufacturerName == null)
- || (filter.mManufacturerName == null && mManufacturerName != null)
- || (filter.mProductName != null && mProductName == null)
- || (filter.mProductName == null && mProductName != null)
- || (filter.mSerialNumber != null && mSerialNumber == null)
- || (filter.mSerialNumber == null && mSerialNumber != null)) {
- return false;
- }
- if ((filter.mManufacturerName != null && mManufacturerName != null
- && !mManufacturerName.equals(filter.mManufacturerName))
- || (filter.mProductName != null && mProductName != null
- && !mProductName.equals(filter.mProductName))
- || (filter.mSerialNumber != null && mSerialNumber != null
- && !mSerialNumber.equals(filter.mSerialNumber))) {
- return false;
- }
- return true;
- }
- if (obj instanceof UsbDevice) {
- UsbDevice device = (UsbDevice) obj;
- if (device.getVendorId() != mVendorId
- || device.getProductId() != mProductId
- || device.getDeviceClass() != mClass
- || device.getDeviceSubclass() != mSubclass
- || device.getDeviceProtocol() != mProtocol) {
- return false;
- }
- if ((mManufacturerName != null && device.getManufacturerName() == null)
- || (mManufacturerName == null && device.getManufacturerName() != null)
- || (mProductName != null && device.getProductName() == null)
- || (mProductName == null && device.getProductName() != null)
- || (mSerialNumber != null && device.getSerialNumber() == null)
- || (mSerialNumber == null && device.getSerialNumber() != null)) {
- return false;
- }
- if ((device.getManufacturerName() != null
- && !mManufacturerName.equals(device.getManufacturerName()))
- || (device.getProductName() != null
- && !mProductName.equals(device.getProductName()))
- || (device.getSerialNumber() != null
- && !mSerialNumber.equals(device.getSerialNumber()))) {
- return false;
- }
- return true;
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return (((mVendorId << 16) | mProductId)
- ^ ((mClass << 16) | (mSubclass << 8) | mProtocol));
- }
-
- @Override
- public String toString() {
- return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId=" + mProductId
- + ",mClass=" + mClass + ",mSubclass=" + mSubclass
- + ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName
- + ",mProductName=" + mProductName + ",mSerialNumber=" + mSerialNumber + "]";
- }
- }
+ private final AoapServiceManager mAoapServiceManager;
public UsbDeviceHandlerResolver(UsbManager manager, Context context,
UsbDeviceHandlerResolverCallback deviceListener) {
@@ -405,6 +81,7 @@
mHandlerThread.start();
mHandler = new UsbDeviceResolverHandler(mHandlerThread.getLooper());
mPackageManager = context.getPackageManager();
+ mAoapServiceManager = new AoapServiceManager(mContext.getApplicationContext());
}
/**
@@ -444,10 +121,19 @@
if (AoapInterface.isDeviceInAoapMode(device)) {
mDeviceCallback.onDeviceDispatched();
} else {
- DeviceFilter filter =
+ UsbDeviceFilter filter =
packageMatches(activityInfo, intent.getAction(), device, true);
+
if (filter != null) {
- requestAoapSwitch(device, filter);
+ mHandlerThread.getThreadHandler().post(() -> {
+ if (mAoapServiceManager.canSwitchDeviceToAoap(device,
+ ComponentName.unflattenFromString(filter.mAoapService))) {
+ requestAoapSwitch(device, filter);
+ } else {
+ Log.i(TAG, "Ignore AOAP switch for device " + device
+ + " handled by " + filter.mAoapService);
+ }
+ });
return true;
}
}
@@ -474,52 +160,35 @@
}
Intent intent = createDeviceAttachedIntent(device);
- List<Pair<ResolveInfo, DeviceFilter>> matches = getDeviceMatches(device, intent, false);
+ List<UsbHandlerPackage> matches = getDeviceMatches(device, intent, false);
if (LOCAL_LOGD) {
Log.d(TAG, "matches size: " + matches.size());
}
- List<UsbDeviceSettings> settings = new ArrayList<>(matches.size());
- for (Pair<ResolveInfo, DeviceFilter> info : matches) {
- UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(device);
- setting.setHandler(
- new ComponentName(
- info.first.activityInfo.packageName, info.first.activityInfo.name));
- settings.add(setting);
+ List<UsbDeviceSettings> settings = new ArrayList<>();
+ for (UsbHandlerPackage pkg : matches) {
+ settings.add(createSettings(device, pkg));
}
- DeviceContext deviceContext =
- new DeviceContext(device, UsbDeviceSettings.constructSettings(device), settings);
- if (deviceContext.connection != null
- && AoapInterface.isSupported(mContext, device, deviceContext.connection)) {
- deviceContext.mActiveDeviceOptions.addAll(getDeviceMatches(device, intent, true));
- queryNextAoapHandler(deviceContext);
- } else {
- deviceProbingComplete(deviceContext);
- }
- }
- private void queryNextAoapHandler(DeviceContext context) {
- Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
- if (option == null) {
- Log.w(TAG, "No more options left.");
- deviceProbingComplete(context);
- return;
- }
- Intent serviceIntent = new Intent();
- serviceIntent.setComponent(ComponentName.unflattenFromString(option.second.mAoapService));
- boolean bound = mContext.bindService(serviceIntent, context.mServiceConnection,
- Context.BIND_AUTO_CREATE);
- if (bound) {
- mHandler.requestServiceConnectionTimeout();
- } else {
- if (LOCAL_LOGD) {
- Log.d(TAG, "Failed to bind to the service");
+ UsbDeviceConnection devConnection = UsbUtil.openConnection(mUsbManager, device);
+ if (devConnection != null && AoapInterface.isSupported(mContext, device, devConnection)) {
+ for (UsbHandlerPackage pkg : getDeviceMatches(device, intent, true)) {
+ if (mAoapServiceManager.isDeviceSupported(device, pkg.mAoapService)) {
+ settings.add(createSettings(device, pkg));
+ }
}
- context.mActiveDeviceOptions.poll();
- queryNextAoapHandler(context);
}
+
+ deviceProbingComplete(device, settings);
}
- private void requestAoapSwitch(UsbDevice device, DeviceFilter filter) {
+ private UsbDeviceSettings createSettings(UsbDevice device, UsbHandlerPackage pkg) {
+ UsbDeviceSettings settings = UsbDeviceSettings.constructSettings(device);
+ settings.setHandler(pkg.mActivity);
+ settings.setAoap(pkg.mAoapService != null);
+ return settings;
+ }
+
+ private void requestAoapSwitch(UsbDevice device, UsbDeviceFilter filter) {
UsbDeviceConnection connection = UsbUtil.openConnection(mUsbManager, device);
if (connection == null) {
Log.e(TAG, "Failed to connect to usb device.");
@@ -540,67 +209,79 @@
connection.close();
}
- private void deviceProbingComplete(DeviceContext context) {
+ private void deviceProbingComplete(UsbDevice device, List<UsbDeviceSettings> settings) {
if (LOCAL_LOGD) {
Log.d(TAG, "deviceProbingComplete");
}
- mDeviceCallback.onHandlersResolveCompleted(context.usbDevice, context.activeDeviceSettings);
+ mDeviceCallback.onHandlersResolveCompleted(device, settings);
}
- private void doHandleServiceConnectionStateChanged(DeviceContext context) {
- if (LOCAL_LOGD) {
- Log.d(TAG, "doHandleServiceConnectionStateChanged: "
- + context.mUsbAoapSupportCheckService);
- }
- if (context.mUsbAoapSupportCheckService != null) {
- boolean deviceSupported = false;
- try {
- deviceSupported =
- context.mUsbAoapSupportCheckService.isDeviceSupported(context.usbDevice);
- } catch (RemoteException e) {
- Log.e(TAG, "Call to remote service failed", e);
- }
- if (deviceSupported) {
- Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
-
- UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(context.settings);
- setting.setHandler(
- new ComponentName(
- option.first.activityInfo.packageName, option.first.activityInfo.name));
- setting.setAoap(true);
- context.activeDeviceSettings.add(setting);
- }
- mContext.unbindService(context.mServiceConnection);
- }
- context.mActiveDeviceOptions.poll();
- queryNextAoapHandler(context);
- }
-
- private List<Pair<ResolveInfo, DeviceFilter>> getDeviceMatches(
+ private List<UsbHandlerPackage> getDeviceMatches(
UsbDevice device, Intent intent, boolean forAoap) {
- List<Pair<ResolveInfo, DeviceFilter>> matches = new ArrayList<>();
+ List<UsbHandlerPackage> matches = new ArrayList<>();
List<ResolveInfo> resolveInfos =
mPackageManager.queryIntentActivities(intent, PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
- DeviceFilter filter = packageMatches(resolveInfo.activityInfo,
+ if (forAoap && !hasAoapPermission(resolveInfo.resolvePackageName)) {
+ Log.w(TAG, "Package " + resolveInfo.resolvePackageName + " does not hold "
+ + AOAP_HANDLE_PERMISSION + " permission. Ignore the package.");
+ continue;
+ }
+
+ UsbDeviceFilter filter = packageMatches(resolveInfo.activityInfo,
intent.getAction(), device, forAoap);
if (filter != null) {
- matches.add(Pair.create(resolveInfo, filter));
+ ActivityInfo ai = resolveInfo.activityInfo;
+ ComponentName activity = new ComponentName(ai.packageName, ai.name);
+ ComponentName aoapService = filter.mAoapService == null
+ ? null : ComponentName.unflattenFromString(filter.mAoapService);
+
+ if (aoapService != null && !checkServiceRequiresPermission(aoapService)) {
+ continue;
+ }
+
+ if (aoapService != null || !forAoap) {
+ matches.add(new UsbHandlerPackage(activity, aoapService));
+ }
}
}
return matches;
}
- private DeviceFilter packageMatches(ActivityInfo ai, String metaDataName, UsbDevice device,
+ private boolean checkServiceRequiresPermission(ComponentName serviceName) {
+ Intent intent = new Intent();
+ intent.setComponent(serviceName);
+ boolean found = false;
+ for (ResolveInfo info : mPackageManager.queryIntentServices(intent, 0)) {
+ if (info.serviceInfo != null) {
+ found = true;
+ if ((Manifest.permission.MANAGE_USB.equals(info.serviceInfo.permission))) {
+ return true;
+ }
+ }
+ }
+ if (found) {
+ Log.w(TAG, "Component " + serviceName + " must be protected with "
+ + Manifest.permission.MANAGE_USB + " permission");
+ } else {
+ Log.w(TAG, "Component " + serviceName + " not found");
+ }
+ return false;
+ }
+
+ private boolean hasAoapPermission(String packageName) {
+ return mPackageManager
+ .checkPermission(AOAP_HANDLE_PERMISSION, packageName) == PERMISSION_GRANTED;
+ }
+
+ private UsbDeviceFilter packageMatches(ActivityInfo ai, String metaDataName, UsbDevice device,
boolean forAoap) {
if (LOCAL_LOGD) {
Log.d(TAG, "packageMatches ai: " + ai + "metaDataName: " + metaDataName + " forAoap: "
+ forAoap);
}
String filterTagName = forAoap ? "usb-aoap-accessory" : "usb-device";
- XmlResourceParser parser = null;
- try {
- parser = ai.loadXmlMetaData(mPackageManager, metaDataName);
+ try (XmlResourceParser parser = ai.loadXmlMetaData(mPackageManager, metaDataName)) {
if (parser == null) {
Log.w(TAG, "no meta-data for " + ai);
return null;
@@ -610,7 +291,7 @@
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
if (device != null && filterTagName.equals(tagName)) {
- DeviceFilter filter = DeviceFilter.read(parser, forAoap);
+ UsbDeviceFilter filter = UsbDeviceFilter.read(parser, forAoap);
if (forAoap || filter.matches(device)) {
return filter;
}
@@ -619,38 +300,24 @@
}
} catch (Exception e) {
Log.w(TAG, "Unable to load component info " + ai.toString(), e);
- } finally {
- if (parser != null) parser.close();
}
return null;
}
private class UsbDeviceResolverHandler extends Handler {
private static final int MSG_RESOLVE_HANDLERS = 0;
- private static final int MSG_SERVICE_CONNECTION_STATE_CHANGE = 1;
- private static final int MSG_SERVICE_CONNECTION_TIMEOUT = 2;
private static final int MSG_COMPLETE_DISPATCH = 3;
- private static final long CONNECT_TIMEOUT_MS = 5000;
-
private UsbDeviceResolverHandler(Looper looper) {
super(looper);
}
- public void requestResolveHandlers(UsbDevice device) {
+ void requestResolveHandlers(UsbDevice device) {
Message msg = obtainMessage(MSG_RESOLVE_HANDLERS, device);
sendMessage(msg);
}
- public void requestOnServiceConnectionStateChanged(DeviceContext deviceContext) {
- sendMessage(obtainMessage(MSG_SERVICE_CONNECTION_STATE_CHANGE, deviceContext));
- }
-
- public void requestServiceConnectionTimeout() {
- sendEmptyMessageDelayed(MSG_SERVICE_CONNECTION_TIMEOUT, CONNECT_TIMEOUT_MS);
- }
-
- public void requestCompleteDeviceDispatch() {
+ void requestCompleteDeviceDispatch() {
sendEmptyMessage(MSG_COMPLETE_DISPATCH);
}
@@ -660,14 +327,6 @@
case MSG_RESOLVE_HANDLERS:
doHandleResolveHandlers((UsbDevice) msg.obj);
break;
- case MSG_SERVICE_CONNECTION_STATE_CHANGE:
- removeMessages(MSG_SERVICE_CONNECTION_TIMEOUT);
- doHandleServiceConnectionStateChanged((DeviceContext) msg.obj);
- break;
- case MSG_SERVICE_CONNECTION_TIMEOUT:
- Log.i(TAG, "Service connection timeout");
- doHandleServiceConnectionStateChanged(null);
- break;
case MSG_COMPLETE_DISPATCH:
mDeviceCallback.onDeviceDispatched();
break;
@@ -676,4 +335,14 @@
}
}
}
+
+ private static class UsbHandlerPackage {
+ final ComponentName mActivity;
+ final @Nullable ComponentName mAoapService;
+
+ UsbHandlerPackage(ComponentName activity, @Nullable ComponentName aoapService) {
+ mActivity = activity;
+ mAoapService = aoapService;
+ }
+ }
}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
index ec9a89a..2762e6f 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
@@ -56,6 +56,8 @@
private static final boolean LOCAL_LOGD = true;
private static final boolean LOCAL_LOGV = true;
+ private static final int DISPATCH_RETRY_DELAY_MS = 1000;
+ private static final int DISPATCH_RETRY_ATTEMPTS = 5;
private final List<UsbDeviceSettings> mEmptyList = new ArrayList<>();
private final Context mContext;
@@ -158,16 +160,16 @@
mCallback.processingStateChanged(true);
UsbDeviceSettings settings = mUsbSettingsStorage.getSettings(device);
- if (settings != null && mUsbResolver.dispatch(
- device, settings.getHandler(), settings.getAoap())) {
- if (LOCAL_LOGV) {
- Log.v(TAG, "Usb Device: " + device + " was sent to component: "
- + settings.getHandler());
- }
- return;
+
+ if (settings == null) {
+ resolveDevice(device);
+ } else {
+ Object obj =
+ new UsbHostControllerHandlerDispatchData(
+ device, settings, DISPATCH_RETRY_ATTEMPTS, true);
+ Message.obtain(mHandler, UsbHostControllerHandler.MSG_DEVICE_DISPATCH, obj)
+ .sendToTarget();
}
- mCallback.titleChanged(generateTitle(mContext, device));
- mUsbResolver.resolve(device);
}
/**
@@ -175,7 +177,17 @@
*/
public void applyDeviceSettings(UsbDeviceSettings settings) {
mUsbSettingsStorage.saveSettings(settings);
- mUsbResolver.dispatch(getActiveDevice(), settings.getHandler(), settings.getAoap());
+ Message msg = mHandler.obtainMessage();
+ msg.obj =
+ new UsbHostControllerHandlerDispatchData(
+ getActiveDevice(), settings, DISPATCH_RETRY_ATTEMPTS, false);
+ msg.what = UsbHostControllerHandler.MSG_DEVICE_DISPATCH;
+ msg.sendToTarget();
+ }
+
+ private void resolveDevice(UsbDevice device) {
+ mCallback.titleChanged(generateTitle(mContext, device));
+ mUsbResolver.resolve(device);
}
/**
@@ -244,8 +256,34 @@
}
}
+ private class UsbHostControllerHandlerDispatchData {
+ private final UsbDevice mUsbDevice;
+ private final UsbDeviceSettings mUsbDeviceSettings;
+
+ public int mRetries = 0;
+ public boolean mCanResolve = true;
+
+ public UsbHostControllerHandlerDispatchData(
+ UsbDevice usbDevice, UsbDeviceSettings usbDeviceSettings,
+ int retries, boolean canResolve) {
+ mUsbDevice = usbDevice;
+ mUsbDeviceSettings = usbDeviceSettings;
+ mRetries = retries;
+ mCanResolve = canResolve;
+ }
+
+ public UsbDevice getUsbDevice() {
+ return mUsbDevice;
+ }
+
+ public UsbDeviceSettings getUsbDeviceSettings() {
+ return mUsbDeviceSettings;
+ }
+ }
+
private class UsbHostControllerHandler extends Handler {
private static final int MSG_DEVICE_REMOVED = 1;
+ private static final int MSG_DEVICE_DISPATCH = 2;
private static final int DEVICE_REMOVE_TIMEOUT_MS = 500;
@@ -263,6 +301,24 @@
case MSG_DEVICE_REMOVED:
doHandleDeviceRemoved();
break;
+ case MSG_DEVICE_DISPATCH:
+ UsbHostControllerHandlerDispatchData data =
+ (UsbHostControllerHandlerDispatchData) msg.obj;
+ UsbDevice device = data.getUsbDevice();
+ UsbDeviceSettings settings = data.getUsbDeviceSettings();
+ if (!mUsbResolver.dispatch(device, settings.getHandler(), settings.getAoap())) {
+ if (data.mRetries > 0) {
+ --data.mRetries;
+ Message nextMessage = Message.obtain(msg);
+ mHandler.sendMessageDelayed(nextMessage, DISPATCH_RETRY_DELAY_MS);
+ } else if (data.mCanResolve) {
+ resolveDevice(device);
+ }
+ } else if (LOCAL_LOGV) {
+ Log.v(TAG, "Usb Device: " + data.getUsbDevice() + " was sent to component: "
+ + settings.getHandler());
+ }
+ break;
default:
Log.w(TAG, "Unhandled message: " + msg);
super.handleMessage(msg);
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java b/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java
index 387ae62..d571d5d 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java
@@ -15,17 +15,23 @@
*/
package android.car.usb.handler;
+import static android.content.Intent.ACTION_USER_UNLOCKED;
+
import android.annotation.Nullable;
import android.app.Activity;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -55,6 +61,39 @@
private UsbHostController mController;
private PackageManager mPackageManager;
+ private final ResolveBroadcastReceiver mResolveBroadcastReceiver
+ = new ResolveBroadcastReceiver();
+ private boolean mReceiverRegistered = false;
+
+ private void unregisterResolveBroadcastReceiver() {
+ if (mReceiverRegistered) {
+ unregisterReceiver(mResolveBroadcastReceiver);
+ mReceiverRegistered = false;
+ }
+ }
+
+ private void processDevice() {
+ UsbDevice connectedDevice = getDevice();
+
+ if (connectedDevice != null) {
+ mController.processDevice(connectedDevice);
+ } else {
+ unregisterResolveBroadcastReceiver();
+ finish();
+ }
+ }
+
+ private class ResolveBroadcastReceiver extends BroadcastReceiver {
+ public void onReceive(Context context, Intent intent) {
+ // We could have been unregistered after receiving the intent but before processing it,
+ // so make sure we are still registered.
+ if (mReceiverRegistered) {
+ processDevice();
+ unregisterResolveBroadcastReceiver();
+ }
+ }
+ }
+
private final AdapterView.OnItemClickListener mHandlerClickListener =
new AdapterView.OnItemClickListener() {
@Override
@@ -85,13 +124,25 @@
}
@Override
+ public void onPause() {
+ super.onPause();
+ unregisterResolveBroadcastReceiver();
+ }
+
+ @Override
public void onResume() {
super.onResume();
- UsbDevice connectedDevice = getDevice();
- if (connectedDevice != null) {
- mController.processDevice(connectedDevice);
+
+ UserManager userManager = getSystemService(UserManager.class);
+ if (userManager.isUserUnlocked() || getUserId() == UserHandle.USER_SYSTEM) {
+ processDevice();
} else {
- finish();
+ mReceiverRegistered = true;
+ registerReceiver(mResolveBroadcastReceiver, new IntentFilter(ACTION_USER_UNLOCKED));
+ // in case the car was unlocked while the receiver was being registered
+ if (userManager.isUserUnlocked()) {
+ mResolveBroadcastReceiver.onReceive(this, new Intent(ACTION_USER_UNLOCKED));
+ }
}
}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
index 7e8704e..b578161 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
@@ -183,13 +183,18 @@
return contentValues;
}
-
private static class UsbSettingsDbHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 2;
private static final String DATABASE_NAME = "usb_devices.db";
+ // we are using device protected storage because we may need to access the db before the
+ // user has authenticated
UsbSettingsDbHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ super(
+ context.createDeviceProtectedStorageContext(),
+ DATABASE_NAME,
+ null,
+ DATABASE_VERSION);
}
@Override
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 62c8042..0fbed7d 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -83,6 +83,7 @@
# Automotive specific packages
PRODUCT_PACKAGES += \
+ CarFrameworkPackageStubs \
CarService \
CarDialerApp \
CarRadioApp \
diff --git a/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml b/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
index 27dbeda..ab798c2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
@@ -37,6 +37,7 @@
android:id="@*android:id/button3"
style="@*android:style/CarAction1"
android:background="@*android:drawable/car_dialog_button_background"
+ android:layout_marginEnd="@*android:dimen/car_padding_4"
android:layout_width="wrap_content"
android:layout_height="@*android:dimen/car_card_action_bar_height" />
@@ -52,7 +53,6 @@
android:id="@*android:id/button1"
style="@*android:style/CarAction1"
android:background="@*android:drawable/car_dialog_button_background"
- android:layout_marginStart="@*android:dimen/car_padding_4"
android:layout_width="wrap_content"
android:layout_height="@*android:dimen/car_card_action_bar_height" />
<Space
diff --git a/car_product/sepolicy/public/service.te b/car_product/sepolicy/public/service.te
index 6c514f3..87426f4 100644
--- a/car_product/sepolicy/public/service.te
+++ b/car_product/sepolicy/public/service.te
@@ -1,2 +1,2 @@
-type carservice_service, service_manager_type;
+type carservice_service, app_api_service, service_manager_type;
type procfsinspector_service, service_manager_type;
diff --git a/evs/manager/Android.mk b/evs/manager/Android.mk
index 375a84c..9545be6 100644
--- a/evs/manager/Android.mk
+++ b/evs/manager/Android.mk
@@ -8,6 +8,7 @@
Enumerator.cpp \
HalCamera.cpp \
VirtualCamera.cpp \
+ HalDisplay.cpp
LOCAL_SHARED_LIBRARIES := \
diff --git a/evs/manager/Enumerator.cpp b/evs/manager/Enumerator.cpp
index 7e53304..317d473 100644
--- a/evs/manager/Enumerator.cpp
+++ b/evs/manager/Enumerator.cpp
@@ -15,6 +15,7 @@
*/
#include "Enumerator.h"
+#include "HalDisplay.h"
namespace android {
namespace automotive {
@@ -138,28 +139,34 @@
sp<IEvsDisplay> pActiveDisplay = mHwEnumerator->openDisplay();
if (pActiveDisplay == nullptr) {
ALOGE("EVS Display unavailable");
+
+ return nullptr;
}
// Remember (via weak pointer) who we think the most recently opened display is so that
// we can proxy state requests from other callers to it.
- mActiveDisplay = pActiveDisplay;
- return pActiveDisplay;
+ // TODO: Because of b/129284474, an additional class, HalDisplay, has been defined and
+ // wraps the IEvsDisplay object the driver returns. We may want to remove this
+ // additional class when it is fixed properly.
+ sp<IEvsDisplay> pHalDisplay = new HalDisplay(pActiveDisplay);
+ mActiveDisplay = pHalDisplay;
+
+ return pHalDisplay;
}
Return<void> Enumerator::closeDisplay(const ::android::sp<IEvsDisplay>& display) {
ALOGD("closeDisplay");
- // Do we still have a display object we think should be active?
sp<IEvsDisplay> pActiveDisplay = mActiveDisplay.promote();
// Drop the active display
if (display.get() != pActiveDisplay.get()) {
- ALOGW("Ignoring call to closeDisplay with unrecognzied display object.");
- ALOGI("Got %p while active display is %p.", display.get(), pActiveDisplay.get());
+ ALOGW("Ignoring call to closeDisplay with unrecognized display object.");
} else {
// Pass this request through to the hardware layer
- mHwEnumerator->closeDisplay(display);
+ sp<HalDisplay> halDisplay = reinterpret_cast<HalDisplay *>(pActiveDisplay.get());
+ mHwEnumerator->closeDisplay(halDisplay->getHwDisplay());
mActiveDisplay = nullptr;
}
diff --git a/evs/manager/Enumerator.h b/evs/manager/Enumerator.h
index 4d2c769..4dd3483 100644
--- a/evs/manager/Enumerator.h
+++ b/evs/manager/Enumerator.h
@@ -50,7 +50,7 @@
private:
sp<IEvsEnumerator> mHwEnumerator; // Hardware enumerator
- wp<IEvsDisplay> mActiveDisplay; // Hardware display
+ wp<IEvsDisplay> mActiveDisplay; // Display proxy object warpping hw display
std::list<sp<HalCamera>> mCameras; // Camera proxy objects wrapping hw cameras
};
diff --git a/evs/manager/HalDisplay.cpp b/evs/manager/HalDisplay.cpp
new file mode 100644
index 0000000..c3f72b2
--- /dev/null
+++ b/evs/manager/HalDisplay.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include <log/log.h>
+#include "HalDisplay.h"
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_0 {
+namespace implementation {
+
+HalDisplay::HalDisplay(sp<IEvsDisplay>& display) :
+ mHwDisplay(display) {
+ // nothing to do.
+}
+
+HalDisplay::~HalDisplay() {
+ shutdown();
+}
+
+void HalDisplay::shutdown() {
+ // simply release a strong pointer to remote display object.
+ mHwDisplay = nullptr;
+}
+
+/**
+ * Returns a strong pointer to remote display object.
+ */
+sp<IEvsDisplay> HalDisplay::getHwDisplay() {
+ return mHwDisplay;
+}
+
+/**
+ * Gets basic display information from a hardware display object
+ * and returns.
+ */
+Return<void> HalDisplay::getDisplayInfo(getDisplayInfo_cb _hidl_cb) {
+ if (mHwDisplay) {
+ mHwDisplay->getDisplayInfo(_hidl_cb);
+ }
+
+ return Void();
+}
+
+/**
+ * Sets the display state as what the clients wants.
+ */
+Return<EvsResult> HalDisplay::setDisplayState(DisplayState state) {
+ if (mHwDisplay) {
+ return mHwDisplay->setDisplayState(state);
+ } else {
+ return EvsResult::UNDERLYING_SERVICE_ERROR;
+ }
+}
+
+/**
+ * Gets current display state from a hardware display object and return.
+ */
+Return<DisplayState> HalDisplay::getDisplayState() {
+ if (mHwDisplay) {
+ return mHwDisplay->getDisplayState();
+ } else {
+ return DisplayState::DEAD;
+ }
+}
+
+/**
+ * Returns a handle to a frame buffer associated with the display.
+ */
+Return<void> HalDisplay::getTargetBuffer(getTargetBuffer_cb _hidl_cb) {
+ if (mHwDisplay) {
+ mHwDisplay->getTargetBuffer(_hidl_cb);
+ }
+
+ return Void();
+}
+
+/**
+ * Notifies the display that the buffer is ready to be used.
+ */
+Return<EvsResult> HalDisplay::returnTargetBufferForDisplay(const BufferDesc& buffer) {
+ if (mHwDisplay) {
+ return mHwDisplay->returnTargetBufferForDisplay(buffer);
+ } else {
+ return EvsResult::OWNERSHIP_LOST;
+ }
+}
+
+} // namespace implementation
+} // namespace V1_0
+} // namespace evs
+} // namespace automotive
+} // namespace android
diff --git a/evs/manager/HalDisplay.h b/evs/manager/HalDisplay.h
new file mode 100644
index 0000000..539c9aa
--- /dev/null
+++ b/evs/manager/HalDisplay.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#ifndef ANDROID_AUTOMOTIVE_EVS_V1_0_DISPLAYPROXY_H
+#define ANDROID_AUTOMOTIVE_EVS_V1_0_DISPLAYPROXY_H
+
+#include <android/hardware/automotive/evs/1.0/types.h>
+#include <android/hardware/automotive/evs/1.0/IEvsDisplay.h>
+
+using namespace ::android::hardware::automotive::evs::V1_0;
+using ::android::hardware::Return;
+using ::android::hardware::Void;
+using ::android::hardware::hidl_handle;
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_0 {
+namespace implementation {
+
+// TODO: This class has been defined to wrap the IEvsDisplay object the driver
+// returns because of b/129284474 and represents an EVS display to the client
+// application. With a proper bug fix, we may remove this class and update the
+// manager directly to use the IEvsDisplay object the driver provides.
+class HalDisplay : public IEvsDisplay {
+public:
+ explicit HalDisplay(sp<IEvsDisplay>& display);
+ virtual ~HalDisplay() override;
+
+ inline void shutdown();
+ sp<IEvsDisplay> getHwDisplay();
+
+ // Methods from ::android::hardware::automotive::evs::V1_0::IEvsDisplay follow.
+ Return<void> getDisplayInfo(getDisplayInfo_cb _hidl_cb) override;
+ Return<EvsResult> setDisplayState(DisplayState state) override;
+ Return<DisplayState> getDisplayState() override;
+ Return<void> getTargetBuffer(getTargetBuffer_cb _hidl_cb) override;
+ Return<EvsResult> returnTargetBufferForDisplay(const BufferDesc& buffer) override;
+
+private:
+ sp<IEvsDisplay> mHwDisplay; // The low level display interface that backs this proxy
+};
+
+} // namespace implementation
+} // namespace V1_0
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
+#endif // ANDROID_AUTOMOTIVE_EVS_V1_0_DISPLAYPROXY_H
diff --git a/evs/manager/VirtualCamera.cpp b/evs/manager/VirtualCamera.cpp
index af90f2c..293f4b4 100644
--- a/evs/manager/VirtualCamera.cpp
+++ b/evs/manager/VirtualCamera.cpp
@@ -45,7 +45,9 @@
// Note that if we hit this case, no terminating frame will be sent to the client,
// but they're probably already dead anyway.
ALOGW("Virtual camera being shutdown while stream is running");
- mStreamState = STOPPED;
+
+ // Tell the frame delivery pipeline we don't want any more frames
+ mStreamState = STOPPING;
if (mFramesHeld.size() > 0) {
ALOGW("VirtualCamera destructing with frames in flight.");
@@ -57,6 +59,9 @@
}
mFramesHeld.clear();
}
+
+ // Give the underlying hardware camera the heads up that it might be time to stop
+ mHalCamera->clientStreamEnding();
}
// Drop our reference to our associated hardware camera
diff --git a/procfs-inspector/server/com.android.car.procfsinspector.rc b/procfs-inspector/server/com.android.car.procfsinspector.rc
index 7faaf69..f069ee1 100644
--- a/procfs-inspector/server/com.android.car.procfsinspector.rc
+++ b/procfs-inspector/server/com.android.car.procfsinspector.rc
@@ -2,6 +2,7 @@
class core
user nobody
group readproc
+ disabled
-on boot && property:boot.car_service_created=1
+on property:boot.car_service_created=1
start com.android.car.procfsinspector
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 76c4d37..8d37772 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -252,6 +252,12 @@
android:description="@string/car_permission_desc_car_cluster_control" />
<permission
+ android:name="android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_label_car_handle_usb_aoap_device"
+ android:description="@string/car_permission_desc_car_handle_usb_aoap_device" />
+
+ <permission
android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"
android:protectionLevel="system|signature"
android:label="@string/car_permission_label_car_ux_restrictions_configuration"
@@ -263,6 +269,12 @@
android:label="@string/car_permission_label_storage_monitoring"
android:description="@string/car_permission_desc_storage_monitoring" />
+ <permission
+ android:name="android.car.permission.CAR_ENROLL_TRUST"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_label_enroll_trust"
+ android:description="@string/car_permission_desc_enroll_trust" />
+
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
@@ -283,6 +295,7 @@
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.LOCATION_HARDWARE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" />
<application android:label="@string/app_title"
android:directBootAware="true"
@@ -297,6 +310,21 @@
</intent-filter>
</service>
<service android:name=".PerUserCarService" android:exported="false" />
+
+ <service
+ android:name="com.android.car.trust.CarBleTrustAgent"
+ android:permission="android.permission.BIND_TRUST_AGENT"
+ android:singleUser="true">
+ <intent-filter>
+ <action android:name="android.service.trust.TrustAgentService" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <!-- Warning: the meta data must be included if the service is direct boot aware.
+ If not included, the device will crash before boot completes. Rendering the
+ device unusable. -->
+ <meta-data android:name="android.service.trust.trustagent"
+ android:resource="@xml/car_trust_agent"/>
+ </service>
<activity android:name="com.android.car.pm.ActivityBlockingActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 48f346e..200c4c3 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -158,4 +158,16 @@
<!-- UI mode for projection activity. See ProjectionOptions class for possible values. -->
<integer name="config_projectionUiMode" translatable="false">0</integer>
+ <!-- service/characteristics uuid for adding new escrow token -->
+ <string name="enrollment_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="enrollment_handle_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="enrollment_token_uuid" translatable="false">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string>
+
+ <!-- service/characteristics uuid for unlocking a device -->
+ <string name="unlock_service_uuid" translatable="false">5e2a68a1-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="unlock_escrow_token_uuid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="unlock_handle_uuid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
+
+ <string name="token_handle_shared_preferences" translatable="false">com.android.car.trust.TOKEN_HANDLE</string>
+
</resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index f74710c..325788b 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -99,6 +99,8 @@
<string name="car_permission_desc_bind_instrument_cluster_rendering">Receive instrument cluster data</string>
<string name="car_permission_label_car_ux_restrictions_configuration">UX Restrictions Configuration</string>
<string name="car_permission_desc_car_ux_restrictions_configuration">Configure UX Restrictions</string>
+ <string name="car_permission_label_car_handle_usb_aoap_device">Communicate with USB device in AOAP mode</string>
+ <string name="car_permission_desc_car_handle_usb_aoap_device">Allows an app to communicate with a device in AOAP mode</string>
<!-- Permission text: apps can handle input events [CHAR LIMIT=NONE] -->
<string name="car_permission_label_bind_input_service">Car Input Service</string>
@@ -251,6 +253,9 @@
<!-- Permission text: apps read information of car's power state [CHAR LIMIT=NONE] -->
<string name="car_permission_desc_car_power">Access car power state</string>
+ <string name="car_permission_label_enroll_trust">Enroll Trusted Device</string>
+ <string name="car_permission_desc_enroll_trust">Allow Trusted Device Enrollment</string>
+
<!-- The package name of the media application that will be selected as the default [CHAR LIMIT=NONE] -->
<string name="default_media_application" translatable="false">com.android.bluetooth</string>
</resources>
diff --git a/service/res/xml/car_trust_agent.xml b/service/res/xml/car_trust_agent.xml
new file mode 100644
index 0000000..e53932d
--- /dev/null
+++ b/service/res/xml/car_trust_agent.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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
+ -->
+<trust-agent xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:priv-android="http://schemas.android.com/apk/prv/res/android"
+ android:settingsActivity=".MainActivity"
+ priv-android:unlockProfile="true" />
diff --git a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
index 6b2169a..aa52e73 100644
--- a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
+++ b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
@@ -811,7 +811,7 @@
mPropertyEventListener);
// Get Current restrictions and handle them
handleUxRestrictionsChanged(mUxRService.getCurrentUxRestrictions());
- // Register for future changes to the UxRestrictions
+ // Register for future changes to the DrivingStateRestrictions
mUxRService.registerUxRestrictionsChangeListener(mUxRListener);
mUserServiceHelper.registerServiceCallback(mServiceCallback);
}
diff --git a/service/src/com/android/car/CarLocalServices.java b/service/src/com/android/car/CarLocalServices.java
index 2c619af..fe3dc07 100644
--- a/service/src/com/android/car/CarLocalServices.java
+++ b/service/src/com/android/car/CarLocalServices.java
@@ -17,6 +17,7 @@
package com.android.car;
import android.util.ArrayMap;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,6 +39,7 @@
*/
@SuppressWarnings("unchecked")
public static <T> T getService(Class<T> type) {
+ Log.d("CarLocalServices", " getService " + type.getSimpleName());
synchronized (sLocalServiceObjects) {
return (T) sLocalServiceObjects.get(type);
}
@@ -51,6 +53,7 @@
if (sLocalServiceObjects.containsKey(type)) {
throw new IllegalStateException("Overriding service registration");
}
+ Log.d("CarLocalServices", " Adding " + type.getSimpleName());
sLocalServiceObjects.put(type, service);
}
}
@@ -60,6 +63,7 @@
*/
@VisibleForTesting
public static <T> void removeServiceForTest(Class<T> type) {
+ Log.d("CarLocalServices", " Removing " + type.getSimpleName());
synchronized (sLocalServiceObjects) {
sLocalServiceObjects.remove(type);
}
@@ -69,6 +73,7 @@
* Remove all registered services. Should be called when car service restarts.
*/
public static void removeAllServices() {
+ Log.d("CarLocalServices", " removeAllServices");
synchronized (sLocalServiceObjects) {
sLocalServiceObjects.clear();
}
diff --git a/service/src/com/android/car/CarService.java b/service/src/com/android/car/CarService.java
index 47d3137..509ecdd 100644
--- a/service/src/com/android/car/CarService.java
+++ b/service/src/com/android/car/CarService.java
@@ -21,9 +21,7 @@
import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
-import android.content.pm.PackageManager;
import android.hardware.automotive.vehicle.V2_0.IVehicle;
-import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.IHwBinder.DeathRecipient;
@@ -34,7 +32,6 @@
import android.util.Log;
import com.android.car.systeminterface.SystemInterface;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.RingBufferIndices;
@@ -95,11 +92,11 @@
mCanBusErrorNotifier,
mVehicleInterfaceName);
mICarImpl.init();
- SystemProperties.set("boot.car_service_created", "1");
linkToDeath(mVehicle, mVehicleDeathRecipient);
ServiceManager.addService("car_service", mICarImpl);
+ SystemProperties.set("boot.car_service_created", "1");
super.onCreate();
}
diff --git a/service/src/com/android/car/CarUxRestrictionsManagerService.java b/service/src/com/android/car/CarUxRestrictionsManagerService.java
index 0fc83dc..247cb6c 100644
--- a/service/src/com/android/car/CarUxRestrictionsManagerService.java
+++ b/service/src/com/android/car/CarUxRestrictionsManagerService.java
@@ -16,6 +16,8 @@
package com.android.car;
+import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_BASELINE;
+
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import android.annotation.Nullable;
@@ -24,6 +26,7 @@
import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.car.drivingstate.CarUxRestrictionsManager;
import android.car.drivingstate.ICarDrivingStateChangeListener;
import android.car.drivingstate.ICarUxRestrictionsChangeListener;
import android.car.drivingstate.ICarUxRestrictionsManager;
@@ -95,7 +98,10 @@
private final CarPropertyService mCarPropertyService;
// List of clients listening to UX restriction events.
private final List<UxRestrictionsClient> mUxRClients = new ArrayList<>();
- private CarUxRestrictionsConfiguration mCarUxRestrictionsConfiguration;
+ @VisibleForTesting
+ CarUxRestrictionsConfiguration mCarUxRestrictionsConfiguration;
+ @CarUxRestrictionsManager.UxRestrictionMode
+ private int mRestrictionMode = UX_RESTRICTION_MODE_BASELINE;
private CarUxRestrictions mCurrentUxRestrictions;
private float mCurrentMovingSpeed;
// Flag to disable broadcasting UXR changes - for development purposes
@@ -359,6 +365,42 @@
}
/**
+ * Sets the restriction mode to use. Restriction mode allows a different set of restrictions to
+ * be applied in the same driving state. Restrictions for each mode can be configured through
+ * {@link CarUxRestrictionsConfiguration}.
+ *
+ * <p>Defaults to {@link CarUxRestrictionsManager#UX_RESTRICTION_MODE_BASELINE}.
+ *
+ * @param mode See values in {@link CarUxRestrictionsManager.UxRestrictionMode}.
+ * @return {@code true} if mode was successfully changed; {@code false} otherwise.
+ *
+ * @see CarUxRestrictionsConfiguration.DrivingStateRestrictions
+ * @see CarUxRestrictionsConfiguration.Builder
+ */
+ @Override
+ public synchronized boolean setRestrictionMode(
+ @CarUxRestrictionsManager.UxRestrictionMode int mode) {
+ if (mRestrictionMode == mode) {
+ return true;
+ }
+
+ addTransitionLog(TAG, mRestrictionMode, mode, System.currentTimeMillis(),
+ "Restriction mode");
+ mRestrictionMode = mode;
+ logd("Set restriction mode to: " + CarUxRestrictionsManager.modeToString(mode));
+
+ handleDispatchUxRestrictions(
+ mDrivingStateService.getCurrentDrivingState().eventValue, getCurrentSpeed());
+ return true;
+ }
+
+ @Override
+ @CarUxRestrictionsManager.UxRestrictionMode
+ public synchronized int getRestrictionMode() {
+ return mRestrictionMode;
+ }
+
+ /**
* Writes configuration into the specified file.
*
* IO access on file is not thread safe. Caller should ensure threading protection.
@@ -592,8 +634,8 @@
return;
}
- CarUxRestrictions uxRestrictions =
- mCarUxRestrictionsConfiguration.getUxRestrictions(currentDrivingState, speed);
+ CarUxRestrictions uxRestrictions = mCarUxRestrictionsConfiguration.getUxRestrictions(
+ currentDrivingState, speed, mRestrictionMode);
if (DBG) {
Log.d(TAG, String.format("DO old->new: %b -> %b",
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 2df562e..35255ee 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -42,7 +42,7 @@
import com.android.car.internal.FeatureConfiguration;
import com.android.car.pm.CarPackageManagerService;
import com.android.car.systeminterface.SystemInterface;
-import com.android.car.trust.CarTrustAgentEnrollmentService;
+import com.android.car.trust.CarTrustedDeviceService;
import com.android.car.user.CarUserService;
import com.android.car.vms.VmsBrokerService;
import com.android.car.vms.VmsClientManager;
@@ -86,7 +86,7 @@
private final CarDiagnosticService mCarDiagnosticService;
private final CarStorageMonitoringService mCarStorageMonitoringService;
private final CarConfigurationService mCarConfigurationService;
- private final CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
+ private final CarTrustedDeviceService mCarTrustedDeviceService;
private final CarMediaService mCarMediaService;
private final CarUserManagerHelper mUserManagerHelper;
private final CarUserService mCarUserService;
@@ -156,10 +156,13 @@
new CarConfigurationService(serviceContext, new JsonReaderImpl());
mCarLocationService = new CarLocationService(mContext, mCarPropertyService,
mUserManagerHelper);
- mCarTrustAgentEnrollmentService = new CarTrustAgentEnrollmentService(serviceContext);
+ mCarTrustedDeviceService = new CarTrustedDeviceService(serviceContext);
mCarMediaService = new CarMediaService(serviceContext);
CarLocalServices.addService(CarUserService.class, mCarUserService);
+ Log.d(TAG, "Adding CarTrustedDeviceService");
+ CarLocalServices.addService(CarTrustedDeviceService.class,
+ mCarTrustedDeviceService);
CarLocalServices.addService(SystemInterface.class, mSystemInterface);
// Be careful with order. Service depending on other service should be inited later.
@@ -187,7 +190,7 @@
allServices.add(mVmsClientManager);
allServices.add(mVmsSubscriberService);
allServices.add(mVmsPublisherService);
- allServices.add(mCarTrustAgentEnrollmentService);
+ allServices.add(mCarTrustedDeviceService);
allServices.add(mCarMediaService);
allServices.add(mCarLocationService);
mAllServices = allServices.toArray(new CarServiceBase[allServices.size()]);
@@ -301,7 +304,7 @@
return mCarConfigurationService;
case Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE:
assertTrustAgentEnrollmentPermission(mContext);
- return mCarTrustAgentEnrollmentService;
+ return mCarTrustedDeviceService.getCarTrustAgentEnrollmentService();
case Car.CAR_MEDIA_SERVICE:
return mCarMediaService;
default:
@@ -580,6 +583,7 @@
break;
}
mCarProjectionService.setUiMode(Integer.valueOf(args[1]));
+ break;
default:
writer.println("Unknown command: \"" + arg + "\"");
dumpHelp(writer);
diff --git a/service/src/com/android/car/Utils.java b/service/src/com/android/car/Utils.java
index 284e610..70e1d7c 100644
--- a/service/src/com/android/car/Utils.java
+++ b/service/src/com/android/car/Utils.java
@@ -1,8 +1,25 @@
+/*
+ * Copyright (C) 2019 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 com.android.car;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import java.nio.ByteBuffer;
+
/**
* Some potentially useful static methods.
*/
@@ -17,7 +34,7 @@
}
static String getProfileName(int profile) {
- switch(profile) {
+ switch (profile) {
case BluetoothProfile.A2DP_SINK:
return "A2DP_SINK";
case BluetoothProfile.HEADSET_CLIENT:
@@ -46,7 +63,6 @@
* <p>
* A specific service in CarService can choose to use a circular buffer of N records to keep
* track of the last N transitions.
- *
*/
public static class TransitionLog {
private String mServiceName; // name of the service or tag
@@ -78,4 +94,43 @@
: "") + " changed from " + mFromState + " to " + mToState;
}
}
+
+ /**
+ * Returns a byte buffer corresponding to the passed long argument.
+ *
+ * @param primitive data to convert format.
+ */
+ public static byte[] longToBytes(long primitive) {
+ ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
+ buffer.putLong(primitive);
+ return buffer.array();
+ }
+
+ /**
+ * Returns a byte buffer corresponding to the passed long argument.
+ *
+ * @param array data to convert format.
+ */
+ public static long bytesToLong(byte[] array) {
+ ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
+ buffer.put(array);
+ buffer.flip();
+ long value = buffer.getLong();
+ return value;
+ }
+
+ /**
+ * Returns a String in Hex format that is formed from the bytes in the byte array
+ * Useful for debugging
+ *
+ * @param array the byte array
+ * @return the Hex string version of the input byte array
+ */
+ public static String byteArrayToHexString(byte[] array) {
+ StringBuilder sb = new StringBuilder(array.length * 2);
+ for (byte b : array) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
}
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
new file mode 100644
index 0000000..1d96f93
--- /dev/null
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.trust;
+
+import static android.bluetooth.BluetoothProfile.GATT_SERVER;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.car.Utils;
+
+/**
+ * A generic class that manages BLE operations like start/stop advertising, notifying connects/
+ * disconnects and reading/writing values to GATT characteristics.
+ *
+ * TODO(b/123248433) This could move to a separate comms library.
+ */
+public abstract class BleManager {
+ private static final String TAG = BleManager.class.getSimpleName();
+
+ private static final int BLE_RETRY_LIMIT = 5;
+ private static final int BLE_RETRY_INTERVAL_MS = 1000;
+
+ private final Handler mHandler = new Handler();
+
+ private final Context mContext;
+ private BluetoothManager mBluetoothManager;
+ private BluetoothLeAdvertiser mAdvertiser;
+ private BluetoothGattServer mGattServer;
+ private int mAdvertiserStartCount;
+
+ BleManager(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Starts the GATT server with the given {@link BluetoothGattService} and begins
+ * advertising.
+ *
+ * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
+ * Therefore, several retries will be made to ensure advertising is started.
+ *
+ * @param service {@link BluetoothGattService} that will be discovered by clients
+ */
+ protected void startAdvertising(BluetoothGattService service,
+ AdvertiseCallback advertiseCallback) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "startAdvertising: " + service.getUuid().toString());
+ }
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ Log.e(TAG, "System does not support BLE");
+ return;
+ }
+
+ // Only open one Gatt server.
+ if (mGattServer == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Opening a new GATT Server");
+ }
+ mBluetoothManager = (BluetoothManager) mContext.getSystemService(
+ Context.BLUETOOTH_SERVICE);
+ mGattServer = mBluetoothManager.openGattServer(mContext, mGattServerCallback);
+
+ if (mGattServer == null) {
+ Log.e(TAG, "Gatt Server not created");
+ return;
+ }
+ }
+
+ mGattServer.clearServices();
+ mGattServer.addService(service);
+
+ AdvertiseSettings settings = new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .setConnectable(true)
+ .build();
+
+ AdvertiseData data = new AdvertiseData.Builder()
+ .setIncludeDeviceName(true)
+ .addServiceUuid(new ParcelUuid(service.getUuid()))
+ .build();
+
+ mAdvertiserStartCount = 0;
+ startAdvertisingInternally(settings, data, advertiseCallback);
+ }
+
+ private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data,
+ AdvertiseCallback advertiseCallback) {
+ if (BluetoothAdapter.getDefaultAdapter() != null) {
+ mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+ }
+
+ if (mAdvertiser != null) {
+ mAdvertiser.startAdvertising(settings, data, advertiseCallback);
+ mAdvertiserStartCount = 0;
+ } else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
+ mHandler.postDelayed(
+ () -> startAdvertisingInternally(settings, data, advertiseCallback),
+ BLE_RETRY_INTERVAL_MS);
+ mAdvertiserStartCount += 1;
+ } else {
+ Log.e(TAG, "Cannot start BLE Advertisement. BT Adapter: "
+ + BluetoothAdapter.getDefaultAdapter() + " Advertise Retry count: "
+ + mAdvertiserStartCount);
+ }
+ }
+
+ protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
+ if (mAdvertiser != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "stopAdvertising: ");
+ }
+ mAdvertiser.stopAdvertising(advertiseCallback);
+ }
+ }
+
+ /**
+ * Notifies the characteristic change via {@link BluetoothGattServer}
+ */
+ protected void notifyCharacteristicChanged(BluetoothDevice device,
+ BluetoothGattCharacteristic characteristic, boolean confirm) {
+ if (mGattServer != null) {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+ }
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Cleans up the BLE GATT server state.
+ */
+ void cleanup() {
+ // Stops the advertiser and GATT server. This needs to be done to avoid leaks
+ if (mAdvertiser != null) {
+ mAdvertiser.cleanup();
+ }
+
+ if (mGattServer != null) {
+ mGattServer.clearServices();
+ try {
+ for (BluetoothDevice d : mBluetoothManager.getConnectedDevices(GATT_SERVER)) {
+ mGattServer.cancelConnection(d);
+ }
+ } catch (UnsupportedOperationException e) {
+ Log.e(TAG, "Error getting connected devices", e);
+ } finally {
+ stopGattServer();
+ }
+ }
+ }
+
+ /**
+ * Close the GATT Server
+ */
+ void stopGattServer() {
+ if (mGattServer == null) {
+ return;
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "stopGattServer");
+ }
+ mGattServer.close();
+ mGattServer = null;
+ }
+
+ /**
+ * Triggered when a device (GATT client) connected.
+ *
+ * @param device Remote device that connected on BLE.
+ */
+ protected void onRemoteDeviceConnected(BluetoothDevice device) {
+ }
+
+ /**
+ * Triggered when a device (GATT client) disconnected.
+ *
+ * @param device Remote device that disconnected on BLE.
+ */
+ protected void onRemoteDeviceDisconnected(BluetoothDevice device) {
+ }
+
+ /**
+ * Triggered when this BleManager receives a write request from a remote
+ * device. Sub-classes should implement how to handle requests.
+ * <p>
+ *
+ * @see BluetoothGattServerCallback#onCharacteristicWriteRequest(BluetoothDevice, int,
+ * BluetoothGattCharacteristic, boolean, boolean, int, byte[])
+ */
+ protected abstract void onCharacteristicWrite(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
+ responseNeeded, int offset, byte[] value);
+
+ /**
+ * Triggered when this BleManager receives a read request from a remote device.
+ * <p>
+ *
+ * @see BluetoothGattServerCallback#onCharacteristicReadRequest(BluetoothDevice, int, int,
+ * BluetoothGattCharacteristic)
+ */
+ protected abstract void onCharacteristicRead(BluetoothDevice device,
+ int requestId, int offset, BluetoothGattCharacteristic characteristic);
+
+ private final BluetoothGattServerCallback mGattServerCallback =
+ new BluetoothGattServerCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status,
+ int newState) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "BLE Connection State Change: " + newState);
+ }
+ switch (newState) {
+ case BluetoothProfile.STATE_CONNECTED:
+ onRemoteDeviceConnected(device);
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ onRemoteDeviceDisconnected(device);
+ break;
+ default:
+ Log.w(TAG,
+ "Connection state not connecting or disconnecting; ignoring: "
+ + newState);
+ }
+ }
+
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "Service added status: " + status + " uuid: " + service.getUuid());
+ }
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattCharacteristic characteristic) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
+ }
+
+ mGattServer.sendResponse(device, requestId,
+ BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
+ onCharacteristicRead(device, requestId, offset, characteristic);
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic, boolean preparedWrite,
+ boolean responseNeeded, int offset, byte[] value) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid()
+ + "value: " + Utils.byteArrayToHexString(value));
+ }
+
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+ offset, value);
+ onCharacteristicWrite(device, requestId, characteristic,
+ preparedWrite, responseNeeded, offset, value);
+ }
+ };
+}
diff --git a/service/src/com/android/car/trust/BleService.java b/service/src/com/android/car/trust/BleService.java
deleted file mode 100644
index 3ec1dca..0000000
--- a/service/src/com/android/car/trust/BleService.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2019 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 com.android.car.trust;
-
-import static android.bluetooth.BluetoothProfile.GATT_SERVER;
-
-import android.app.Service;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattServer;
-import android.bluetooth.BluetoothGattServerCallback;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-/**
- * A generic service to start a BLE
- * TODO(b/123248433) This could move to a separate comms library.
- */
-public abstract class BleService extends Service {
- private static final String TAG = BleService.class.getSimpleName();
-
- private static final int BLE_RETRY_LIMIT = 5;
- private static final int BLE_RETRY_INTERVAL_MS = 1000;
-
- private final Handler mHandler = new Handler();
-
- private BluetoothManager mBluetoothManager;
- private BluetoothLeAdvertiser mAdvertiser;
- private BluetoothGattServer mGattServer;
- private int mAdvertiserStartCount;
-
- /**
- * Starts the GATT server with the given {@link BluetoothGattService} and begins
- * advertising.
- *
- * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
- * Therefore, several retries will be made to ensure advertising is started.
- *
- * @param service {@link BluetoothGattService} that will be discovered by clients
- */
- protected void startAdvertising(BluetoothGattService service,
- AdvertiseCallback advertiseCallback) {
- if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
- Log.e(TAG, "System does not support BLE");
- return;
- }
-
- // Only open one Gatt server.
- if (mGattServer == null) {
- mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
- mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
-
- if (mGattServer == null) {
- Log.e(TAG, "Gatt Server not created");
- return;
- }
- }
-
- mGattServer.clearServices();
- mGattServer.addService(service);
-
- AdvertiseSettings settings = new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
- .setConnectable(true)
- .build();
-
- AdvertiseData data = new AdvertiseData.Builder()
- .setIncludeDeviceName(true)
- .addServiceUuid(new ParcelUuid(service.getUuid()))
- .build();
-
- mAdvertiserStartCount = 0;
- startAdvertisingInternally(settings, data, advertiseCallback);
- }
-
- private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data,
- AdvertiseCallback advertiseCallback) {
- mAdvertiserStartCount += 1;
- mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
- if (mAdvertiser == null && mAdvertiserStartCount < BLE_RETRY_LIMIT) {
- mHandler.postDelayed(
- () -> startAdvertisingInternally(settings, data, advertiseCallback),
- BLE_RETRY_INTERVAL_MS);
- } else {
- mHandler.removeCallbacks(null);
- mAdvertiser.startAdvertising(settings, data, advertiseCallback);
- mAdvertiserStartCount = 0;
- }
- }
-
- protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
- if (mAdvertiser != null) {
- mAdvertiser.stopAdvertising(advertiseCallback);
- }
- }
-
- /**
- * Notifies the characteristic change via {@link BluetoothGattServer}
- */
- protected void notifyCharacteristicChanged(BluetoothDevice device,
- BluetoothGattCharacteristic characteristic, boolean confirm) {
- if (mGattServer != null) {
- mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
- }
- }
-
- @Override
- public void onDestroy() {
- // Stops the advertiser and GATT server. This needs to be done to avoid leaks
- if (mAdvertiser != null) {
- mAdvertiser.cleanup();
- }
-
- if (mGattServer != null) {
- mGattServer.clearServices();
- try {
- for (BluetoothDevice d : mBluetoothManager.getConnectedDevices(GATT_SERVER)) {
- mGattServer.cancelConnection(d);
- }
- } catch (UnsupportedOperationException e) {
- Log.e(TAG, "Error getting connected devices", e);
- } finally {
- mGattServer.close();
- }
- }
- super.onDestroy();
- }
-
- // Delegate to subclass
- protected void onAdvertiseStartSuccess() { }
- protected void onAdvertiseStartFailure(int errorCode) { }
- protected void onAdvertiseDeviceConnected(BluetoothDevice device) { }
- protected void onAdvertiseDeviceDisconnected(BluetoothDevice device) { }
-
- /**
- * Triggered when this BleService receives a write request from a remote
- * device. Sub-classes should implement how to handle requests.
- */
- protected abstract void onCharacteristicWrite(BluetoothDevice device, int requestId,
- BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
- responseNeeded, int offset, byte[] value);
-
- /**
- * Triggered when this BleService receives a read request from a remote device.
- */
- protected abstract void onCharacteristicRead(BluetoothDevice device,
- int requestId, int offset, BluetoothGattCharacteristic characteristic);
-
- private final BluetoothGattServerCallback mGattServerCallback =
- new BluetoothGattServerCallback() {
- @Override
- public void onConnectionStateChange(BluetoothDevice device,
- final int status, final int newState) {
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- onAdvertiseDeviceConnected(device);
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- onAdvertiseDeviceDisconnected(device);
- break;
- default:
- Log.w(TAG, "Connection state not connecting or disconnecting; ignoring: "
- + newState);
- }
- }
-
- @Override
- public void onServiceAdded(final int status, BluetoothGattService service) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
- }
- }
-
- @Override
- public void onCharacteristicReadRequest(BluetoothDevice device,
- int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
- }
-
- mGattServer.sendResponse(device, requestId,
- BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
- onCharacteristicRead(device, requestId, offset, characteristic);
- }
-
- @Override
- public void onCharacteristicWriteRequest(final BluetoothDevice device, int requestId,
- BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
- responseNeeded, int offset, byte[] value) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid());
- }
-
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
- offset, value);
- onCharacteristicWrite(device, requestId, characteristic,
- preparedWrite, responseNeeded, offset, value);
- }
- };
-}
diff --git a/service/src/com/android/car/trust/CarBleTrustAgent.java b/service/src/com/android/car/trust/CarBleTrustAgent.java
new file mode 100644
index 0000000..9ea1d42
--- /dev/null
+++ b/service/src/com/android/car/trust/CarBleTrustAgent.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.trust;
+
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.trust.TrustAgentService;
+import android.util.Log;
+
+import com.android.car.CarLocalServices;
+import com.android.car.Utils;
+import com.android.car.trust.CarTrustAgentEnrollmentService.CarTrustAgentEnrollmentRequestDelegate;
+import com.android.car.trust.CarTrustAgentUnlockService.CarTrustAgentUnlockDelegate;
+
+/**
+ * A BluetoothLE (BLE) based {@link TrustAgentService} that uses the escrow token unlock APIs.
+ * <p>
+ * This trust agent runs during direct boot and interacts with {@link CarTrustedDeviceService}
+ * to listen for remote devices to trigger an unlock.
+ * <p>
+ * The system {@link com.android.server.trust.TrustManagerService} binds to this agent and uses
+ * the data it receives from this agent to authorize a user in lieu of the PIN/Pattern/Password
+ * credentials.
+ */
+public class CarBleTrustAgent extends TrustAgentService {
+ private static final String TAG = CarBleTrustAgent.class.getSimpleName();
+ private boolean mIsDeviceLocked;
+ private CarTrustedDeviceService mCarTrustedDeviceService;
+ private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
+ private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
+
+ @Override
+ public void onCreate() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate()");
+ }
+ super.onCreate();
+ // Registering for more granular BLE specific state changes as against Bluetooth state
+ // changes, helps with reducing latency in getting notified.
+ IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
+ registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
+
+ // TODO(b/129144535) handle scenarios where CarService crashed. Maybe retrieve this
+ // every time we need instead of caching.
+ mCarTrustedDeviceService = CarLocalServices.getService(CarTrustedDeviceService.class);
+ if (mCarTrustedDeviceService == null) {
+ Log.e(TAG, "Cannot retrieve the Trusted device Service");
+ return;
+ }
+ mCarTrustAgentEnrollmentService =
+ mCarTrustedDeviceService.getCarTrustAgentEnrollmentService();
+ setEnrollmentRequestDelegate();
+ mCarTrustAgentUnlockService = mCarTrustedDeviceService.getCarTrustAgentUnlockService();
+ setUnlockRequestDelegate();
+ setManagingTrust(true);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Car Trust agent shutting down");
+ }
+ super.onDestroy();
+ mCarTrustAgentEnrollmentService = null;
+ if (mBluetoothBroadcastReceiver != null) {
+ unregisterReceiver(mBluetoothBroadcastReceiver);
+ }
+ }
+
+ // Overriding TrustAgentService methods
+ @Override
+ public void onDeviceLocked() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onDeviceLocked Current user: " + ActivityManager.getCurrentUser());
+ }
+ super.onDeviceLocked();
+ mIsDeviceLocked = true;
+ if (BluetoothAdapter.getDefaultAdapter().getState() == BluetoothAdapter.STATE_OFF) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Not starting Unlock Advertising yet, since Bluetooth Adapter is off");
+ }
+ return;
+ }
+ if (mCarTrustAgentUnlockService != null) {
+ mCarTrustAgentUnlockService.startUnlockAdvertising();
+ }
+ }
+
+ @Override
+ public void onDeviceUnlocked() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onDeviceUnlocked Current user: " + ActivityManager.getCurrentUser());
+ }
+ super.onDeviceUnlocked();
+ mIsDeviceLocked = false;
+ if (BluetoothAdapter.getDefaultAdapter().getState() == BluetoothAdapter.STATE_OFF) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Not stopping Unlock Advertising, since Bluetooth Adapter is off");
+ }
+ return;
+ }
+ if (mCarTrustAgentUnlockService != null) {
+ mCarTrustAgentUnlockService.stopUnlockAdvertising();
+
+ }
+ revokeTrust();
+ }
+
+ @Override
+ public void onEscrowTokenRemoved(long handle, boolean successful) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenRemoved handle: " + Long.toHexString(handle));
+ }
+ if (mCarTrustAgentEnrollmentService == null) {
+ return;
+ }
+ if (successful) {
+ mCarTrustAgentEnrollmentService.onEscrowTokenRemoved(handle,
+ ActivityManager.getCurrentUser());
+ }
+ }
+
+ @Override
+ public void onEscrowTokenStateReceived(long handle, int tokenState) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenStateReceived: " + Long.toHexString(handle) + " state: "
+ + tokenState);
+ }
+ if (mCarTrustAgentEnrollmentService == null) {
+ return;
+ }
+ mCarTrustAgentEnrollmentService.onEscrowTokenActiveStateChanged(handle,
+ tokenState == TOKEN_STATE_ACTIVE);
+ }
+
+ @Override
+ public void onEscrowTokenAdded(byte[] token, long handle, UserHandle user) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenAdded handle: " + Long.toHexString(handle) + "token: "
+ + Utils.byteArrayToHexString(token));
+ }
+ if (mCarTrustAgentEnrollmentService == null) {
+ return;
+ }
+ mCarTrustAgentEnrollmentService.onEscrowTokenAdded(token, handle, user.getIdentifier());
+ }
+
+ private void setEnrollmentRequestDelegate() {
+ if (mCarTrustAgentEnrollmentService == null) {
+ return;
+ }
+ mCarTrustAgentEnrollmentService.setEnrollmentRequestDelegate(mEnrollDelegate);
+ }
+
+ private void setUnlockRequestDelegate() {
+ if (mCarTrustAgentUnlockService == null) {
+ return;
+ }
+ mCarTrustAgentUnlockService.setUnlockRequestDelegate(mUnlockDelegate);
+ }
+
+ private void unlockUserInternally(int uid, byte[] token, long handle) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "About to unlock user: " + uid);
+ UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (um.isUserUnlocked(UserHandle.of(uid))) {
+ Log.d(TAG, "User currently unlocked");
+ } else {
+ Log.d(TAG, "User currently locked");
+ }
+ }
+ unlockUserWithToken(handle, token, UserHandle.of(uid));
+ grantTrust("Granting trust from escrow token",
+ 0, FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
+ }
+
+ private final BroadcastReceiver mBluetoothBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null && BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(
+ intent.getAction())) {
+ onBluetoothStateChanged(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1));
+ }
+ }
+ };
+
+ private void onBluetoothStateChanged(int state) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onBluetoothStateChanged: " + state);
+ }
+ if (!mIsDeviceLocked) {
+ return;
+ }
+ switch (state) {
+ case BluetoothAdapter.STATE_BLE_ON:
+ if (mCarTrustAgentUnlockService != null) {
+ mCarTrustAgentUnlockService.startUnlockAdvertising();
+ }
+ break;
+ case BluetoothAdapter.STATE_OFF:
+ Log.e(TAG, "Bluetooth Adapter Off in lock screen");
+ if (mCarTrustedDeviceService != null) {
+ mCarTrustedDeviceService.cleanupBleService();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Implementing Delegates for Enrollment and Unlock. The CarBleTrustAgent acts as the interface
+ // between the Trust Agent framework and the Car Service. The Car service handles communicating
+ // with the peer device part and the framework handles the actual authentication. The
+ // CarBleTrustAgent abstracts these 2 pieces from each other.
+ /**
+ * Implementation of the {@link CarTrustAgentEnrollmentRequestDelegate}
+ */
+ private final CarTrustAgentEnrollmentRequestDelegate mEnrollDelegate =
+ new CarTrustAgentEnrollmentRequestDelegate() {
+ @Override
+ public void addEscrowToken(byte[] token, int uid) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "addEscrowToken. uid: " + uid + " token: "
+ + Utils.byteArrayToHexString(
+ token));
+ }
+ CarBleTrustAgent.this.addEscrowToken(token, UserHandle.of(uid));
+ }
+
+ @Override
+ public void removeEscrowToken(long handle, int uid) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "removeEscrowToken. uid: " + ActivityManager.getCurrentUser()
+ + " handle: " + handle);
+ }
+ CarBleTrustAgent.this.removeEscrowToken(handle,
+ UserHandle.of(uid));
+ }
+
+ @Override
+ public void isEscrowTokenActive(long handle, int uid) {
+ CarBleTrustAgent.this.isEscrowTokenActive(handle, UserHandle.of(uid));
+ }
+ };
+
+ /**
+ * Implementation of the {@link CarTrustAgentUnlockDelegate}
+ */
+ private final CarTrustAgentUnlockDelegate mUnlockDelegate = new CarTrustAgentUnlockDelegate() {
+ /**
+ * Pass the user and token credentials to authenticate with the LockSettingsService.
+ *
+ * @param user user being authorized
+ * @param token escrow token for the user
+ * @param handle the handle corresponding to the escrow token
+ */
+ @Override
+ public void onUnlockDataReceived(int user, byte[] token, long handle) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onUnlockDataReceived:" + user + " token: " + Long.toHexString(
+ Utils.bytesToLong(token)) + " handle: " + Long.toHexString(handle));
+ }
+ if (ActivityManager.getCurrentUser() != user) {
+ // Current behavior is to only authenticate the user we have booted into.
+ // TODO(b/129029418) Make identification & Auth vs Auth-only a
+ // configurable option
+ Log.e(TAG, "Expected User: " + ActivityManager.getCurrentUser()
+ + " Presented User: " + user);
+ return;
+ } else {
+ unlockUserInternally(user, token, handle);
+ }
+
+ }
+ };
+}
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
new file mode 100644
index 0000000..9b3db66
--- /dev/null
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.trust;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.car.CarLocalServices;
+import com.android.car.R;
+import com.android.car.Utils;
+
+import java.util.UUID;
+
+/**
+ * A BLE Service that is used for communicating with the trusted peer device. This extends from a
+ * more generic {@link BLEManager} and has more context on the BLE requirements for the Trusted
+ * device feature. It has knowledge on the GATT services and characteristics that are specific to
+ * the Trusted Device feature.
+ */
+class CarTrustAgentBleManager extends BleManager {
+ private static final String TAG = "CarTrustBLEManager";
+ private static final String CONFIRMATION_SIGNAL = "True";
+ private CarTrustedDeviceService mCarTrustedDeviceService;
+ private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
+ private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
+
+ // Enrollment Service and Characteristic UUIDs
+ private UUID mEnrollmentServiceUuid;
+ private UUID mEnrollmentEscrowTokenUuid;
+ private UUID mEnrollmentTokenHandleUuid;
+ private BluetoothGattService mEnrollmentGattService;
+
+ // Unlock Service and Characteristic UUIDs
+ private UUID mUnlockServiceUuid;
+ private UUID mUnlockEscrowTokenUuid;
+ private UUID mUnlockTokenHandleUuid;
+ private BluetoothGattService mUnlockGattService;
+
+ CarTrustAgentBleManager(Context context) {
+ super(context);
+ }
+
+ // Overriding some of the {@link BLEManager} methods to be specific for Trusted Device feature.
+ @Override
+ public void onRemoteDeviceConnected(BluetoothDevice device) {
+ if (getTrustedDeviceService() != null) {
+ getTrustedDeviceService().onRemoteDeviceConnected(device);
+ }
+ }
+
+ @Override
+ public void onRemoteDeviceDisconnected(BluetoothDevice device) {
+ if (getTrustedDeviceService() != null) {
+ getTrustedDeviceService().onRemoteDeviceDisconnected(device);
+ }
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
+ responseNeeded, int offset, byte[] value) {
+ UUID uuid = characteristic.getUuid();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
+ }
+ if (uuid.equals(mEnrollmentEscrowTokenUuid)) {
+ if (getEnrollmentService() != null) {
+ getEnrollmentService().onEnrollmentDataReceived(value);
+ }
+ } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
+ if (getUnlockService() != null) {
+ getUnlockService().onUnlockTokenReceived(value);
+ }
+ } else if (uuid.equals(mUnlockTokenHandleUuid)) {
+ if (getUnlockService() != null) {
+ getUnlockService().onUnlockHandleReceived(value);
+ }
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothDevice device,
+ int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
+ // Ignored read requests.
+ }
+
+ @Nullable
+ private CarTrustedDeviceService getTrustedDeviceService() {
+ if (mCarTrustedDeviceService == null) {
+ mCarTrustedDeviceService = CarLocalServices.getService(
+ CarTrustedDeviceService.class);
+ }
+ return mCarTrustedDeviceService;
+ }
+
+ @Nullable
+ private CarTrustAgentEnrollmentService getEnrollmentService() {
+ if (mCarTrustAgentEnrollmentService != null) {
+ return mCarTrustAgentEnrollmentService;
+ }
+
+ if (getTrustedDeviceService() != null) {
+ mCarTrustAgentEnrollmentService =
+ getTrustedDeviceService().getCarTrustAgentEnrollmentService();
+ }
+ return mCarTrustAgentEnrollmentService;
+ }
+
+ @Nullable
+ private CarTrustAgentUnlockService getUnlockService() {
+ if (mCarTrustAgentUnlockService != null) {
+ return mCarTrustAgentUnlockService;
+ }
+
+ if (getTrustedDeviceService() != null) {
+ mCarTrustAgentUnlockService =
+ getTrustedDeviceService().getCarTrustAgentUnlockService();
+ }
+ return mCarTrustAgentUnlockService;
+ }
+
+ /**
+ * Setup the BLE GATT server for Enrollment. The GATT server for Enrollment comprises of
+ * one GATT Service and 2 characteristics - one for the escrow token to be generated and sent
+ * from the phone and the other for the handle generated and sent by the Head unit.
+ */
+ void setupEnrollmentBleServer() {
+ mEnrollmentServiceUuid = UUID.fromString(
+ getContext().getString(R.string.enrollment_service_uuid));
+ mEnrollmentEscrowTokenUuid = UUID.fromString(
+ getContext().getString(R.string.enrollment_token_uuid));
+ mEnrollmentTokenHandleUuid = UUID.fromString(
+ getContext().getString(R.string.enrollment_handle_uuid));
+
+ mEnrollmentGattService = new BluetoothGattService(mEnrollmentServiceUuid,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY);
+
+ // Characteristic to describe the escrow token being used for unlock
+ BluetoothGattCharacteristic tokenCharacteristic = new BluetoothGattCharacteristic(
+ mEnrollmentEscrowTokenUuid,
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ // Characteristic to describe the handle being used for this escrow token
+ BluetoothGattCharacteristic handleCharacteristic = new BluetoothGattCharacteristic(
+ mEnrollmentTokenHandleUuid,
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+ BluetoothGattCharacteristic.PERMISSION_READ);
+
+ mEnrollmentGattService.addCharacteristic(tokenCharacteristic);
+ mEnrollmentGattService.addCharacteristic(handleCharacteristic);
+ }
+
+ /**
+ * Setup the BLE GATT server for Unlocking the Head unit. The GATT server for this phase also
+ * comprises of 1 Service and 2 characteristics. However both the token and the handle are
+ * sent ftrom the phone to the head unit.
+ */
+ void setupUnlockBleServer() {
+ mUnlockServiceUuid = UUID.fromString(
+ getContext().getString(R.string.unlock_service_uuid));
+ mUnlockEscrowTokenUuid = UUID.fromString(
+ getContext().getString(R.string.unlock_escrow_token_uuid));
+ mUnlockTokenHandleUuid = UUID.fromString(
+ getContext().getString(R.string.unlock_handle_uuid));
+
+ mUnlockGattService = new BluetoothGattService(mUnlockServiceUuid,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY);
+
+ // Characteristic to describe the escrow token being used for unlock
+ BluetoothGattCharacteristic tokenCharacteristic = new BluetoothGattCharacteristic(
+ mUnlockEscrowTokenUuid,
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ // Characteristic to describe the handle being used for this escrow token
+ BluetoothGattCharacteristic handleCharacteristic = new BluetoothGattCharacteristic(
+ mUnlockTokenHandleUuid,
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ mUnlockGattService.addCharacteristic(tokenCharacteristic);
+ mUnlockGattService.addCharacteristic(handleCharacteristic);
+ }
+
+ void startEnrollmentAdvertising() {
+ startAdvertising(mEnrollmentGattService, mEnrollmentAdvertisingCallback);
+ }
+
+ void stopEnrollmentAdvertising() {
+ stopAdvertising(mEnrollmentAdvertisingCallback);
+ }
+
+ void startUnlockAdvertising() {
+ startAdvertising(mUnlockGattService, mUnlockAdvertisingCallback);
+ }
+
+ void stopUnlockAdvertising() {
+ stopAdvertising(mUnlockAdvertisingCallback);
+ }
+
+ void disconnectRemoteDevice(BluetoothDevice device) {
+ // TODO(b/129029421) - is closing the GATT Server the right thing to do here?
+ }
+
+ /**
+ * Sends the handle corresponding to the escrow token that was generated by the phone to the
+ * phone.
+ *
+ * @param device the BLE peer device to send the handle to.
+ * @param handle the handle corresponding to the escrow token
+ */
+ void sendEnrollmentHandle(BluetoothDevice device, long handle) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sendEnrollmentHandle: " + Long.toHexString(handle) + " "
+ + device.getAddress());
+ }
+ BluetoothGattCharacteristic enrollmentHandle = mEnrollmentGattService.getCharacteristic(
+ mEnrollmentTokenHandleUuid);
+ enrollmentHandle.setValue(Utils.longToBytes(handle));
+ notifyCharacteristicChanged(device, enrollmentHandle, false);
+ }
+
+ /**
+ * Sends the pairing code confirmation signal to the phone
+ *
+ * @param device the BLE peer device to send the signal to.
+ */
+ void sendPairingCodeConfirmation(BluetoothDevice device) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sendPairingCodeConfirmation: " + device.getAddress());
+ }
+ BluetoothGattCharacteristic confirmation = mEnrollmentGattService.getCharacteristic(
+ mEnrollmentTokenHandleUuid);
+ confirmation.setValue(CONFIRMATION_SIGNAL.getBytes());
+ notifyCharacteristicChanged(device, confirmation, false);
+ }
+
+
+ private final AdvertiseCallback mEnrollmentAdvertisingCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ if (getEnrollmentService() != null) {
+ getEnrollmentService().onEnrollmentAdvertiseStartSuccess();
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Successfully started advertising service");
+ }
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
+
+ super.onStartFailure(errorCode);
+ if (getEnrollmentService() != null) {
+ getEnrollmentService().onEnrollmentAdvertiseStartFailure(errorCode);
+ }
+ }
+ };
+
+ private final AdvertiseCallback mUnlockAdvertisingCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Advertising onStartSuccess");
+ }
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
+ super.onStartFailure(errorCode);
+ }
+ };
+}
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index cd421e7..230c3af 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -17,86 +17,165 @@
package com.android.car.trust;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
import android.car.trust.ICarTrustAgentBleCallback;
import android.car.trust.ICarTrustAgentEnrollment;
import android.car.trust.ICarTrustAgentEnrollmentCallback;
-import android.content.Context;
+import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
-import com.android.car.CarServiceBase;
+import com.android.car.Utils;
+import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
- * A service that enables enrolling a phone as a trusted device for authenticating a user on the
- * IHU. This implements the APIs that an enrollment app can call to conduct an enrollment.
+ * A service that is part of the CarTrustedDeviceService that is responsible for allowing a
+ * phone to enroll as a trusted device. The enrolled phone can then be used for authenticating a
+ * user on the HU. This implements the {@link android.car.trust.CarTrustAgentEnrollmentManager}
+ * APIs that an app like Car Settings can call to conduct an enrollment.
*/
-public class CarTrustAgentEnrollmentService extends ICarTrustAgentEnrollment.Stub implements
- CarServiceBase {
+public class CarTrustAgentEnrollmentService extends ICarTrustAgentEnrollment.Stub {
private static final String TAG = "CarTrustAgentEnroll";
- private final Context mContext;
+ private static final String FAKE_AUTH_STRING = "000000";
+ private final CarTrustedDeviceService mTrustedDeviceService;
// List of clients listening to Enrollment state change events.
private final List<EnrollmentStateClient> mEnrollmentStateClients = new ArrayList<>();
- // List of clients listening to BLE state change events.
+ // List of clients listening to BLE state changes events during enrollment.
private final List<BleStateChangeClient> mBleStateChangeClients = new ArrayList<>();
+ private final CarTrustAgentBleManager mCarTrustAgentBleManager;
+ private CarTrustAgentEnrollmentRequestDelegate mEnrollmentDelegate;
+ private Object mRemoteDeviceLock = new Object();
+ @GuardedBy("mRemoteDeviceLock")
+ private BluetoothDevice mRemoteEnrollmentDevice;
+ @GuardedBy("this")
+ private boolean mEnrollmentHandshakeAccepted;
+ private final Map<Long, Boolean> mTokenActiveState = new HashMap<>();
- public CarTrustAgentEnrollmentService(Context context) {
- mContext = context;
+ public CarTrustAgentEnrollmentService(CarTrustedDeviceService service,
+ CarTrustAgentBleManager bleService) {
+ mTrustedDeviceService = service;
+ mCarTrustAgentBleManager = bleService;
}
- @Override
public synchronized void init() {
+ mCarTrustAgentBleManager.setupEnrollmentBleServer();
}
- @Override
public synchronized void release() {
for (EnrollmentStateClient client : mEnrollmentStateClients) {
client.mListenerBinder.unlinkToDeath(client, 0);
}
+ for (BleStateChangeClient client : mBleStateChangeClients) {
+ client.mListenerBinder.unlinkToDeath(client, 0);
+ }
mEnrollmentStateClients.clear();
+ setEnrollmentHandshakeAccepted(false);
}
+ // Implementing the ICarTrustAgentEnrollment interface
- // Binder methods
- // TODO(b/120911995) The methods don't do anything yet. The implementation will be checked in
- // a follow up CL.
+ /**
+ * Begin BLE advertisement for Enrollment. This should be called from an app that conducts
+ * the enrollment of the trusted device.
+ */
@Override
public void startEnrollmentAdvertising() {
+ // Stop any current broadcasts
+ mTrustedDeviceService.getCarTrustAgentUnlockService().stopUnlockAdvertising();
+ stopEnrollmentAdvertising();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "startEnrollmentAdvertising");
+ }
+ mCarTrustAgentBleManager.startEnrollmentAdvertising();
}
+ /**
+ * Stop BLE advertisement for Enrollment
+ */
@Override
public void stopEnrollmentAdvertising() {
+ mCarTrustAgentBleManager.stopEnrollmentAdvertising();
}
@Override
public void initiateEnrollmentHandshake(BluetoothDevice device) {
+ // TODO(b/129029320) - this is not needed since the IHU plays the server
+ // role and the secure handshake is initiated by the client.
}
+ /**
+ * Called by the client to notify that the user has accepted a pairing code or any out-of-band
+ * confirmation, and send confirmation signals to remote bluetooth device.
+ *
+ * @param device the remote Bluetooth device that will receive the signal.
+ */
@Override
- public void enrollmentHandshakeAccepted() {
+ public void enrollmentHandshakeAccepted(BluetoothDevice device) {
+ mCarTrustAgentBleManager.sendPairingCodeConfirmation(device);
+ setEnrollmentHandshakeAccepted(true);
}
+ /**
+ * Terminate the Enrollment process. To be called when an error is encountered during
+ * enrollment. For example - user pressed cancel on pairing code confirmation or user
+ * navigated away from the app before completing enrollment.
+ */
@Override
public void terminateEnrollmentHandshake() {
+ setEnrollmentHandshakeAccepted(false);
+ // Disconnect from BLE
+ mCarTrustAgentBleManager.disconnectRemoteDevice(mRemoteEnrollmentDevice);
}
+ /**
+ * Returns if there is an active token for the given user and handle.
+ *
+ * @param handle handle corresponding to the escrow token
+ * @param uid user id
+ * @return True if the escrow token is active, false if not
+ */
@Override
public boolean isEscrowTokenActive(long handle, int uid) {
+ if (mTokenActiveState.get(handle) != null) {
+ return mTokenActiveState.get(handle);
+ }
return false;
}
@Override
- public void revokeTrust(long handle) {
+ public void removeEscrowToken(long handle, int uid) {
+ mEnrollmentDelegate.removeEscrowToken(handle, uid);
}
+ /**
+ * Get the Handles corresponding to the token for the current user. The client can use this
+ * to list the trusted devices for the user. This means that the client should maintain a map
+ * of the handles:Bluetooth device names.
+ *
+ * @param uid user id
+ * @return array of handles for the user.
+ */
@Override
public long[] getEnrollmentHandlesForUser(int uid) {
- return new long[0];
+ Set<String> handlesSet = mTrustedDeviceService.getSharedPrefs().getStringSet(
+ String.valueOf(uid),
+ new HashSet<>());
+ long[] handles = new long[handlesSet.size()];
+ int i = 0;
+ for (String handle : handlesSet) {
+ handles[i++] = Long.valueOf(handle);
+ }
+ return handles;
}
/**
@@ -125,6 +204,146 @@
}
}
+ void onEscrowTokenAdded(byte[] token, long handle, int uid) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
+ }
+ mTrustedDeviceService.getSharedPrefs().edit().putInt(String.valueOf(handle), uid).apply();
+ Set<String> handles = mTrustedDeviceService.getSharedPrefs().getStringSet(
+ String.valueOf(uid), new HashSet<>());
+ handles.add(String.valueOf(handle));
+ mTrustedDeviceService.getSharedPrefs().edit().putStringSet(String.valueOf(uid),
+ handles).apply();
+
+ if (mRemoteEnrollmentDevice == null) {
+ Log.e(TAG, "onEscrowTokenAdded() but no remote device connected!");
+ removeEscrowToken(handle, uid);
+ return;
+ }
+ for (EnrollmentStateClient client : mEnrollmentStateClients) {
+ try {
+ client.mListener.onEscrowTokenAdded(handle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onEscrowTokenAdded dispatch failed", e);
+ }
+ }
+ }
+
+ void onEscrowTokenRemoved(long handle, int uid) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenRemoved handle:" + handle + " uid:" + uid);
+ }
+ for (EnrollmentStateClient client : mEnrollmentStateClients) {
+ try {
+ client.mListener.onEscrowTokenRemoved(handle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onEscrowTokenAdded dispatch failed", e);
+ }
+ }
+ SharedPreferences.Editor editor = mTrustedDeviceService.getSharedPrefs().edit();
+ editor.remove(String.valueOf(handle));
+ Set<String> handles = mTrustedDeviceService.getSharedPrefs().getStringSet(
+ String.valueOf(uid), new HashSet<>());
+ handles.remove(String.valueOf(handle));
+ editor.putStringSet(String.valueOf(uid), handles);
+ editor.apply();
+ }
+
+ void onEscrowTokenActiveStateChanged(long handle, boolean isTokenActive) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenActiveStateChanged: " + Long.toHexString(handle));
+ }
+ mTokenActiveState.put(handle, isTokenActive);
+ dispatchEscrowTokenActiveStateChanged(handle, isTokenActive);
+ if (isTokenActive) {
+ mCarTrustAgentBleManager.sendEnrollmentHandle(mRemoteEnrollmentDevice, handle);
+ }
+ }
+
+ void onEnrollmentAdvertiseStartSuccess() {
+ for (BleStateChangeClient client : mBleStateChangeClients) {
+ try {
+ client.mListener.onEnrollmentAdvertisingStarted();
+ } catch (RemoteException e) {
+ Log.e(TAG, "onAdvertiseSuccess dispatch failed", e);
+ }
+ }
+ }
+
+ void onEnrollmentAdvertiseStartFailure(int errorcode) {
+ for (BleStateChangeClient client : mBleStateChangeClients) {
+ try {
+ client.mListener.onEnrollmentAdvertisingFailed(errorcode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onAdvertiseSuccess dispatch failed", e);
+ }
+ }
+ }
+
+ void onRemoteDeviceConnected(BluetoothDevice device) {
+ synchronized (mRemoteDeviceLock) {
+ mRemoteEnrollmentDevice = device;
+ }
+ for (BleStateChangeClient client : mBleStateChangeClients) {
+ try {
+ client.mListener.onBleEnrollmentDeviceConnected(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onAdvertiseSuccess dispatch failed", e);
+ }
+ }
+ //TODO(b/11788064) Fake Authentication to enable clients to go through the enrollment flow.
+ fakeAuthentication();
+ }
+
+ void onRemoteDeviceDisconnected(BluetoothDevice device) {
+ synchronized (mRemoteDeviceLock) {
+ mRemoteEnrollmentDevice = null;
+ }
+ for (BleStateChangeClient client : mBleStateChangeClients) {
+ try {
+ client.mListener.onBleEnrollmentDeviceDisconnected(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onAdvertiseSuccess dispatch failed", e);
+ }
+ }
+ }
+
+ void onEnrollmentDataReceived(byte[] value) {
+ if (mEnrollmentDelegate == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Enrollment Delegate not set");
+ }
+ return;
+ }
+ // The phone is not expected to send any data until the user has accepted the
+ // pairing.
+ if (!mEnrollmentHandshakeAccepted) {
+ Log.e(TAG, "User has not accepted the pairing code yet."
+ + Utils.byteArrayToHexString(value));
+ return;
+ }
+ mEnrollmentDelegate.addEscrowToken(value, ActivityManager.getCurrentUser());
+ }
+
+ // TODO(b/11788064) Fake Authentication until we hook up the crypto lib
+ private void fakeAuthentication() {
+ if (mRemoteEnrollmentDevice == null) {
+ Log.e(TAG, "Remote Device disconnected before Enrollment completed");
+ return;
+ }
+ for (EnrollmentStateClient client : mEnrollmentStateClients) {
+ try {
+ client.mListener.onAuthStringAvailable(mRemoteEnrollmentDevice, FAKE_AUTH_STRING);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onAdvertiseSuccess dispatch failed", e);
+ }
+ }
+ }
+
+ private synchronized void setEnrollmentHandshakeAccepted(boolean accepted) {
+ mEnrollmentHandshakeAccepted = accepted;
+ }
+
/**
* Iterates through the list of registered Enrollment State Change clients -
* {@link EnrollmentStateClient} and finds if the given client is already registered.
@@ -235,6 +454,53 @@
}
/**
+ * The interface that an enrollment delegate has to implement to add/remove escrow tokens.
+ */
+ interface CarTrustAgentEnrollmentRequestDelegate {
+ /**
+ * Add the given escrow token that was generated by the peer device that is being enrolled.
+ *
+ * @param token the 64 bit token
+ * @param uid user id
+ */
+ void addEscrowToken(byte[] token, int uid);
+
+ /**
+ * Remove the given escrow token. This should be called when removing a trusted device.
+ *
+ * @param handle the 64 bit token
+ * @param uid user id
+ */
+ void removeEscrowToken(long handle, int uid);
+
+ /**
+ * Query if the token is active. The result is asynchronously delivered through a callback
+ * {@link CarTrustAgentEnrollmentService#onEscrowTokenActiveStateChanged(long, boolean)}
+ *
+ * @param handle the 64 bit token
+ * @param uid user id
+ */
+ void isEscrowTokenActive(long handle, int uid);
+ }
+
+ void setEnrollmentRequestDelegate(CarTrustAgentEnrollmentRequestDelegate delegate) {
+ mEnrollmentDelegate = delegate;
+ }
+
+ void dump(PrintWriter writer) {
+ }
+
+ private void dispatchEscrowTokenActiveStateChanged(long handle, boolean active) {
+ for (EnrollmentStateClient client : mEnrollmentStateClients) {
+ try {
+ client.mListener.onEscrowTokenActiveStateChanged(handle, active);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot notify client of a Token Activation change: " + active);
+ }
+ }
+ }
+
+ /**
* Class that holds onto client related information - listener interface, process that hosts the
* binder object etc.
* <p>
@@ -301,9 +567,12 @@
return mListenerBinder == binder;
}
- }
-
- @Override
- public void dump(PrintWriter writer) {
+ public void onEnrollmentAdvertisementStarted() {
+ try {
+ mListener.onEnrollmentAdvertisingStarted();
+ } catch (RemoteException e) {
+ Log.e(TAG, "onEnrollmentAdvertisementStarted() failed", e);
+ }
+ }
}
}
diff --git a/service/src/com/android/car/trust/CarTrustAgentUnlockService.java b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
new file mode 100644
index 0000000..0f5e031
--- /dev/null
+++ b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.trust;
+
+import android.bluetooth.BluetoothDevice;
+import android.util.Log;
+
+import com.android.car.Utils;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+
+/**
+ * A service that interacts with the Trust Agent {@link CarBleTrustAgent} and a comms (BLE) service
+ * {@link CarTrustAgentBleManager} to receive the necessary credentials to authenticate
+ * an Android user.
+ */
+public class CarTrustAgentUnlockService {
+ private static final String TAG = "CarTrustAgentUnlock";
+ private final CarTrustedDeviceService mTrustedDeviceService;
+ private final CarTrustAgentBleManager mCarTrustAgentBleManager;
+ private CarTrustAgentUnlockDelegate mUnlockDelegate;
+ // Locks
+ private final Object mTokenLock = new Object();
+ private final Object mHandleLock = new Object();
+ private final Object mDeviceLock = new Object();
+
+ @GuardedBy("mTokenLock")
+ private byte[] mUnlockToken;
+ @GuardedBy("mHandleLock")
+ private byte[] mUnlockHandle;
+ @GuardedBy("mDeviceLock")
+ private BluetoothDevice mRemoteUnlockDevice;
+
+ CarTrustAgentUnlockService(CarTrustedDeviceService service,
+ CarTrustAgentBleManager bleService) {
+ mTrustedDeviceService = service;
+ mCarTrustAgentBleManager = bleService;
+ }
+
+ /**
+ * The interface that an unlock delegate has to implement to get the auth credentials from
+ * the unlock service.
+ */
+ interface CarTrustAgentUnlockDelegate {
+ /**
+ * Called when the Unlock service has the auth credentials to pass.
+ *
+ * @param user user being authorized
+ * @param token escrow token for the user
+ * @param handle the handle corresponding to the escrow token
+ */
+ void onUnlockDataReceived(int user, byte[] token, long handle);
+ }
+
+ /**
+ * Set a delegate that implements {@link CarTrustAgentUnlockDelegate}. The delegate will be
+ * handed the auth related data (token and handle) when it is received from the remote
+ * trusted device. The delegate is expected to use that to authorize the user.
+ */
+ void setUnlockRequestDelegate(CarTrustAgentUnlockDelegate delegate) {
+ mUnlockDelegate = delegate;
+ }
+
+ /**
+ * Start Unlock Advertising
+ */
+ void startUnlockAdvertising() {
+ mTrustedDeviceService.getCarTrustAgentEnrollmentService().stopEnrollmentAdvertising();
+ stopUnlockAdvertising();
+ mCarTrustAgentBleManager.startUnlockAdvertising();
+ }
+
+ /**
+ * Stop unlock advertising
+ */
+ void stopUnlockAdvertising() {
+ mCarTrustAgentBleManager.stopUnlockAdvertising();
+ // Also disconnect from the peer.
+ if (mRemoteUnlockDevice != null) {
+ mCarTrustAgentBleManager.disconnectRemoteDevice(mRemoteUnlockDevice);
+ }
+ }
+
+ void init() {
+ mCarTrustAgentBleManager.setupUnlockBleServer();
+ }
+
+ void release() {
+ synchronized (mDeviceLock) {
+ mRemoteUnlockDevice = null;
+ }
+ }
+
+ void onRemoteDeviceConnected(BluetoothDevice device) {
+ synchronized (mDeviceLock) {
+ if (mRemoteUnlockDevice != null) {
+ // TBD, return when this is encountered?
+ Log.e(TAG, "Unexpected: Cannot connect to another device when already connected");
+ }
+ mRemoteUnlockDevice = device;
+ }
+ }
+
+ void onRemoteDeviceDisconnected(BluetoothDevice device) {
+ // sanity checking
+ if (!device.equals(mRemoteUnlockDevice) && device.getAddress() != null) {
+ Log.e(TAG, "Disconnected from an unknown device:" + device.getAddress());
+ }
+ synchronized (mDeviceLock) {
+ mRemoteUnlockDevice = null;
+ }
+ }
+
+ void onUnlockTokenReceived(byte[] value) {
+ synchronized (mTokenLock) {
+ mUnlockToken = value;
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Token: " + mUnlockToken);
+ }
+ if (mUnlockToken == null || mUnlockHandle == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Handle not available yet");
+ }
+ return;
+ }
+ if (mUnlockDelegate == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "No Unlock delegate");
+ }
+ return;
+ }
+ mUnlockDelegate.onUnlockDataReceived(
+ mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
+ mUnlockToken,
+ Utils.bytesToLong(mUnlockHandle));
+
+ synchronized (mTokenLock) {
+ mUnlockToken = null;
+ }
+ synchronized (mHandleLock) {
+ mUnlockHandle = null;
+ }
+ }
+
+ void onUnlockHandleReceived(byte[] value) {
+ synchronized (mHandleLock) {
+ mUnlockHandle = value;
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Handle: " + mUnlockHandle);
+ }
+ if (mUnlockToken == null || mUnlockHandle == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Token not available yet");
+ }
+ return;
+ }
+
+ if (mUnlockDelegate == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "No Unlock delegate");
+ }
+ return;
+ }
+ mUnlockDelegate.onUnlockDataReceived(
+ mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
+ mUnlockToken,
+ Utils.bytesToLong(mUnlockHandle));
+
+ synchronized (mUnlockToken) {
+ mUnlockToken = null;
+ }
+ synchronized (mHandleLock) {
+ mUnlockHandle = null;
+ }
+ }
+
+ void dump(PrintWriter writer) {
+ }
+}
diff --git a/service/src/com/android/car/trust/CarTrustedDeviceService.java b/service/src/com/android/car/trust/CarTrustedDeviceService.java
new file mode 100644
index 0000000..538f31f
--- /dev/null
+++ b/service/src/com/android/car/trust/CarTrustedDeviceService.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2019 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 com.android.car.trust;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.car.CarServiceBase;
+import com.android.car.R;
+
+import java.io.PrintWriter;
+
+/**
+ * The part of the Car service that enables the Trusted device feature. Trusted Device is a feature
+ * where a remote device is enrolled as a trusted device that can authorize an Android user in lieu
+ * of the user entering a password or PIN.
+ * <p>
+ * It is comprised of the {@link CarTrustAgentEnrollmentService} for handling enrollment and
+ * {@link CarTrustAgentUnlockService} for handling unlock/auth.
+ *
+ */
+public class CarTrustedDeviceService implements CarServiceBase {
+ private static final String TAG = CarTrustedDeviceService.class.getSimpleName();
+ private final Context mContext;
+ private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
+ private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
+ private CarTrustAgentBleManager mCarTrustAgentBleManager;
+ private SharedPreferences mTrustAgentTokenPreferences;
+
+
+ public CarTrustedDeviceService(Context context) {
+ mContext = context;
+ mCarTrustAgentBleManager = new CarTrustAgentBleManager(context);
+ mCarTrustAgentEnrollmentService = new CarTrustAgentEnrollmentService(this,
+ mCarTrustAgentBleManager);
+ mCarTrustAgentUnlockService = new CarTrustAgentUnlockService(this,
+ mCarTrustAgentBleManager);
+ }
+
+ @Override
+ public synchronized void init() {
+ mCarTrustAgentEnrollmentService.init();
+ mCarTrustAgentUnlockService.init();
+ }
+
+ @Override
+ public synchronized void release() {
+ mCarTrustAgentBleManager.cleanup();
+ mCarTrustAgentEnrollmentService.release();
+ mCarTrustAgentUnlockService.release();
+ }
+
+ /**
+ * Returns the internal {@link CarTrustAgentEnrollmentService} instance.
+ */
+ public CarTrustAgentEnrollmentService getCarTrustAgentEnrollmentService() {
+ return mCarTrustAgentEnrollmentService;
+ }
+
+ /**
+ * Returns the internal {@link CarTrustAgentUnlockService} instance.
+ */
+ public CarTrustAgentUnlockService getCarTrustAgentUnlockService() {
+ return mCarTrustAgentUnlockService;
+ }
+
+ /**
+ * Returns User Id for the given token handle
+ *
+ * @param handle The handle corresponding to the escrow token
+ * @return User id corresponding to the handle
+ */
+ int getUserHandleByTokenHandle(long handle) {
+ return getSharedPrefs().getInt(String.valueOf(handle), -1);
+ }
+
+ void onRemoteDeviceConnected(BluetoothDevice device) {
+ mCarTrustAgentEnrollmentService.onRemoteDeviceConnected(device);
+ mCarTrustAgentUnlockService.onRemoteDeviceConnected(device);
+ }
+
+ void onRemoteDeviceDisconnected(BluetoothDevice device) {
+ mCarTrustAgentEnrollmentService.onRemoteDeviceDisconnected(device);
+ mCarTrustAgentUnlockService.onRemoteDeviceDisconnected(device);
+ }
+
+ void cleanupBleService() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "cleanupBleService");
+ }
+ mCarTrustAgentBleManager.stopGattServer();
+ mCarTrustAgentBleManager.stopEnrollmentAdvertising();
+ mCarTrustAgentBleManager.stopUnlockAdvertising();
+ }
+
+ SharedPreferences getSharedPrefs() {
+ if (mTrustAgentTokenPreferences != null) {
+ return mTrustAgentTokenPreferences;
+ }
+ mTrustAgentTokenPreferences = mContext.getSharedPreferences(
+ mContext.getString(R.string.token_handle_shared_preferences), Context.MODE_PRIVATE);
+ return mTrustAgentTokenPreferences;
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ }
+}
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 0657cba..0cf62e7 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -23,7 +23,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
-import android.content.pm.UserInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -56,10 +55,10 @@
/**
* Called when a client connection is established or re-established.
*
- * @param clientName String that uniquely identifies the service and user.
- * @param binder Binder for communicating with the client.
+ * @param clientName String that uniquely identifies the service and user.
+ * @param clientService The IBinder of the client's communication channel.
*/
- void onClientConnected(String clientName, IBinder binder);
+ void onClientConnected(String clientName, IBinder clientService);
/**
* Called when a client connection is terminated.
@@ -114,7 +113,7 @@
/**
* Constructor for client managers.
*
- * @param context Context to use for registering receivers and binding services.
+ * @param context Context to use for registering receivers and binding services.
* @param userManagerHelper User manager for querying current user state.
*/
public VmsClientManager(Context context, CarUserManagerHelper userManagerHelper) {
@@ -142,10 +141,10 @@
mContext.unregisterReceiver(mBootCompletedReceiver);
mContext.unregisterReceiver(mUserSwitchReceiver);
synchronized (mSystemClients) {
- unbind(mSystemClients);
+ terminate(mSystemClients);
}
synchronized (mCurrentUserClients) {
- unbind(mCurrentUserClients);
+ terminate(mCurrentUserClients);
}
}
@@ -194,12 +193,12 @@
}
private void bindToCurrentUserClients() {
- UserInfo userInfo = mUserManagerHelper.getCurrentForegroundUserInfo();
+ int currentUserId = mUserManagerHelper.getCurrentForegroundUserId();
synchronized (mCurrentUserClients) {
- if (mCurrentUser != userInfo.id) {
- unbind(mCurrentUserClients);
+ if (mCurrentUser != currentUserId) {
+ terminate(mCurrentUserClients);
}
- mCurrentUser = userInfo.id;
+ mCurrentUser = currentUserId;
// To avoid the risk of double-binding, clients running as the system user must only
// ever be bound in bindToSystemClients().
@@ -212,8 +211,9 @@
String[] clientNames = mContext.getResources().getStringArray(
R.array.vmsPublisherUserClients);
Log.i(TAG, "Attempting to bind " + clientNames.length + " user client(s)");
+ UserHandle currentUserHandle = UserHandle.of(mCurrentUser);
for (String clientName : clientNames) {
- bind(mCurrentUserClients, clientName, userInfo.getUserHandle());
+ bind(mCurrentUserClients, clientName, currentUserHandle);
}
}
}
@@ -244,17 +244,17 @@
}
}
- private void unbind(Map<String, ClientConnection> connectionMap) {
+ private void terminate(Map<String, ClientConnection> connectionMap) {
for (ClientConnection connection : connectionMap.values()) {
- connection.unbind();
+ connection.terminate();
}
connectionMap.clear();
}
- private void notifyListenersOnClientConnected(String clientName, IBinder binder) {
+ private void notifyListenersOnClientConnected(String clientName, IBinder clientService) {
synchronized (mListeners) {
for (ConnectionListener listener : mListeners) {
- listener.onClientConnected(clientName, binder);
+ listener.onClientConnected(clientName, clientService);
}
}
}
@@ -272,7 +272,8 @@
private final UserHandle mUser;
private final String mFullName;
private boolean mIsBound = false;
- private IBinder mBinder;
+ private boolean mIsTerminated = false;
+ private IBinder mClientService;
ClientConnection(ComponentName name, UserHandle user) {
mName = name;
@@ -281,10 +282,12 @@
}
synchronized boolean bind() {
- // Ignore if already bound
if (mIsBound) {
return true;
}
+ if (mIsTerminated) {
+ return false;
+ }
if (DBG) Log.d(TAG, "binding: " + mFullName);
Intent intent = new Intent();
@@ -307,26 +310,34 @@
Log.e(TAG, "While unbinding " + mFullName, t);
}
mIsBound = false;
- if (mBinder != null) {
+ if (mClientService != null) {
notifyListenersOnClientDisconnected(mFullName);
}
- mBinder = null;
+ mClientService = null;
}
- void rebind() {
+ synchronized void rebind() {
unbind();
if (DBG) {
Log.d(TAG,
String.format("rebinding %s after %dms", mFullName, mMillisBeforeRebind));
}
- mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+ if (!mIsTerminated) {
+ mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+ }
+ }
+
+ synchronized void terminate() {
+ if (DBG) Log.d(TAG, "terminating: " + mFullName);
+ mIsTerminated = true;
+ unbind();
}
@Override
- public void onServiceConnected(ComponentName name, IBinder binder) {
+ public void onServiceConnected(ComponentName name, IBinder service) {
if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
- mBinder = binder;
- notifyListenersOnClientConnected(mFullName, mBinder);
+ mClientService = service;
+ notifyListenersOnClientConnected(mFullName, mClientService);
}
@Override
@@ -344,7 +355,7 @@
@Override
public void onNullBinding(ComponentName name) {
if (DBG) Log.d(TAG, "onNullBinding: " + mFullName);
- unbind();
+ terminate();
}
@Override
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
index 68fafcf..088aa68 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
@@ -51,7 +51,7 @@
android:layout_margin="10dp"
android:background="#ffa9a8"
android:foreground="?android:attr/selectableItemBackground"
- android:text="Call (Shows persistent heads-up until swiped away)"
+ android:text="Call (Shows persistent heads-up, only dismissed on cancel)"
android:textSize="30sp"/>
<Button
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
index aa4a2eb..245b945 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
@@ -30,7 +30,8 @@
<TextView
android:id="@+id/location_info"
android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"/>
<TextView
android:id="@+id/accel_info"
android:layout_width="wrap_content"
@@ -57,6 +58,7 @@
<TextView
android:id="@+id/sensor_info"
android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"/>
</LinearLayout>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/storagevolumes.xml b/tests/EmbeddedKitchenSinkApp/res/layout/storagevolumes.xml
new file mode 100644
index 0000000..7310acd
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/storagevolumes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<ListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/storage_volumes_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</ListView>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index 138cfba..a80c3b4 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -63,6 +63,7 @@
import com.google.android.car.kitchensink.property.PropertyTestFragment;
import com.google.android.car.kitchensink.sensor.SensorsTestFragment;
import com.google.android.car.kitchensink.storagelifetime.StorageLifetimeFragment;
+import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
import com.google.android.car.kitchensink.touch.TouchTestFragment;
import com.google.android.car.kitchensink.users.UsersFragment;
import com.google.android.car.kitchensink.vhal.VehicleHalFragment;
@@ -174,6 +175,7 @@
new FragmentMenuEntry("property test", PropertyTestFragment.class),
new FragmentMenuEntry("sensors", SensorsTestFragment.class),
new FragmentMenuEntry("storage lifetime", StorageLifetimeFragment.class),
+ new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
new FragmentMenuEntry("touch test", TouchTestFragment.class),
new FragmentMenuEntry("users", UsersFragment.class),
new FragmentMenuEntry("volume test", VolumeTestFragment.class),
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
index e1edea5..6e3d585 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
@@ -28,6 +28,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -106,7 +107,9 @@
mActivity = (KitchenSinkActivity) getHost();
mSensorInfo = (TextView) view.findViewById(R.id.sensor_info);
+ mSensorInfo.setMovementMethod(new ScrollingMovementMethod());
mLocationInfo = (TextView) view.findViewById(R.id.location_info);
+ mLocationInfo.setMovementMethod(new ScrollingMovementMethod());
mAccelInfo = (TextView) view.findViewById(R.id.accel_info);
mGyroInfo = (TextView) view.findViewById(R.id.gyro_info);
mMagInfo = (TextView) view.findViewById(R.id.mag_info);
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagevolumes/StorageVolumesFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagevolumes/StorageVolumesFragment.java
new file mode 100644
index 0000000..f10b0f3
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagevolumes/StorageVolumesFragment.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 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 com.google.android.car.kitchensink.storagevolumes;
+
+import android.annotation.Nullable;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Listens for changes in content and displays updated list of volumes
+ */
+public class StorageVolumesFragment extends Fragment {
+ private static final String TAG = "CAR.STORAGEVOLUMES.KS";
+
+ private ListView mStorageVolumesList;
+ private StorageManager mStorageManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mStorageManager = Objects.requireNonNull(getContext())
+ .getSystemService(StorageManager.class);
+
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ mStorageVolumesList = (ListView) inflater.inflate(R.layout.storagevolumes,
+ container, false);
+ return mStorageVolumesList;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refresh();
+ Objects.requireNonNull(getContext()).getContentResolver()
+ .registerContentObserver(MediaStore.AUTHORITY_URI, true, mContentObserver);
+ }
+
+ @Override
+ public void onPause() {
+ Objects.requireNonNull(getContext()).getContentResolver()
+ .unregisterContentObserver(mContentObserver);
+ super.onPause();
+ }
+
+ private void refresh() {
+ final List<VolumeInfo> volumes = mStorageManager.getVolumes();
+ volumes.sort(VolumeInfo.getDescriptionComparator());
+
+ mStorageVolumesList.setAdapter(new ArrayAdapter<>(getContext(),
+ android.R.layout.simple_list_item_1, volumes.toArray()));
+ }
+
+ private final ContentObserver mContentObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ if (isResumed()) {
+ Log.d(TAG, "Content has changed for URI " + uri);
+ refresh();
+ }
+ }
+ };
+}
diff --git a/tests/PowerTestService/com.android.car.powertestservice.rc b/tests/PowerTestService/com.android.car.powertestservice.rc
index db27237..117da7e 100644
--- a/tests/PowerTestService/com.android.car.powertestservice.rc
+++ b/tests/PowerTestService/com.android.car.powertestservice.rc
@@ -16,6 +16,7 @@
class core
user system
group system
+ disabled
-on boot && property:boot.car_service_created=1
+on property:boot.car_service_created=1
start com.android.car.powertestservice
diff --git a/tests/ThemePlayground/res/layout/dialog_samples.xml b/tests/ThemePlayground/res/layout/dialog_samples.xml
index e8c398b..47d679f 100644
--- a/tests/ThemePlayground/res/layout/dialog_samples.xml
+++ b/tests/ThemePlayground/res/layout/dialog_samples.xml
@@ -38,20 +38,30 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show Dialog"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/showDialogOnlyPositiveBT"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toBottomOf="@+id/setBackground" />
+
+ <Button
+ android:id="@+id/showDialogOnlyPositiveBT"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog with only positive button"
+ app:layout_constraintBottom_toTopOf="@+id/showDialogWithCheckboxBT"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/showDialogBT" />
<Button
android:id="@+id/showDialogWithCheckboxBT"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show Dialog With Checkbox"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/showToast"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="@+id/showDialogBT" />
+ app:layout_constraintTop_toBottomOf="@+id/showDialogOnlyPositiveBT" />
<Button
android:id="@+id/showToast"
@@ -61,7 +71,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="@+id/showDialogWithCheckboxBT" />
+ app:layout_constraintTop_toBottomOf="@+id/showDialogWithCheckboxBT" />
<include layout="@layout/menu_button"/>
diff --git a/tests/ThemePlayground/src/com/android/car/themeplayground/DialogSamples.java b/tests/ThemePlayground/src/com/android/car/themeplayground/DialogSamples.java
index ca07a71..0b311ef 100644
--- a/tests/ThemePlayground/src/com/android/car/themeplayground/DialogSamples.java
+++ b/tests/ThemePlayground/src/com/android/car/themeplayground/DialogSamples.java
@@ -36,9 +36,11 @@
setContentView(R.layout.dialog_samples);
Button mShowDialogBT = findViewById(R.id.showDialogBT);
+ Button mShowDialogOnlyPositiveBT = findViewById(R.id.showDialogOnlyPositiveBT);
Button mShowDialogWithCheckboxBT = findViewById(R.id.showDialogWithCheckboxBT);
setupBackgroundColorControls(R.id.dialogLayout);
mShowDialogBT.setOnClickListener(v -> openDialog(false));
+ mShowDialogOnlyPositiveBT.setOnClickListener(v -> openDialogWithOnlyPositiveButton());
mShowDialogWithCheckboxBT.setOnClickListener(v -> openDialog(true));
Button mShowToast = findViewById(R.id.showToast);
mShowToast.setOnClickListener(v -> showToast());
@@ -72,6 +74,16 @@
builder.show();
}
+ private void openDialogWithOnlyPositiveButton() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ new ContextThemeWrapper(this, R.style.Theme_Testing_Dialog_Alert));
+ builder.setTitle("Standard Alert Dialog")
+ .setMessage("With a message to show.");
+ builder.setPositiveButton("OK", (dialoginterface, i) -> {
+ });
+ builder.show();
+ }
+
private void showToast() {
Toast.makeText(this, "Toast message looks like this",
Toast.LENGTH_LONG).show();
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
index 369c8f3..f1a25a5 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
@@ -17,7 +17,6 @@
import static android.car.CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED;
import static android.car.CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION;
-import static android.car.CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND;
import android.car.Car;
import android.car.CarAppFocusManager;
@@ -95,7 +94,6 @@
CarAppFocusManager manager = createManager();
manager.addFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
manager.addFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
- manager.addFocusListener(listener2, APP_FOCUS_TYPE_VOICE_COMMAND);
manager.removeFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
@@ -113,10 +111,6 @@
manager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION, new FocusOwnershipCallback()));
assertFalse(listener2.waitForFocusChangeAndAssert(
DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true));
- assertEquals(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED,
- manager.requestAppFocus(APP_FOCUS_TYPE_VOICE_COMMAND, new FocusOwnershipCallback()));
- assertTrue(listener2.waitForFocusChangeAndAssert(
- DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_VOICE_COMMAND, true));
manager.removeFocusListener(listener2, 2);
manager.removeFocusListener(listener2, 2); // Double-unregister is OK
@@ -134,9 +128,7 @@
FocusOwnershipCallback owner1 = new FocusOwnershipCallback();
FocusOwnershipCallback owner2 = new FocusOwnershipCallback();
manager1.addFocusListener(change1, APP_FOCUS_TYPE_NAVIGATION);
- manager1.addFocusListener(change1, APP_FOCUS_TYPE_VOICE_COMMAND);
manager2.addFocusListener(change2, APP_FOCUS_TYPE_NAVIGATION);
- manager2.addFocusListener(change2, APP_FOCUS_TYPE_VOICE_COMMAND);
assertEquals(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED,
@@ -147,33 +139,19 @@
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
assertTrue(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertFalse(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
assertTrue(change2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
APP_FOCUS_TYPE_NAVIGATION, true));
assertTrue(change1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
APP_FOCUS_TYPE_NAVIGATION, true));
- assertEquals(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED,
- manager1.requestAppFocus(APP_FOCUS_TYPE_VOICE_COMMAND, owner1));
- assertTrue(owner1.waitForOwnershipGrantAndAssert(
- DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_VOICE_COMMAND));
expectedFocuses = new int[] {
APP_FOCUS_TYPE_NAVIGATION,
- APP_FOCUS_TYPE_VOICE_COMMAND };
+ };
assertTrue(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertTrue(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertFalse(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
- assertTrue(change2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
- assertTrue(change1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
// this should be no-op
change1.reset();
@@ -196,10 +174,7 @@
DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION));
assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertTrue(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertTrue(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
assertTrue(owner1.waitForOwnershipLossAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
@@ -210,37 +185,23 @@
change2.reset();
manager1.abandonAppFocus(owner1, APP_FOCUS_TYPE_NAVIGATION);
assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertTrue(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertTrue(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
change1.reset();
change2.reset();
- manager1.abandonAppFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND);
assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertTrue(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
expectedFocuses = new int[] {APP_FOCUS_TYPE_NAVIGATION};
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
- assertTrue(change2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
- assertTrue(change1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
change1.reset();
change2.reset();
manager2.abandonAppFocus(owner2, APP_FOCUS_TYPE_NAVIGATION);
assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager1.isOwningFocus(owner1, APP_FOCUS_TYPE_VOICE_COMMAND));
assertFalse(manager2.isOwningFocus(owner2, APP_FOCUS_TYPE_NAVIGATION));
- assertFalse(manager2.isOwningFocus(owner2,
- APP_FOCUS_TYPE_VOICE_COMMAND));
expectedFocuses = emptyFocus;
Assert.assertArrayEquals(expectedFocuses, manager1.getActiveAppTypes());
Assert.assertArrayEquals(expectedFocuses, manager2.getActiveAppTypes());
@@ -262,7 +223,6 @@
FocusChangedListener listener2 = new FocusChangedListener();
FocusOwnershipCallback owner = new FocusOwnershipCallback();
manager1.addFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
- manager1.addFocusListener(listener1, APP_FOCUS_TYPE_VOICE_COMMAND);
manager2.addFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
assertEquals(APP_FOCUS_REQUEST_SUCCEEDED,
@@ -277,26 +237,6 @@
listener1.reset();
listener2.reset();
- assertEquals(APP_FOCUS_REQUEST_SUCCEEDED,
- manager1.requestAppFocus(APP_FOCUS_TYPE_VOICE_COMMAND, owner));
- assertTrue(owner.waitForOwnershipGrantAndAssert(
- DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_VOICE_COMMAND));
-
- assertTrue(listener1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
- assertFalse(listener2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
-
- listener1.reset();
- listener2.reset();
- manager1.abandonAppFocus(owner, APP_FOCUS_TYPE_VOICE_COMMAND);
- assertTrue(listener1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
- assertFalse(listener2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
-
- listener1.reset();
- listener2.reset();
manager1.abandonAppFocus(owner, APP_FOCUS_TYPE_NAVIGATION);
assertTrue(listener1.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
APP_FOCUS_TYPE_NAVIGATION, false));
@@ -333,7 +273,6 @@
FocusChangedListener listener2 = new FocusChangedListener();
FocusOwnershipCallback owner = new FocusOwnershipCallback();
manager.addFocusListener(listener, APP_FOCUS_TYPE_NAVIGATION);
- manager.addFocusListener(listener, APP_FOCUS_TYPE_VOICE_COMMAND);
manager.addFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
assertEquals(APP_FOCUS_REQUEST_SUCCEEDED,
@@ -348,26 +287,6 @@
listener.reset();
listener2.reset();
- assertEquals(APP_FOCUS_REQUEST_SUCCEEDED,
- manager.requestAppFocus(APP_FOCUS_TYPE_VOICE_COMMAND, owner));
- assertTrue(owner.waitForOwnershipGrantAndAssert(
- DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_VOICE_COMMAND));
-
- assertTrue(listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
- assertFalse(listener2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, true));
-
- listener.reset();
- listener2.reset();
- manager.abandonAppFocus(owner, APP_FOCUS_TYPE_VOICE_COMMAND);
- assertTrue(listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
- assertFalse(listener2.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- APP_FOCUS_TYPE_VOICE_COMMAND, false));
-
- listener.reset();
- listener2.reset();
manager.abandonAppFocus(owner, APP_FOCUS_TYPE_NAVIGATION);
assertTrue(listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
APP_FOCUS_TYPE_NAVIGATION, false));
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
index f86c76e..5b991a9 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
@@ -23,9 +23,13 @@
import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO;
import static android.car.drivingstate.CarUxRestrictionsConfiguration.Builder.SpeedRange.MAX_SPEED;
+import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_BASELINE;
+import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_PASSENGER;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.car.drivingstate.CarUxRestrictionsConfiguration.Builder;
+import android.car.drivingstate.CarUxRestrictionsConfiguration.DrivingStateRestrictions;
import android.util.JsonReader;
import android.util.JsonWriter;
@@ -47,39 +51,41 @@
// This test verifies the expected way to build config would succeed.
public void testConstruction() {
- new CarUxRestrictionsConfiguration.Builder().build();
+ new Builder().build();
- new CarUxRestrictionsConfiguration.Builder()
+ new Builder()
.setMaxStringLength(1)
.build();
- new CarUxRestrictionsConfiguration.Builder()
+ new Builder()
.setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
.build();
- new CarUxRestrictionsConfiguration.Builder()
+ new Builder()
.setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
.build();
- new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(0f, MAX_SPEED)))
.build();
- new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED)
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f, MAX_SPEED),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(0f, 1f)))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(1f, MAX_SPEED)))
.build();
}
public void testUnspecifiedDrivingStateUsesDefaultRestriction() {
- CarUxRestrictionsConfiguration config =
- new CarUxRestrictionsConfiguration.Builder().build();
+ CarUxRestrictionsConfiguration config = new Builder().build();
CarUxRestrictions parkedRestrictions = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
assertTrue(parkedRestrictions.isRequiresDistractionOptimization());
@@ -90,183 +96,245 @@
assertEquals(movingRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
}
- public void testBuilderValidation_MultipleSpeedRange_NonZeroStart() {
- CarUxRestrictionsConfiguration.Builder builder =
- new CarUxRestrictionsConfiguration.Builder();
- builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, 2),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED);
- builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(2, MAX_SPEED),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+ public void testBuilderValidation_UnspecifiedStateUsesRestrictiveDefault() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+ assertTrue(config.getUxRestrictions(DRIVING_STATE_PARKED, 0f)
+ .isRequiresDistractionOptimization());
+ assertTrue(config.getUxRestrictions(DRIVING_STATE_IDLING, 0f)
+ .isRequiresDistractionOptimization());
+ }
+ public void testBuilderValidation_NonMovingStateHasOneRestriction() {
+ Builder builder = new Builder();
+ builder.setUxRestrictions(DRIVING_STATE_IDLING,
+ true, UX_RESTRICTIONS_NO_VIDEO);
+ builder.setUxRestrictions(DRIVING_STATE_IDLING,
+ false, UX_RESTRICTIONS_BASELINE);
try {
builder.build();
fail();
- } catch (IllegalStateException e) {
+ } catch (Exception e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_PassengerModeNoSpeedRangeOverlap() {
+ Builder builder = new Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(1f, 2f)));
+ builder.setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(1f)));
+ try {
+ builder.build();
+ fail();
+ } catch (Exception e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_PassengerModeCanSpecifySubsetOfSpeedRange() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER)
+ .setSpeedRange(new Builder.SpeedRange(1f, 2f)))
+ .build();
+
+ assertTrue(config.getUxRestrictions(DRIVING_STATE_MOVING, 1f, UX_RESTRICTION_MODE_PASSENGER)
+ .isRequiresDistractionOptimization());
+ }
+
+ public void testBuilderValidation_MultipleSpeedRange_NonZeroStart() {
+ Builder builder = new Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new Builder.SpeedRange(1, 2),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new Builder.SpeedRange(2, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+ try {
+ builder.build();
+ fail();
+ } catch (Exception e) {
// Expected exception.
}
}
public void testBuilderValidation_SpeedRange_NonZeroStart() {
- CarUxRestrictionsConfiguration.Builder builder =
- new CarUxRestrictionsConfiguration.Builder();
+ Builder builder = new Builder();
builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, MAX_SPEED),
+ new Builder.SpeedRange(1, MAX_SPEED),
true, UX_RESTRICTIONS_FULLY_RESTRICTED);
-
try {
builder.build();
fail();
- } catch (IllegalStateException e) {
+ } catch (Exception e) {
// Expected exception.
}
}
public void testBuilderValidation_SpeedRange_Overlap() {
- CarUxRestrictionsConfiguration.Builder builder =
- new CarUxRestrictionsConfiguration.Builder();
+ Builder builder = new Builder();
builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ new Builder.SpeedRange(0, 5), true,
UX_RESTRICTIONS_FULLY_RESTRICTED);
builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(4), true,
+ new Builder.SpeedRange(4), true,
UX_RESTRICTIONS_FULLY_RESTRICTED);
-
try {
builder.build();
fail();
- } catch (IllegalStateException e) {
+ } catch (Exception e) {
// Expected exception.
}
}
public void testBuilderValidation_SpeedRange_Gap() {
- CarUxRestrictionsConfiguration.Builder builder =
- new CarUxRestrictionsConfiguration.Builder();
+ Builder builder = new Builder();
builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ new Builder.SpeedRange(0, 5), true,
UX_RESTRICTIONS_FULLY_RESTRICTED);
builder.setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(8), true,
+ new Builder.SpeedRange(8), true,
UX_RESTRICTIONS_FULLY_RESTRICTED);
-
try {
builder.build();
fail();
- } catch (IllegalStateException e) {
+ } catch (Exception e) {
// Expected exception.
}
}
public void testBuilderValidation_NonMovingStateCannotUseSpeedRange() {
- CarUxRestrictionsConfiguration.Builder builder =
- new CarUxRestrictionsConfiguration.Builder();
+ Builder builder = new Builder();
try {
builder.setUxRestrictions(DRIVING_STATE_PARKED,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ new Builder.SpeedRange(0, 5), true,
UX_RESTRICTIONS_FULLY_RESTRICTED);
- } catch (IllegalArgumentException e) {
+ fail();
+ } catch (Exception e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_MultipleMovingRestrictionsShouldAllContainSpeedRange() {
+ Builder builder = new Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED));
+ builder.setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(1f)));
+ try {
+ builder.build();
+ fail();
+ } catch (Exception e) {
// Expected exception.
}
}
public void testSpeedRange_Construction() {
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ new Builder.SpeedRange(0f);
+ new Builder.SpeedRange(0f, 1f);
+ new Builder.SpeedRange(0f, MAX_SPEED);
}
public void testSpeedRange_NegativeMax() {
try {
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f, -1f);
- } catch (IllegalArgumentException e) {
+ new Builder.SpeedRange(2f, -1f);
+ } catch (Exception e) {
// Expected exception.
}
}
public void testSpeedRange_MinGreaterThanMax() {
try {
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, 2f);
- } catch (IllegalArgumentException e) {
+ new Builder.SpeedRange(5f, 2f);
+ } catch (Exception e) {
// Expected exception.
}
}
public void testSpeedRangeComparison_DifferentMin() {
- CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
- CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f);
+ Builder.SpeedRange s1 =
+ new Builder.SpeedRange(1f);
+ Builder.SpeedRange s2 =
+ new Builder.SpeedRange(2f);
assertTrue(s1.compareTo(s2) < 0);
assertTrue(s2.compareTo(s1) > 0);
}
public void testSpeedRangeComparison_SameMin() {
- CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
- CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ Builder.SpeedRange s1 =
+ new Builder.SpeedRange(1f);
+ Builder.SpeedRange s2 =
+ new Builder.SpeedRange(1f);
assertTrue(s1.compareTo(s2) == 0);
}
public void testSpeedRangeComparison_SameMinDifferentMax() {
- CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
- CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+ Builder.SpeedRange s1 =
+ new Builder.SpeedRange(0f, 1f);
+ Builder.SpeedRange s2 =
+ new Builder.SpeedRange(0f, 2f);
assertTrue(s1.compareTo(s2) < 0);
assertTrue(s2.compareTo(s1) > 0);
}
public void testSpeedRangeComparison_MaxSpeed() {
- CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
- CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ Builder.SpeedRange s1 =
+ new Builder.SpeedRange(0f, 1f);
+ Builder.SpeedRange s2 =
+ new Builder.SpeedRange(0f);
assertTrue(s1.compareTo(s2) < 0);
assertTrue(s2.compareTo(s1) > 0);
}
public void testSpeedRangeEquals() {
- CarUxRestrictionsConfiguration.Builder.SpeedRange s1, s2;
+ Builder.SpeedRange s1, s2;
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ s1 = new Builder.SpeedRange(0f);
assertTrue(s1.equals(s1));
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
- s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ s1 = new Builder.SpeedRange(1f);
+ s2 = new Builder.SpeedRange(1f);
assertTrue(s1.compareTo(s2) == 0);
assertTrue(s1.equals(s2));
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
- s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ s1 = new Builder.SpeedRange(0f, 1f);
+ s2 = new Builder.SpeedRange(0f, 1f);
assertTrue(s1.equals(s2));
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
- s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ s1 = new Builder.SpeedRange(0f, MAX_SPEED);
+ s2 = new Builder.SpeedRange(0f, MAX_SPEED);
assertTrue(s1.equals(s2));
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
- s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ s1 = new Builder.SpeedRange(0f);
+ s2 = new Builder.SpeedRange(1f);
assertFalse(s1.equals(s2));
- s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
- s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+ s1 = new Builder.SpeedRange(0f, 1f);
+ s2 = new Builder.SpeedRange(0f, 2f);
assertFalse(s1.equals(s2));
}
- // TODO: add more tests that each verifies setting one filed in builder can be
- // successfully retrieved out of saved json.
public void testJsonSerialization_DefaultConstructor() {
CarUxRestrictionsConfiguration config =
- new CarUxRestrictionsConfiguration.Builder().build();
+ new Builder().build();
verifyConfigThroughJsonSerialization(config);
}
public void testJsonSerialization_RestrictionParameters() {
- CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ CarUxRestrictionsConfiguration config = new Builder()
.setMaxStringLength(1)
.setMaxCumulativeContentItems(1)
.setMaxContentDepth(1)
@@ -276,7 +344,7 @@
}
public void testJsonSerialization_NonMovingStateRestrictions() {
- CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ CarUxRestrictionsConfiguration config = new Builder()
.setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
.build();
@@ -284,7 +352,7 @@
}
public void testJsonSerialization_MovingStateNoSpeedRange() {
- CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ CarUxRestrictionsConfiguration config = new Builder()
.setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
.build();
@@ -292,13 +360,36 @@
}
public void testJsonSerialization_MovingStateWithSpeedRange() {
- CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 5f),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED)
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, MAX_SPEED),
- true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(0f, 5f)))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setSpeedRange(new Builder.SpeedRange(5f, MAX_SPEED)))
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_UxRestrictionMode() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ // Passenger mode
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(false)
+ .setRestrictions(UX_RESTRICTIONS_BASELINE)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER))
+ // Explicitly specify baseline mode
+ .setUxRestrictions(DRIVING_STATE_PARKED, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setMode(UX_RESTRICTION_MODE_BASELINE))
+ // Implicitly defaults to baseline mode
+ .setUxRestrictions(DRIVING_STATE_IDLING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO))
.build();
verifyConfigThroughJsonSerialization(config);
@@ -307,31 +398,45 @@
public void testDump() {
CarUxRestrictionsConfiguration[] configs = new CarUxRestrictionsConfiguration[] {
// Driving state with no speed range
- new CarUxRestrictionsConfiguration.Builder()
+ new Builder()
.setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
.setUxRestrictions(DRIVING_STATE_IDLING, true, UX_RESTRICTIONS_NO_VIDEO)
.setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_NO_VIDEO)
.build(),
// Parameters
- new CarUxRestrictionsConfiguration.Builder()
+ new Builder()
.setMaxStringLength(1)
.setMaxContentDepth(1)
.setMaxCumulativeContentItems(1)
.build(),
// Driving state with single speed range
- new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f),
- true, UX_RESTRICTIONS_NO_VIDEO)
+ new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setSpeedRange(new Builder.SpeedRange(0f)))
.build(),
// Driving state with multiple speed ranges
- new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
- true, UX_RESTRICTIONS_NO_VIDEO)
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f),
- true, UX_RESTRICTIONS_NO_VIDEO)
+ new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setSpeedRange(new Builder.SpeedRange(0f, 1f)))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setSpeedRange(new Builder.SpeedRange(1f)))
+ .build(),
+ // Driving state with passenger mode
+ new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(false)
+ .setRestrictions(UX_RESTRICTIONS_BASELINE)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setMode(UX_RESTRICTION_MODE_BASELINE))
.build(),
};
@@ -341,14 +446,19 @@
}
public void testDumpContainsNecessaryInfo() {
-
- CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
- true, UX_RESTRICTIONS_NO_VIDEO)
- .setUxRestrictions(DRIVING_STATE_MOVING,
- new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f),
- true, UX_RESTRICTIONS_NO_VIDEO)
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setSpeedRange(new Builder.SpeedRange(0f, 1f)))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO)
+ .setSpeedRange(new Builder.SpeedRange(1f)))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(false)
+ .setRestrictions(UX_RESTRICTIONS_BASELINE)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER))
.build();
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (PrintWriter writer = new PrintWriter(output)) {
@@ -363,8 +473,74 @@
assertTrue(dump.contains("Speed Range"));
assertTrue(dump.contains("Requires DO?"));
assertTrue(dump.contains("Restrictions"));
+ assertTrue(dump.contains("Passenger mode"));
+ assertTrue(dump.contains("Baseline mode"));
}
+ public void testSetUxRestrictions_UnspecifiedModeDefaultsToBaseline() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO))
+ .build();
+
+ CarUxRestrictions restrictions = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+ assertTrue(restrictions.isRequiresDistractionOptimization());
+ assertEquals(UX_RESTRICTIONS_NO_VIDEO, restrictions.getActiveRestrictions());
+
+ assertTrue(restrictions.isSameRestrictions(
+ config.getUxRestrictions(DRIVING_STATE_PARKED, 0f, UX_RESTRICTIONS_BASELINE)));
+ }
+
+ public void testSetUxRestrictions_PassengerMode() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(false)
+ .setRestrictions(UX_RESTRICTIONS_BASELINE)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER))
+ .setUxRestrictions(DRIVING_STATE_PARKED, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO))
+ .build();
+
+ CarUxRestrictions passenger = config.getUxRestrictions(
+ DRIVING_STATE_PARKED, 0f, UX_RESTRICTION_MODE_PASSENGER);
+ assertFalse(passenger.isRequiresDistractionOptimization());
+
+ CarUxRestrictions baseline = config.getUxRestrictions(
+ DRIVING_STATE_PARKED, 0f, UX_RESTRICTION_MODE_BASELINE);
+ assertTrue(baseline.isRequiresDistractionOptimization());
+ assertEquals(UX_RESTRICTIONS_NO_VIDEO, baseline.getActiveRestrictions());
+ }
+
+ public void testPassengerModeFallbackToBaseline() {
+ CarUxRestrictionsConfiguration config = new Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(true)
+ .setRestrictions(UX_RESTRICTIONS_NO_VIDEO))
+ .setUxRestrictions(DRIVING_STATE_MOVING, new DrivingStateRestrictions()
+ .setDistractionOptimizationRequired(false)
+ .setRestrictions(UX_RESTRICTIONS_BASELINE)
+ .setMode(UX_RESTRICTION_MODE_PASSENGER)
+ .setSpeedRange(new Builder.SpeedRange(3f)))
+ .build();
+
+ // Retrieve at speed within passenger mode range.
+ CarUxRestrictions passenger = config.getUxRestrictions(
+ DRIVING_STATE_MOVING, 5f, UX_RESTRICTION_MODE_PASSENGER);
+ assertFalse(passenger.isRequiresDistractionOptimization());
+
+ // Retrieve with passenger mode but outside speed range
+ CarUxRestrictions baseline = config.getUxRestrictions(
+ DRIVING_STATE_MOVING, 1f, UX_RESTRICTION_MODE_PASSENGER);
+ assertTrue(baseline.isRequiresDistractionOptimization());
+ assertEquals(UX_RESTRICTIONS_NO_VIDEO, baseline.getActiveRestrictions());
+ }
+
+ /**
+ * Writes input config as json, then reads a config out of json.
+ * Asserts the deserialized config is the same as input.
+ */
private void verifyConfigThroughJsonSerialization(CarUxRestrictionsConfiguration config) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out))) {
diff --git a/tests/carservice_test/src/com/android/car/AppFocusTest.java b/tests/carservice_test/src/com/android/car/AppFocusTest.java
index 910a132..3474d88 100644
--- a/tests/carservice_test/src/com/android/car/AppFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/AppFocusTest.java
@@ -43,19 +43,12 @@
FocusChangedListener listener = new FocusChangedListener();
FocusOwnershipCallback ownershipListener = new FocusOwnershipCallback();
manager.addFocusListener(listener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
- manager.addFocusListener(listener, CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
manager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, ownershipListener);
listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, true);
- manager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND, ownershipListener);
- listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND, true);
manager.abandonAppFocus(ownershipListener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, false);
- manager.abandonAppFocus(ownershipListener, CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
- listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
- CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND, false);
manager.removeFocusListener(listener);
}
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
index a1ff59f..15e7feb 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -37,7 +38,6 @@
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Handler;
@@ -70,7 +70,7 @@
private Resources mResources;
@Mock
private CarUserManagerHelper mUserManager;
- private UserInfo mUserInfo;
+ private int mUserId;
@Mock
private VmsClientManager.ConnectionListener mConnectionListener;
@@ -89,16 +89,17 @@
5);
when(mResources.getStringArray(
com.android.car.R.array.vmsPublisherSystemClients)).thenReturn(
- new String[]{
- "com.google.android.apps.vms.test/.VmsSystemClient"
- });
+ new String[]{
+ "com.google.android.apps.vms.test/.VmsSystemClient"
+ });
when(mResources.getStringArray(
com.android.car.R.array.vmsPublisherUserClients)).thenReturn(
- new String[]{
- "com.google.android.apps.vms.test/.VmsUserClient"
- });
- mUserInfo = new UserInfo(10, "Driver", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ new String[]{
+ "com.google.android.apps.vms.test/.VmsUserClient"
+ });
+
+ mUserId = 10;
+ when(mUserManager.getCurrentForegroundUserId()).thenAnswer((invocation) -> mUserId);
mClientManager = new VmsClientManager(mContext, mUserManager);
mClientManager.registerConnectionListener(mConnectionListener);
@@ -244,12 +245,12 @@
@Test
public void testUserSwitchedToSystemUser() {
- mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = UserHandle.USER_SYSTEM;
notifyUserSwitched();
// System user should not trigger any binding
- verifyUserBind(0);
+ verifySystemBind(0);
+ verifyNoBind();
}
@Test
@@ -502,16 +503,14 @@
resetContext();
reset(mConnectionListener);
- mUserInfo = new UserInfo(11, "Driver", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = 11;
notifyUserSwitched();
verify(mContext).unbindService(connection);
verify(mConnectionListener).onClientDisconnected(
eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+ ".VmsUserClient U=10"));
- verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyUserBind(1);
}
@Test
@@ -523,8 +522,7 @@
resetContext();
reset(mConnectionListener);
- mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = UserHandle.USER_SYSTEM;
notifyUserSwitched();
verify(mContext).unbindService(connection);
@@ -532,8 +530,7 @@
eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+ ".VmsUserClient U=10"));
// User processes will not be bound for system user
- verifyBind(0, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyNoBind();
}
@Test
@@ -543,13 +540,11 @@
ServiceConnection connection = mConnectionCaptor.getValue();
resetContext();
- mUserInfo = new UserInfo(11, "Driver", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = 11;
notifyUserSwitched();
verify(mContext).unbindService(connection);
- verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyUserBind(1);
}
@Test
@@ -561,16 +556,14 @@
resetContext();
reset(mConnectionListener);
- mUserInfo = new UserInfo(11, "Driver", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = 11;
notifyUserUnlocked();
verify(mContext).unbindService(connection);
verify(mConnectionListener).onClientDisconnected(
eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+ ".VmsUserClient U=10"));
- verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyUserBind(1);
}
@Test
@@ -582,8 +575,7 @@
resetContext();
reset(mConnectionListener);
- mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = UserHandle.USER_SYSTEM;
notifyUserUnlocked();
verify(mContext).unbindService(connection);
@@ -591,8 +583,7 @@
eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+ ".VmsUserClient U=10"));
// User processes will not be bound for system user
- verifyBind(0, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyNoBind();
}
@Test
@@ -602,13 +593,11 @@
ServiceConnection connection = mConnectionCaptor.getValue();
resetContext();
- mUserInfo = new UserInfo(11, "Driver", 0);
- when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+ mUserId = 11;
notifyUserUnlocked();
verify(mContext).unbindService(connection);
- verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ verifyUserBind(1);
}
private void resetContext() {
@@ -640,7 +629,12 @@
private void verifyUserBind(int times) {
verifyBind(times, "com.google.android.apps.vms.test/.VmsUserClient",
- mUserInfo.getUserHandle());
+ UserHandle.of(mUserId));
+ }
+
+ private void verifyNoBind() {
+ verify(mContext, never()).bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
+ anyInt(), any(Handler.class), any(UserHandle.class));
}
private void verifyBind(int times, String componentName,
diff --git a/tests/usb/AoapHostApp/src/com/google/android/car/usb/aoap/host/AoapSupportCheckService.java b/tests/usb/AoapHostApp/src/com/google/android/car/usb/aoap/host/AoapSupportCheckService.java
index 7825a66..f387a50 100644
--- a/tests/usb/AoapHostApp/src/com/google/android/car/usb/aoap/host/AoapSupportCheckService.java
+++ b/tests/usb/AoapHostApp/src/com/google/android/car/usb/aoap/host/AoapSupportCheckService.java
@@ -15,33 +15,15 @@
*/
package com.google.android.car.usb.aoap.host;
-import android.app.Service;
-import android.car.IUsbAoapSupportCheckService;
-import android.content.Intent;
+import android.car.AoapService;
import android.hardware.usb.UsbDevice;
-import android.os.IBinder;
/**
* Service to check is AOAP device supports Android Auto.
*/
-public class AoapSupportCheckService extends Service {
-
- private final IUsbAoapSupportCheckService.Stub mBinder =
- new IUsbAoapSupportCheckService.Stub() {
- public boolean isDeviceSupported(UsbDevice device) {
- // TODO: do some check.
- return true;
- }
- };
-
+public class AoapSupportCheckService extends AoapService {
@Override
- public void onCreate() {
- super.onCreate();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- // Return the interface
- return mBinder;
+ public int isDeviceSupported(UsbDevice device) {
+ return RESULT_OK;
}
}
diff --git a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2eCarTestBase.java b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2eCarTestBase.java
index d78713f..24ee1ff 100644
--- a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2eCarTestBase.java
+++ b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2eCarTestBase.java
@@ -47,7 +47,7 @@
public class E2eCarTestBase {
private static final String TAG = Utils.concatTag(E2eCarTestBase.class);
- private static final int DEFAULT_WAIT_TIMEOUT_MS = 1000;
+ private static final int DEFAULT_WAIT_TIMEOUT_MS = 5000;
protected IVehicle mVehicle;
protected Car mCar;
@@ -56,7 +56,7 @@
@Before
public void connectToVehicleHal() throws Exception {
- mVehicle = Utils.getVehicle();
+ mVehicle = Utils.getVehicleWithTimeout(DEFAULT_WAIT_TIMEOUT_MS);
mVehicle.getPropConfigs(
Lists.newArrayList(VhalEventGenerator.GENERATE_FAKE_DATA_CONTROLLING_PROPERTY),
(status, propConfigs) -> assumeTrue(status == StatusCode.OK));
diff --git a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2ePerformanceTest.java b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2ePerformanceTest.java
index 56ce568..6399549 100644
--- a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2ePerformanceTest.java
+++ b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/E2ePerformanceTest.java
@@ -183,10 +183,10 @@
final int EXPECTED_INVOCATIONS = 1000; // How many time get/set will be called.
- final int EXPECTED_DURATION_MS = 2500;
+ final int EXPECTED_DURATION_MS = 3000;
// This is a stress test and it can be flaky because it shares resources with all currently
// running process. Let's have this number of attempt before giving up.
- final int ATTEMPTS = 3;
+ final int ATTEMPTS = 5;
for (int curAttempt = 0; curAttempt < ATTEMPTS; curAttempt++) {
long missingInvocations = stressTestHvacProperties(mgr, cfg,
diff --git a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2FreezeFrameTest.java b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2FreezeFrameTest.java
index 0d6048d..fb09ebd 100644
--- a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2FreezeFrameTest.java
+++ b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2FreezeFrameTest.java
@@ -18,6 +18,7 @@
import static com.android.car.vehiclehal.test.Utils.isVhalPropertyAvailable;
import static com.android.car.vehiclehal.test.Utils.readVhalProperty;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;
@@ -37,12 +38,12 @@
/** Test retrieving the OBD2_FREEZE_FRAME property from VHAL */
public class Obd2FreezeFrameTest {
private static final String TAG = Utils.concatTag(Obd2FreezeFrameTest.class);
-
+ private static final int DEFAULT_WAIT_TIMEOUT_MS = 5000;
private IVehicle mVehicle = null;
@Before
public void setUp() throws Exception {
- mVehicle = Utils.getVehicle();
+ mVehicle = Utils.getVehicleWithTimeout(DEFAULT_WAIT_TIMEOUT_MS);
assumeTrue("Freeze frame not available, test-case ignored.", isFreezeFrameAvailable());
}
diff --git a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2LiveFrameTest.java b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2LiveFrameTest.java
index 25f2454..627e032 100644
--- a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2LiveFrameTest.java
+++ b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Obd2LiveFrameTest.java
@@ -18,6 +18,7 @@
import static com.android.car.vehiclehal.test.Utils.isVhalPropertyAvailable;
import static com.android.car.vehiclehal.test.Utils.readVhalProperty;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;
@@ -35,12 +36,12 @@
/** Test retrieving the OBD2_LIVE_FRAME property from VHAL */
public class Obd2LiveFrameTest {
private static final String TAG = Utils.concatTag(Obd2LiveFrameTest.class);
-
+ private static final int DEFAULT_WAIT_TIMEOUT_MS = 5000;
private IVehicle mVehicle = null;
@Before
public void setUp() throws Exception {
- mVehicle = Utils.getVehicle();
+ mVehicle = Utils.getVehicleWithTimeout(DEFAULT_WAIT_TIMEOUT_MS);
assumeTrue("Live frame not available, test-case ignored.", isLiveFrameAvailable());
}
diff --git a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Utils.java b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Utils.java
index 496b504..952f430 100644
--- a/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Utils.java
+++ b/tests/vehiclehal_test/src/com/android/car/vehiclehal/test/Utils.java
@@ -16,6 +16,9 @@
package com.android.car.vehiclehal.test;
+import static android.os.SystemClock.elapsedRealtime;
+
+import android.annotation.Nullable;
import android.car.hardware.CarPropertyValue;
import android.hardware.automotive.vehicle.V2_0.IVehicle;
import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
@@ -81,15 +84,32 @@
return readVhalProperty(vehicle, request, f);
}
- static IVehicle getVehicle() throws RemoteException {
- IVehicle service;
+ @Nullable
+ private static IVehicle getVehicle() {
try {
- service = IVehicle.getService();
+ return IVehicle.getService();
} catch (NoSuchElementException ex) {
- throw new RuntimeException("Couldn't connect to vehicle@2.0", ex);
+ Log.e(TAG, "IVehicle service not registered yet", ex);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get IVehicle Service ", e);
}
- Log.d(TAG, "Connected to IVehicle service: " + service);
- return service;
+ Log.d(TAG, "Failed to connect to IVehicle service");
+ return null;
+ }
+
+ static IVehicle getVehicleWithTimeout(long waitMilliseconds) {
+ IVehicle vehicle = getVehicle();
+ long endTime = elapsedRealtime() + waitMilliseconds;
+ while (vehicle == null && endTime > elapsedRealtime()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Sleep was interrupted", e);
+ }
+ vehicle = getVehicle();
+ }
+
+ return vehicle;
}
/**