Merge "Fix bind/unbind for projection receiver app"
diff --git a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java b/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
index 45babf7..f93c1b4 100644
--- a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
+++ b/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
@@ -55,6 +55,7 @@
private static final String SP_HANDLE_KEY = "sp-test";
private static final int FINE_LOCATION_REQUEST_CODE = 42;
+ private PagedListView mList;
private OutputAdapter mOutputAdapter;
private BluetoothDevice mBluetoothDevice;
private ICarTrustAgentBleService mCarTrustAgentBleService;
@@ -87,8 +88,8 @@
mOutputAdapter = new OutputAdapter();
- PagedListView list = findViewById(R.id.list);
- list.setAdapter(mOutputAdapter);
+ mList = findViewById(R.id.list);
+ mList.setAdapter(mOutputAdapter);
}
@Override
@@ -114,7 +115,10 @@
}
private void appendOutputText(String text) {
- runOnUiThread(() -> mOutputAdapter.addOutput(text));
+ runOnUiThread(() -> {
+ mOutputAdapter.addOutput(text);
+ mList.scrollToPosition(mOutputAdapter.getItemCount() - 1);
+ });
}
private void addEscrowToken(byte[] token) throws RemoteException {
diff --git a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java b/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
index 2cc2080..d70fd2f 100644
--- a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
+++ b/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
@@ -18,6 +18,8 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseSettings;
import android.car.trust.ICarTrustAgentBleCallback;
import android.car.trust.ICarTrustAgentBleService;
import android.car.trust.ICarTrustAgentEnrolmentCallback;
@@ -27,12 +29,12 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
-import android.os.ParcelUuid;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import java.nio.ByteBuffer;
+import java.text.DateFormat;
import java.util.UUID;
/**
@@ -41,7 +43,6 @@
* {@link ICarTrustAgentBleService}.
*/
public class CarTrustAgentBleService extends SimpleBleServer {
-
private static final String TAG = CarTrustAgentBleService.class.getSimpleName();
private RemoteCallbackList<ICarTrustAgentBleCallback> mBleCallbacks;
@@ -51,15 +52,14 @@
private ICarTrustAgentTokenResponseCallback mTokenResponseCallback;
private CarTrustAgentBleWrapper mCarTrustBleService;
- private ParcelUuid mEnrolmentUuid;
- private BluetoothGattCharacteristic mEnrolmentEscrowToken;
- private BluetoothGattCharacteristic mEnrolmentTokenHandle;
- private BluetoothGattService mEnrolmentGattServer;
+ private UUID mEnrolmentEscrowTokenUuid;
+ private UUID mEnrolmentTokenHandleUuid;
+ private BluetoothGattService mEnrolmentGattService;
- private ParcelUuid mUnlockUuid;
- private BluetoothGattCharacteristic mUnlockEscrowToken;
- private BluetoothGattCharacteristic mUnlockTokenHandle;
- private BluetoothGattService mUnlockGattServer;
+ private UUID mUnlockEscrowTokenUuid;
+ private UUID mUnlockTokenHandleUuid;
+ private BluetoothGattService mUnlockGattService;
+
private byte[] mCurrentUnlockToken;
private Long mCurrentUnlockHandle;
@@ -73,6 +73,11 @@
mUnlockCallbacks = new RemoteCallbackList<>();
mCarTrustBleService = new CarTrustAgentBleWrapper();
+ mEnrolmentEscrowTokenUuid = UUID.fromString(getString(R.string.enrollment_token_uuid));
+ mEnrolmentTokenHandleUuid = UUID.fromString(getString(R.string.enrollment_handle_uuid));
+ mUnlockEscrowTokenUuid = UUID.fromString(getString(R.string.unlock_escrow_token_uiid));
+ mUnlockTokenHandleUuid = UUID.fromString(getString(R.string.unlock_handle_uiid));
+
setupEnrolmentBleServer();
setupUnlockBleServer();
@@ -97,8 +102,12 @@
BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
responseNeeded, int offset, byte[] value) {
UUID uuid = characteristic.getUuid();
- Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
- if (uuid.equals(mEnrolmentEscrowToken.getUuid())) {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
+ }
+
+ if (uuid.equals(mEnrolmentEscrowTokenUuid)) {
final int callbackCount = mEnrolmentCallbacks.beginBroadcast();
for (int i = 0; i < callbackCount; i++) {
try {
@@ -108,10 +117,10 @@
}
}
mEnrolmentCallbacks.finishBroadcast();
- } else if (uuid.equals(mUnlockEscrowToken.getUuid())) {
+ } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
mCurrentUnlockToken = value;
maybeSendUnlockToken();
- } else if (uuid.equals(mUnlockTokenHandle.getUuid())) {
+ } else if (uuid.equals(mUnlockTokenHandleUuid)) {
mCurrentUnlockHandle = getLong(value);
maybeSendUnlockToken();
}
@@ -175,59 +184,67 @@
mBleCallbacks.finishBroadcast();
}
+ @Override
+ public void onDestroy() {
+ stopAdvertising(mEnrolmentAdvertisingCallback);
+ stopAdvertising(mUnlockAdvertisingCallback);
+ super.onDestroy();
+ }
+
private void setupEnrolmentBleServer() {
- mEnrolmentUuid = new ParcelUuid(
- UUID.fromString(getString(R.string.enrollment_service_uuid)));
- mEnrolmentGattServer = new BluetoothGattService(
+ mEnrolmentGattService = new BluetoothGattService(
UUID.fromString(getString(R.string.enrollment_service_uuid)),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
// Characteristic to describe the escrow token being used for unlock
- mEnrolmentEscrowToken = new BluetoothGattCharacteristic(
- UUID.fromString(getString(R.string.enrollment_token_uuid)),
+ BluetoothGattCharacteristic enrolmentEscrowToken = new BluetoothGattCharacteristic(
+ mEnrolmentEscrowTokenUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
// Characteristic to describe the handle being used for this escrow token
- mEnrolmentTokenHandle = new BluetoothGattCharacteristic(
- UUID.fromString(getString(R.string.enrollment_handle_uuid)),
+ BluetoothGattCharacteristic enrolmentTokenHandle = new BluetoothGattCharacteristic(
+ mEnrolmentTokenHandleUuid,
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
- mEnrolmentGattServer.addCharacteristic(mEnrolmentEscrowToken);
- mEnrolmentGattServer.addCharacteristic(mEnrolmentTokenHandle);
+ mEnrolmentGattService.addCharacteristic(enrolmentEscrowToken);
+ mEnrolmentGattService.addCharacteristic(enrolmentTokenHandle);
}
private void setupUnlockBleServer() {
- mUnlockUuid = new ParcelUuid(
- UUID.fromString(getString(R.string.unlock_service_uuid)));
- mUnlockGattServer = new BluetoothGattService(
+ mUnlockGattService = new BluetoothGattService(
UUID.fromString(getString(R.string.unlock_service_uuid)),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
// Characteristic to describe the escrow token being used for unlock
- mUnlockEscrowToken = new BluetoothGattCharacteristic(
- UUID.fromString(getString(R.string.unlock_escrow_token_uiid)),
+ BluetoothGattCharacteristic unlockEscrowToken = new BluetoothGattCharacteristic(
+ mUnlockEscrowTokenUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
// Characteristic to describe the handle being used for this escrow token
- mUnlockTokenHandle = new BluetoothGattCharacteristic(
- UUID.fromString(getString(R.string.unlock_handle_uiid)),
+ BluetoothGattCharacteristic unlockTokenHandle = new BluetoothGattCharacteristic(
+ mUnlockTokenHandleUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
- mUnlockGattServer.addCharacteristic(mUnlockEscrowToken);
- mUnlockGattServer.addCharacteristic(mUnlockTokenHandle);
+ mUnlockGattService.addCharacteristic(unlockEscrowToken);
+ mUnlockGattService.addCharacteristic(unlockTokenHandle);
}
private synchronized void maybeSendUnlockToken() {
if (mCurrentUnlockToken == null || mCurrentUnlockHandle == null) {
return;
}
- Log.d(TAG, "Handle and token both received, requesting unlock. Time: "
- + System.currentTimeMillis());
- final int callbackCount = mUnlockCallbacks.beginBroadcast();
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Handle and token both received, requesting unlock. Time: "
+ + DateFormat.getDateInstance(DateFormat.SHORT).format(
+ System.currentTimeMillis()));
+ }
+
+ int callbackCount = mUnlockCallbacks.beginBroadcast();
for (int i = 0; i < callbackCount; i++) {
try {
mUnlockCallbacks.getBroadcastItem(i).onUnlockDataReceived(
@@ -254,6 +271,46 @@
return buffer.getLong();
}
+ private final AdvertiseCallback mEnrolmentAdvertisingCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ onAdvertiseStartSuccess();
+
+ 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);
+ onAdvertiseStartFailure(errorCode);
+ }
+ };
+
+ private final AdvertiseCallback mUnlockAdvertisingCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ onAdvertiseStartSuccess();
+
+ 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);
+ onAdvertiseStartFailure(errorCode);
+ }
+ };
+
private final class CarTrustAgentBleWrapper extends ICarTrustAgentBleService.Stub {
@Override
public void registerBleCallback(ICarTrustAgentBleCallback callback) {
@@ -267,22 +324,33 @@
@Override
public void startEnrolmentAdvertising() {
+ stopEnrolmentAdvertising();
stopUnlockAdvertising();
- Log.d(TAG, "startEnrolmentAdvertising");
- startAdvertising(mEnrolmentUuid, mEnrolmentGattServer);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "startEnrolmentAdvertising");
+ }
+ startAdvertising(mEnrolmentGattService, mEnrolmentAdvertisingCallback);
}
@Override
public void stopEnrolmentAdvertising() {
- Log.d(TAG, "stopEnrolmentAdvertising");
- stopAdvertising();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "stopEnrolmentAdvertising");
+ }
+
+ stopAdvertising(mEnrolmentAdvertisingCallback);
}
@Override
public void sendEnrolmentHandle(BluetoothDevice device, long handle) {
- Log.d(TAG, "sendEnrolmentHandle: " + handle);
- mEnrolmentTokenHandle.setValue(getBytes(handle));
- notifyCharacteristicChanged(device, mEnrolmentTokenHandle, false);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sendEnrolmentHandle: " + handle);
+ }
+ BluetoothGattCharacteristic enrolmentTokenHandle =
+ mEnrolmentGattService.getCharacteristic(mEnrolmentTokenHandleUuid);
+ enrolmentTokenHandle.setValue(getBytes(handle));
+ notifyCharacteristicChanged(device, enrolmentTokenHandle, false);
}
@Override
@@ -297,15 +365,21 @@
@Override
public void startUnlockAdvertising() {
+ stopUnlockAdvertising();
stopEnrolmentAdvertising();
- Log.d(TAG, "startUnlockAdvertising");
- startAdvertising(mUnlockUuid, mUnlockGattServer);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "startUnlockAdvertising");
+ }
+ startAdvertising(mUnlockGattService, mUnlockAdvertisingCallback);
}
@Override
public void stopUnlockAdvertising() {
- Log.d(TAG, "stopUnlockAdvertising");
- stopAdvertising();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "stopUnlockAdvertising");
+ }
+ stopAdvertising(mUnlockAdvertisingCallback);
}
@Override
@@ -359,7 +433,10 @@
@Override
public void onEscrowTokenAdded(byte[] token, long handle, int uid)
throws RemoteException {
- Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
+ }
+
mTokenHandleSharedPreferences.edit()
.putInt(String.valueOf(handle), uid)
.apply();
@@ -370,7 +447,10 @@
@Override
public void onEscrowTokenRemoved(long handle, boolean successful) throws RemoteException {
- Log.d(TAG, "onEscrowTokenRemoved handle:" + handle);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onEscrowTokenRemoved handle:" + handle);
+ }
+
mTokenHandleSharedPreferences.edit()
.remove(String.valueOf(handle))
.apply();
diff --git a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java b/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
index 7fbf2fe..260f5ac 100644
--- a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
+++ b/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
@@ -41,72 +41,11 @@
* A generic service to start a BLE
*/
public abstract class SimpleBleServer extends Service {
-
private static final String TAG = SimpleBleServer.class.getSimpleName();
private static final int BLE_RETRY_LIMIT = 5;
private static final int BLE_RETRY_INTERVAL_MS = 1000;
- private final AdvertiseCallback mAdvertisingCallback = new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- Log.d(TAG, "Successfully started advertising service");
- onAdvertiseStartSuccess();
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- super.onStartFailure(errorCode);
- Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
- onAdvertiseStartFailure(errorCode);
- }
- };
-
- private final BluetoothGattServerCallback mGattServerCallback =
- new BluetoothGattServerCallback() {
- @Override
- public void onConnectionStateChange(BluetoothDevice device,
- final int status, final int newState) {
- Log.d(TAG, "GattServer connection change status: " + status
- + " newState: " + newState
- + " device name: " + device.getName());
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- onAdvertiseDeviceConnected(device);
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- onAdvertiseDeviceDisconnected(device);
- break;
- }
- }
-
- @Override
- public void onServiceAdded(final int status, BluetoothGattService service) {
- Log.d(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
- }
-
- @Override
- public void onCharacteristicReadRequest(BluetoothDevice device,
- int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
- 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) {
- 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);
- }
- };
-
private final Handler mHandler = new Handler();
private BluetoothManager mBluetoothManager;
@@ -116,29 +55,32 @@
/**
* Starts the GATT server with the given {@link BluetoothGattService} and begins
- * advertising with the {@link ParcelUuid}.
+ * 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 advertiseUuid Service Uuid used in the {@link AdvertiseData}
* @param service {@link BluetoothGattService} that will be discovered by clients
*/
- protected void startAdvertising(ParcelUuid advertiseUuid, BluetoothGattService service) {
+ protected void startAdvertising(BluetoothGattService service,
+ AdvertiseCallback advertiseCallback) {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.e(TAG, "System does not support BLE");
return;
}
- mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
- mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
+ // Only open one Gatt server.
if (mGattServer == null) {
- Log.e(TAG, "Gatt Server not created");
- return;
+ mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
+
+ if (mGattServer == null) {
+ Log.e(TAG, "Gatt Server not created");
+ return;
+ }
}
- // We only allow adding one service in this implementation. If multiple services need
- // to be added, then they need to be queued up and added only after
- // BluetoothGattServerCallback.onServiceAdded is called.
+ mGattServer.clearServices();
mGattServer.addService(service);
AdvertiseSettings settings = new AdvertiseSettings.Builder()
@@ -149,29 +91,31 @@
AdvertiseData data = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
- .addServiceUuid(advertiseUuid)
+ .addServiceUuid(new ParcelUuid(service.getUuid()))
.build();
mAdvertiserStartCount = 0;
- startAdvertisingInternally(settings, data);
+ startAdvertisingInternally(settings, data, advertiseCallback);
}
- private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data) {
+ 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),
- BLE_RETRY_INTERVAL_MS);
+ mHandler.postDelayed(
+ () -> startAdvertisingInternally(settings, data, advertiseCallback),
+ BLE_RETRY_INTERVAL_MS);
} else {
mHandler.removeCallbacks(null);
- mAdvertiser.startAdvertising(settings, data, mAdvertisingCallback);
+ mAdvertiser.startAdvertising(settings, data, advertiseCallback);
mAdvertiserStartCount = 0;
}
}
- protected void stopAdvertising() {
+ protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
if (mAdvertiser != null) {
- mAdvertiser.stopAdvertising(mAdvertisingCallback);
+ mAdvertiser.stopAdvertising(advertiseCallback);
}
}
@@ -189,7 +133,6 @@
public void onDestroy() {
// Stops the advertiser and GATT server. This needs to be done to avoid leaks
if (mAdvertiser != null) {
- mAdvertiser.stopAdvertising(mAdvertisingCallback);
mAdvertiser.cleanup();
}
@@ -228,4 +171,55 @@
protected abstract void onCharacteristicRead(BluetoothDevice device,
int requestId, int offset, final 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/car-lib/api/current.txt b/car-lib/api/current.txt
index 6744238..4d000ee 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -447,20 +447,31 @@
method public android.car.media.CarAudioPatchHandle createAudioPatch(java.lang.String, int, int) throws android.car.CarNotConnectedException;
method public java.lang.String[] getExternalSources() throws android.car.CarNotConnectedException;
method public int getGroupMaxVolume(int) throws android.car.CarNotConnectedException;
+ method public int getGroupMaxVolume(int, int) throws android.car.CarNotConnectedException;
method public int getGroupMinVolume(int) throws android.car.CarNotConnectedException;
+ method public int getGroupMinVolume(int, int) throws android.car.CarNotConnectedException;
method public int getGroupVolume(int) throws android.car.CarNotConnectedException;
+ method public int getGroupVolume(int, int) throws android.car.CarNotConnectedException;
method public int[] getUsagesForVolumeGroupId(int) throws android.car.CarNotConnectedException;
+ method public int[] getUsagesForVolumeGroupId(int, int) throws android.car.CarNotConnectedException;
method public int getVolumeGroupCount() throws android.car.CarNotConnectedException;
+ method public int getVolumeGroupCount(int) throws android.car.CarNotConnectedException;
method public int getVolumeGroupIdForUsage(int) throws android.car.CarNotConnectedException;
- method public static java.lang.String getVolumeSettingsKeyForGroup(int);
- method public void registerVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
- method public void registerVolumeChangeObserver(android.database.ContentObserver);
+ method public int getVolumeGroupIdForUsage(int, int) throws android.car.CarNotConnectedException;
+ method public void registerCarVolumeCallback(android.car.media.CarAudioManager.CarVolumeCallback);
method public void releaseAudioPatch(android.car.media.CarAudioPatchHandle) throws android.car.CarNotConnectedException;
method public void setBalanceTowardRight(float) throws android.car.CarNotConnectedException;
method public void setFadeTowardFront(float) throws android.car.CarNotConnectedException;
method public void setGroupVolume(int, int, int) throws android.car.CarNotConnectedException;
- method public void unregisterVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
- method public void unregisterVolumeChangeObserver(android.database.ContentObserver);
+ method public void setGroupVolume(int, int, int, int) throws android.car.CarNotConnectedException;
+ method public void unregisterCarVolumeCallback(android.car.media.CarAudioManager.CarVolumeCallback);
+ field public static final int PRIMARY_AUDIO_ZONE = 0; // 0x0
+ }
+
+ public static abstract class CarAudioManager.CarVolumeCallback {
+ ctor public CarAudioManager.CarVolumeCallback();
+ method public void onGroupVolumeChanged(int, int, int);
+ method public void onMasterMuteChanged(int, int);
}
public final class CarAudioPatchHandle implements android.os.Parcelable {
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index fcbe439..a0046eb 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -734,29 +734,6 @@
}
-package android.car.media {
-
- public final class CarAudioManager {
- method public android.car.media.CarAudioPatchHandle createAudioPatch(java.lang.String, int, int) throws android.car.CarNotConnectedException;
- method public java.lang.String[] getExternalSources() throws android.car.CarNotConnectedException;
- method public int getGroupMaxVolume(int) throws android.car.CarNotConnectedException;
- method public int getGroupMinVolume(int) throws android.car.CarNotConnectedException;
- method public int getGroupVolume(int) throws android.car.CarNotConnectedException;
- method public int[] getUsagesForVolumeGroupId(int) throws android.car.CarNotConnectedException;
- method public int getVolumeGroupCount() throws android.car.CarNotConnectedException;
- method public int getVolumeGroupIdForUsage(int) throws android.car.CarNotConnectedException;
- method public void registerVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
- method public void registerVolumeChangeObserver(android.database.ContentObserver);
- method public void releaseAudioPatch(android.car.media.CarAudioPatchHandle) throws android.car.CarNotConnectedException;
- method public void setBalanceTowardRight(float) throws android.car.CarNotConnectedException;
- method public void setFadeTowardFront(float) throws android.car.CarNotConnectedException;
- method public void setGroupVolume(int, int, int) throws android.car.CarNotConnectedException;
- method public void unregisterVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
- method public void unregisterVolumeChangeObserver(android.database.ContentObserver);
- }
-
-}
-
package android.car.navigation {
public class CarNavigationInstrumentCluster implements android.os.Parcelable {
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index c1bf502..e262e91 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -405,7 +405,16 @@
* @hide
*/
@SystemApi
- public static final String PERMISSION_CAR_DIAGNOSTIC_CLEAR = "android.car.permission.CLEAR_CAR_DIAGNOSTICS";
+ public static final String PERMISSION_CAR_DIAGNOSTIC_CLEAR =
+ "android.car.permission.CLEAR_CAR_DIAGNOSTICS";
+
+ /**
+ * Permission necessary to configure UX restrictions through {@link CarUxRestrictionsManager}.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION =
+ "android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION";
/**
* Permissions necessary to clear diagnostic information.
@@ -413,7 +422,8 @@
* @hide
*/
@SystemApi
- public static final String PERMISSION_STORAGE_MONITORING = "android.car.permission.STORAGE_MONITORING";
+ public static final String PERMISSION_STORAGE_MONITORING =
+ "android.car.permission.STORAGE_MONITORING";
/** Type of car connection: platform runs directly in car. */
public static final int CONNECTION_TYPE_EMBEDDED = 5;
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/colors.xml b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
similarity index 67%
rename from tests/EmbeddedKitchenSinkApp/res/values/colors.xml
rename to car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
index 9a1ed89..e6f85b5 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/colors.xml
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
@@ -1,20 +1,19 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright (c) 2017, The Android Open Source Project
+/*
+ * Copyright (C) 2018 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
+ * 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.
-*/
--->
-<resources>
- <color name="car_button_tint">#fffafafa</color>
-</resources>
\ No newline at end of file
+ */
+
+package android.car.drivingstate;
+
+parcelable CarUxRestrictionsConfiguration;
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
new file mode 100644
index 0000000..06d19df
--- /dev/null
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2018 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.drivingstate;
+
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Configuration for Car UX Restrictions service.
+ *
+ * @hide
+ */
+public final class CarUxRestrictionsConfiguration implements Parcelable {
+ private static final String TAG = "CarUxRConfig";
+
+ // Constants used by json de/serialization.
+ private static final String JSON_NAME_MAX_CONTENT_DEPTH = "max_content_depth";
+ private static final String JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS =
+ "max_cumulative_content_items";
+ private static final String JSON_NAME_MAX_STRING_LENGTH = "max_string_length";
+ private static final String JSON_NAME_MOVING_RESTRICTIONS = "moving_restrictions";
+ 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_REQ_OPT = "req_opt";
+ private static final String JSON_NAME_RESTRICTIONS = "restrictions";
+ private static final String JSON_NAME_SPEED_RANGE = "speed_range";
+ private static final String JSON_NAME_MIN_SPEED = "min_speed";
+ private static final String JSON_NAME_MAX_SPEED = "max_speed";
+
+ private final int mMaxContentDepth;
+ private final int mMaxCumulativeContentItems;
+ private final int mMaxStringLength;
+ private final Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+ new ArrayMap<>(DRIVING_STATES.length);
+
+ private CarUxRestrictionsConfiguration(CarUxRestrictionsConfiguration.Builder builder) {
+ mMaxContentDepth = builder.mMaxContentDepth;
+ mMaxCumulativeContentItems = builder.mMaxCumulativeContentItems;
+ mMaxStringLength = builder.mMaxStringLength;
+
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> list = new ArrayList<>();
+ for (RestrictionsPerSpeedRange r : builder.mUxRestrictions.get(drivingState)) {
+ list.add(r);
+ }
+ mUxRestrictions.put(drivingState, list);
+ }
+ }
+
+ /**
+ * Returns the restrictions based on current driving state and speed.
+ */
+ public CarUxRestrictions getUxRestrictions(@CarDrivingState int drivingState,
+ float currentSpeed) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+ if (restrictions.isEmpty()) {
+ 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);
+ }
+
+ private CarUxRestrictions createDefaultUxRestrictionsEvent() {
+ return createUxRestrictionsEvent(true,
+ CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
+ }
+
+ /**
+ * Creates CarUxRestrictions with restrictions parameters from current configuration.
+ */
+ private CarUxRestrictions createUxRestrictionsEvent(boolean requiresOpt,
+ @CarUxRestrictions.CarUxRestrictionsInfo int uxr) {
+ // In case the UXR is not baseline, set requiresDistractionOptimization to true since it
+ // doesn't make sense to have an active non baseline restrictions without
+ // requiresDistractionOptimization set to true.
+ if (uxr != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
+ requiresOpt = true;
+ }
+ CarUxRestrictions.Builder builder = new CarUxRestrictions.Builder(requiresOpt, uxr,
+ SystemClock.elapsedRealtimeNanos());
+ if (mMaxStringLength != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxStringLength(mMaxStringLength);
+ }
+ if (mMaxCumulativeContentItems != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxCumulativeContentItems(mMaxCumulativeContentItems);
+ }
+ if (mMaxContentDepth != Builder.UX_RESTRICTIONS_UNKNOWN) {
+ builder.setMaxContentDepth(mMaxContentDepth);
+ }
+ return builder.build();
+ }
+
+ // Json de/serialization methods.
+
+ /**
+ * Writes current configuration as Json.
+ */
+ public void writeJson(JsonWriter writer) throws IOException {
+ // We need to be lenient to accept infinity number (as max speed).
+ writer.setLenient(true);
+
+ writer.beginObject();
+
+ writer.name(JSON_NAME_MAX_CONTENT_DEPTH).value(mMaxContentDepth);
+ writer.name(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS).value(
+ mMaxCumulativeContentItems);
+ writer.name(JSON_NAME_MAX_STRING_LENGTH).value(mMaxStringLength);
+
+ writer.name(JSON_NAME_PARKED_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_PARKED));
+
+ writer.name(JSON_NAME_IDLING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_IDLING));
+
+ writer.name(JSON_NAME_MOVING_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_MOVING));
+
+ writer.name(JSON_NAME_UNKNOWN_RESTRICTIONS);
+ writeRestrictionsList(writer,
+ mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN));
+
+ writer.endObject();
+ }
+
+ private void writeRestrictionsList(JsonWriter writer, List<RestrictionsPerSpeedRange> messages)
+ throws IOException {
+ writer.beginArray();
+ for (RestrictionsPerSpeedRange restrictions : messages) {
+ writeRestrictions(writer, restrictions);
+ }
+ writer.endArray();
+ }
+
+ private void writeRestrictions(JsonWriter writer, RestrictionsPerSpeedRange restrictions)
+ throws IOException {
+ writer.beginObject();
+ writer.name(JSON_NAME_REQ_OPT).value(restrictions.mReqOpt);
+ writer.name(JSON_NAME_RESTRICTIONS).value(restrictions.mRestrictions);
+ if (restrictions.mSpeedRange != null) {
+ writer.name(JSON_NAME_SPEED_RANGE);
+ writer.beginObject();
+ writer.name(JSON_NAME_MIN_SPEED).value(restrictions.mSpeedRange.mMinSpeed);
+ writer.name(JSON_NAME_MAX_SPEED).value(restrictions.mSpeedRange.mMaxSpeed);
+ writer.endObject();
+ }
+ writer.endObject();
+ }
+
+ /**
+ * Reads Json as UX restriction configuration.
+ */
+ public static CarUxRestrictionsConfiguration readJson(JsonReader reader) throws IOException {
+ // We need to be lenient to accept infinity number (as max speed).
+ reader.setLenient(true);
+
+ Builder builder = new Builder();
+ 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();
+ }
+ }
+ reader.endObject();
+ return builder.build();
+ }
+
+ private static void readRestrictionsList(JsonReader reader, @CarDrivingState int drivingState,
+ Builder builder) throws IOException {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ readRestrictions(reader, drivingState, builder);
+ }
+ reader.endArray();
+ }
+
+ private static void readRestrictions(JsonReader reader, @CarDrivingState int drivingState,
+ Builder builder) throws IOException {
+ reader.beginObject();
+ boolean reqOpt = false;
+ int restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+ Builder.SpeedRange speedRange = null;
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals(JSON_NAME_REQ_OPT)) {
+ reqOpt = reader.nextBoolean();
+ } else if (name.equals(JSON_NAME_RESTRICTIONS)) {
+ restrictions = reader.nextInt();
+ } else if (name.equals(JSON_NAME_SPEED_RANGE)) {
+ reader.beginObject();
+ // Okay to set min initial value as MAX_SPEED because SpeedRange() won't allow it.
+ float minSpeed = Builder.SpeedRange.MAX_SPEED;
+ float maxSpeed = Builder.SpeedRange.MAX_SPEED;
+
+ while (reader.hasNext()) {
+ String n = reader.nextName();
+ if (n.equals(JSON_NAME_MIN_SPEED)) {
+ minSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+ } else if (n.equals(JSON_NAME_MAX_SPEED)) {
+ maxSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+ } else {
+ Log.e(TAG, "Unknown name parsing json config: " + n);
+ reader.skipValue();
+ }
+ }
+ speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+ reader.endObject();
+ }
+ }
+ reader.endObject();
+ builder.setUxRestrictions(drivingState, speedRange, reqOpt, restrictions);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof CarUxRestrictionsConfiguration)) {
+ return false;
+ }
+
+ CarUxRestrictionsConfiguration other = (CarUxRestrictionsConfiguration) obj;
+
+ // Compare UXR parameters.
+ if (mMaxContentDepth != other.mMaxContentDepth
+ || mMaxCumulativeContentItems != other.mMaxCumulativeContentItems
+ || mMaxStringLength != other.mMaxStringLength) {
+ return false;
+ }
+
+ // Compare UXR by driving state.
+ if (!mUxRestrictions.keySet().equals(other.mUxRestrictions.keySet())) {
+ 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 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;
+ }
+
+ /**
+ * 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("Max String length: " + mMaxStringLength);
+ writer.println("Max Cumulative Content Items: " + mMaxCumulativeContentItems);
+ writer.println("Max Content depth: " + mMaxContentDepth);
+ }
+
+ private static String getDrivingStateName(@CarDrivingState int state) {
+ switch (state) {
+ case 0:
+ return "parked";
+ case 1:
+ return "idling";
+ case 2:
+ return "moving";
+ default:
+ return "unknown";
+ }
+ }
+
+ // 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
+ };
+
+ public static final Parcelable.Creator<CarUxRestrictionsConfiguration> CREATOR =
+ new Parcelable.Creator<CarUxRestrictionsConfiguration>() {
+
+ @Override
+ public CarUxRestrictionsConfiguration createFromParcel(Parcel source) {
+ return new CarUxRestrictionsConfiguration(source);
+ }
+
+ @Override
+ public CarUxRestrictionsConfiguration[] newArray(int size) {
+ return new CarUxRestrictionsConfiguration[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private CarUxRestrictionsConfiguration(Parcel in) {
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = new ArrayList<>();
+ in.readTypedList(restrictions, RestrictionsPerSpeedRange.CREATOR);
+ mUxRestrictions.put(drivingState, restrictions);
+ }
+ mMaxContentDepth = in.readInt();
+ mMaxCumulativeContentItems = in.readInt();
+ mMaxStringLength = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ for (int drivingState : DRIVING_STATES) {
+ dest.writeTypedList(mUxRestrictions.get(drivingState), 0);
+ }
+ dest.writeInt(mMaxContentDepth);
+ dest.writeInt(mMaxCumulativeContentItems);
+ dest.writeInt(mMaxStringLength);
+ }
+
+ /**
+ * @hide
+ */
+ public static final class Builder {
+
+ private static final int UX_RESTRICTIONS_UNKNOWN = -1;
+
+ private int mMaxContentDepth = UX_RESTRICTIONS_UNKNOWN;
+ private int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN;
+ private int mMaxStringLength = UX_RESTRICTIONS_UNKNOWN;
+
+ private Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+ new ArrayMap<>(DRIVING_STATES.length);
+
+ public Builder() {
+ for (int drivingState : DRIVING_STATES) {
+ mUxRestrictions.put(drivingState, new ArrayList<>());
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Sets ux restrictions with speed range.
+ *
+ * @param drivingState Restrictions will be set for this Driving state.
+ * See constants in {@link CarDrivingStateEvent}.
+ * @param speedRange If set, restrictions will only apply when current speed is within
+ * the range. Only {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}
+ * supports speed range. {@code null} implies the full speed range,
+ * i.e. zero to {@link SpeedRange#MAX_SPEED}.
+ * @param requiresOptimization Whether distraction optimization (DO) is required for this
+ * driving state.
+ * @param restrictions See constants in {@link CarUxRestrictions}.
+ */
+ public Builder setUxRestrictions(@CarDrivingState int drivingState,
+ 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.");
+ }
+ }
+
+ mUxRestrictions.get(drivingState).add(
+ new RestrictionsPerSpeedRange(requiresOptimization, restrictions, speedRange));
+ return this;
+ }
+
+ /**
+ * Sets max string length.
+ */
+ public Builder setMaxStringLength(int maxStringLength) {
+ mMaxStringLength = maxStringLength;
+ return this;
+ }
+
+ /**
+ * Sets max cumulative content items.
+ */
+ public Builder setMaxCumulativeContentItems(int maxCumulativeContentItems) {
+ mMaxCumulativeContentItems = maxCumulativeContentItems;
+ return this;
+ }
+
+ /**
+ * Sets max content depth.
+ */
+ public Builder setMaxContentDepth(int maxContentDepth) {
+ mMaxContentDepth = maxContentDepth;
+ return this;
+ }
+
+ /**
+ * @return CarUxRestrictionsConfiguration based on builder configuration.
+ */
+ public CarUxRestrictionsConfiguration build() {
+ // Create default restriction for unspecified driving state.
+ for (int drivingState : DRIVING_STATES) {
+ List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+ if (restrictions.size() == 0) {
+ Log.i(TAG, "Using default restrictions for driving state: "
+ + getDrivingStateName(drivingState));
+ restrictions.add(new RestrictionsPerSpeedRange(
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED));
+ }
+ }
+
+ // Configuration validation.
+ 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 {
+ if (restrictions.size() != 1) {
+ throw new IllegalStateException("Non-moving driving state should contain "
+ + "one set of restriction rules.");
+ }
+ }
+ }
+ return new CarUxRestrictionsConfiguration(this);
+ }
+
+ /**
+ * restrictions should be sorted based on speed range.
+ */
+ private boolean isAllSpeedRangeCovered(List<RestrictionsPerSpeedRange> restrictions) {
+ if (restrictions.size() == 1) {
+ if (restrictions.get(0).mSpeedRange == null) {
+ // Single restriction with null speed range implies that
+ // it applies to the entire driving state.
+ return true;
+ }
+ return restrictions.get(0).mSpeedRange.mMinSpeed == 0
+ && Float.compare(restrictions.get(0).mSpeedRange.mMaxSpeed,
+ SpeedRange.MAX_SPEED) == 0;
+ }
+
+ if (restrictions.get(0).mSpeedRange.mMinSpeed != 0) {
+ Log.e(TAG, "Speed range min speed should start at 0.");
+ return false;
+ }
+ 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;
+ }
+ }
+ // The last speed range should have max speed.
+ float lastMaxSpeed = restrictions.get(restrictions.size() - 1).mSpeedRange.mMaxSpeed;
+ return lastMaxSpeed == SpeedRange.MAX_SPEED;
+ }
+
+ /**
+ * Speed range is defined by min and max speed. When there is no upper bound for max speed,
+ * set it to {@link SpeedRange#MAX_SPEED}.
+ */
+ public static final class SpeedRange implements Comparable<SpeedRange> {
+ public static final float MAX_SPEED = Float.POSITIVE_INFINITY;
+
+ private float mMinSpeed;
+ private float mMaxSpeed;
+
+ /**
+ * Defaults max speed to {@link SpeedRange#MAX_SPEED}.
+ */
+ public SpeedRange(@FloatRange(from = 0.0) float minSpeed) {
+ this(minSpeed, MAX_SPEED);
+ }
+
+ public SpeedRange(@FloatRange(from = 0.0) float minSpeed,
+ @FloatRange(from = 0.0) float maxSpeed) {
+ 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);
+ }
+ mMinSpeed = minSpeed;
+ mMaxSpeed = maxSpeed;
+ }
+
+ /**
+ * Return if the given speed is in the range of [minSpeed, maxSpeed).
+ *
+ * @param speed Speed to check
+ * @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;
+ }
+
+ @Override
+ public int compareTo(SpeedRange other) {
+ // First compare min speed; then max speed.
+ int minSpeedComparison = Float.compare(this.mMinSpeed, other.mMinSpeed);
+ if (minSpeedComparison != 0) {
+ return minSpeedComparison;
+ }
+
+ return Float.compare(this.mMaxSpeed, other.mMaxSpeed);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof SpeedRange)) {
+ return false;
+ }
+ SpeedRange other = (SpeedRange) obj;
+
+ return this.compareTo(other) == 0;
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ final boolean mReqOpt;
+ final int mRestrictions;
+ @Nullable
+ final Builder.SpeedRange mSpeedRange;
+
+ public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions) {
+ this(reqOpt, restrictions, null);
+ }
+
+ public RestrictionsPerSpeedRange(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.");
+ }
+ mReqOpt = reqOpt;
+ mRestrictions = restrictions;
+ mSpeedRange = speedRange;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof RestrictionsPerSpeedRange)) {
+ return false;
+ }
+ RestrictionsPerSpeedRange other = (RestrictionsPerSpeedRange) obj;
+ return mReqOpt == other.mReqOpt
+ && mRestrictions == other.mRestrictions
+ && ((mSpeedRange == null && other.mSpeedRange == null) || mSpeedRange.equals(
+ other.mSpeedRange));
+ }
+
+ // Parcelable methods/fields.
+
+ public static final Creator<RestrictionsPerSpeedRange> CREATOR =
+ new Creator<RestrictionsPerSpeedRange>() {
+ @Override
+ public RestrictionsPerSpeedRange createFromParcel(Parcel in) {
+ return new RestrictionsPerSpeedRange(in);
+ }
+
+ @Override
+ public RestrictionsPerSpeedRange[] newArray(int size) {
+ return new RestrictionsPerSpeedRange[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ protected RestrictionsPerSpeedRange(Parcel in) {
+ mReqOpt = in.readBoolean();
+ mRestrictions = in.readInt();
+ // Whether speed range is specified.
+ Builder.SpeedRange speedRange = null;
+ if (in.readBoolean()) {
+ float minSpeed = in.readFloat();
+ float maxSpeed = in.readFloat();
+ speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+ }
+ mSpeedRange = speedRange;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBoolean(mReqOpt);
+ dest.writeInt(mRestrictions);
+ // Whether speed range is specified.
+ dest.writeBoolean(mSpeedRange != null);
+ if (mSpeedRange != null) {
+ dest.writeFloat(mSpeedRange.mMinSpeed);
+ dest.writeFloat(mSpeedRange.mMaxSpeed);
+ }
+ }
+ }
+}
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 57e7d60..b93b9e3 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -18,10 +18,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.car.Car;
import android.car.CarManagerBase;
import android.car.CarNotConnectedException;
-import android.car.drivingstate.ICarUxRestrictionsManager;
import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
@@ -117,6 +117,31 @@
}
/**
+ * 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) throws CarNotConnectedException {
+ try {
+ return mUxRService.saveUxRestrictionsConfigurationForNextBoot(config);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not save new UX restrictions configuration", e);
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ /**
* Unregister the registered {@link OnUxRestrictionsChangedListener}
*/
public synchronized void unregisterListener()
diff --git a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
index e5c69b8..270c74e 100644
--- a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
+++ b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
@@ -17,6 +17,7 @@
package android.car.drivingstate;
import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
import android.car.drivingstate.ICarUxRestrictionsChangeListener;
/**
@@ -30,4 +31,5 @@
void registerUxRestrictionsChangeListener(in ICarUxRestrictionsChangeListener listener) = 0;
void unregisterUxRestrictionsChangeListener(in ICarUxRestrictionsChangeListener listener) = 1;
CarUxRestrictions getCurrentUxRestrictions() = 2;
+ boolean saveUxRestrictionsConfigurationForNextBoot(in CarUxRestrictionsConfiguration config) = 3;
}
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index 2b9e5af..4aa1ebf 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -16,70 +16,69 @@
package android.car.media;
import android.annotation.NonNull;
-import android.annotation.SystemApi;
import android.car.CarLibLog;
import android.car.CarManagerBase;
import android.car.CarNotConnectedException;
-import android.content.ContentResolver;
import android.content.Context;
-import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
-import android.provider.Settings;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+
/**
- * APIs for handling car specific audio stuff.
+ * APIs for handling audio in a car.
+ *
+ * In a car environment, we introduced the support to turn audio dynamic routing on /off by
+ * setting the "audioUseDynamicRouting" attribute in config.xml
+ *
+ * When audio dynamic routing is enabled:
+ * - Audio devices are grouped into zones
+ * - There is at least one primary zone, and extra secondary zones such as RSE
+ * (Reat Seat Entertainment)
+ * - Within each zone, audio devices are grouped into volume groups for volume control
+ * - Audio is assigned to an audio device based on its AudioAttributes usage
+ *
+ * When audio dynamic routing is disabled:
+ * - There is exactly one audio zone, which is the primary zone
+ * - Each volume group represents a controllable STREAM_TYPE, same as AudioManager
*/
public final class CarAudioManager implements CarManagerBase {
- // The trailing slash forms a directory-liked hierarchy and
- // allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
- private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
-
/**
- * @param groupId The volume group id
- * @return Key to persist volume index for volume group in {@link Settings.Global}
+ * Zone id of the primary audio zone.
*/
- public static String getVolumeSettingsKeyForGroup(int groupId) {
- return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + groupId;
- }
+ public static final int PRIMARY_AUDIO_ZONE = 0x0;
- /**
- * Key to persist master mute state in {@link Settings.Global}
- *
- * @hide
- */
- public static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
-
- private final ContentResolver mContentResolver;
private final ICarAudio mService;
+ private final List<CarVolumeCallback> mCarVolumeCallbacks;
+
+ private final ICarVolumeCallback mCarVolumeCallbackImpl = new ICarVolumeCallback.Stub() {
+ @Override
+ public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+ for (CarVolumeCallback callback : mCarVolumeCallbacks) {
+ callback.onGroupVolumeChanged(zoneId, groupId, flags);
+ }
+ }
+
+ @Override
+ public void onMasterMuteChanged(int zoneId, int flags) {
+ for (CarVolumeCallback callback : mCarVolumeCallbacks) {
+ callback.onMasterMuteChanged(zoneId, flags);
+ }
+ }
+ };
/**
- * Registers a {@link ContentObserver} to listen for volume group changes.
- * Note that this observer is valid for bus based car audio stack only.
+ * Sets the volume index for a volume group in primary zone.
*
- * {@link ContentObserver#onChange(boolean)} will be called on every group volume change.
- *
- * @param observer The {@link ContentObserver} instance to register, non-null
+ * @see {@link #setGroupVolume(int, int, int, int)}
*/
- @SystemApi
- public void registerVolumeChangeObserver(@NonNull ContentObserver observer) {
- mContentResolver.registerContentObserver(
- Settings.Global.getUriFor(VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX),
- true, observer);
- }
-
- /**
- * Unregisters the {@link ContentObserver} which listens for volume group changes.
- *
- * @param observer The {@link ContentObserver} instance to unregister, non-null
- */
- @SystemApi
- public void unregisterVolumeChangeObserver(@NonNull ContentObserver observer) {
- mContentResolver.unregisterContentObserver(observer);
+ public void setGroupVolume(int groupId, int index, int flags) throws CarNotConnectedException {
+ setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, index, flags);
}
/**
@@ -87,16 +86,17 @@
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume group is affected.
* @param groupId The volume group id whose volume index should be set.
* @param index The volume index to set. See
- * {@link #getGroupMaxVolume(int)} for the largest valid value.
+ * {@link #getGroupMaxVolume(int, int)} for the largest valid value.
* @param flags One or more flags (e.g., {@link android.media.AudioManager#FLAG_SHOW_UI},
* {@link android.media.AudioManager#FLAG_PLAY_SOUND})
*/
- @SystemApi
- public void setGroupVolume(int groupId, int index, int flags) throws CarNotConnectedException {
+ public void setGroupVolume(int zoneId, int groupId, int index, int flags)
+ throws CarNotConnectedException {
try {
- mService.setGroupVolume(groupId, index, flags);
+ mService.setGroupVolume(zoneId, groupId, index, flags);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "setGroupVolume failed", e);
throw new CarNotConnectedException(e);
@@ -104,17 +104,26 @@
}
/**
+ * Returns the maximum volume index for a volume group in primary zone.
+ *
+ * @see {@link #getGroupMaxVolume(int, int)}
+ */
+ public int getGroupMaxVolume(int groupId) throws CarNotConnectedException {
+ return getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId);
+ }
+
+ /**
* Returns the maximum volume index for a volume group.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume group is queried.
* @param groupId The volume group id whose maximum volume index is returned.
* @return The maximum valid volume index for the given group.
*/
- @SystemApi
- public int getGroupMaxVolume(int groupId) throws CarNotConnectedException {
+ public int getGroupMaxVolume(int zoneId, int groupId) throws CarNotConnectedException {
try {
- return mService.getGroupMaxVolume(groupId);
+ return mService.getGroupMaxVolume(zoneId, groupId);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getGroupMaxVolume failed", e);
throw new CarNotConnectedException(e);
@@ -122,17 +131,26 @@
}
/**
+ * Returns the minimum volume index for a volume group in primary zone.
+ *
+ * @see {@link #getGroupMinVolume(int, int)}
+ */
+ public int getGroupMinVolume(int groupId) throws CarNotConnectedException {
+ return getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+ }
+
+ /**
* Returns the minimum volume index for a volume group.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume group is queried.
* @param groupId The volume group id whose minimum volume index is returned.
* @return The minimum valid volume index for the given group, non-negative
*/
- @SystemApi
- public int getGroupMinVolume(int groupId) throws CarNotConnectedException {
+ public int getGroupMinVolume(int zoneId, int groupId) throws CarNotConnectedException {
try {
- return mService.getGroupMinVolume(groupId);
+ return mService.getGroupMinVolume(zoneId, groupId);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getGroupMinVolume failed", e);
throw new CarNotConnectedException(e);
@@ -140,20 +158,29 @@
}
/**
+ * Returns the current volume index for a volume group in primary zone.
+ *
+ * @see {@link #getGroupVolume(int, int)}
+ */
+ public int getGroupVolume(int groupId) throws CarNotConnectedException {
+ return getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+ }
+
+ /**
* Returns the current volume index for a volume group.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume groups is queried.
* @param groupId The volume group id whose volume index is returned.
* @return The current volume index for the given group.
*
- * @see #getGroupMaxVolume(int)
- * @see #setGroupVolume(int, int, int)
+ * @see #getGroupMaxVolume(int, int)
+ * @see #setGroupVolume(int, int, int, int)
*/
- @SystemApi
- public int getGroupVolume(int groupId) throws CarNotConnectedException {
+ public int getGroupVolume(int zoneId, int groupId) throws CarNotConnectedException {
try {
- return mService.getGroupVolume(groupId);
+ return mService.getGroupVolume(zoneId, groupId);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getGroupVolume failed", e);
throw new CarNotConnectedException(e);
@@ -170,7 +197,6 @@
*
* @see #setBalanceTowardRight(float)
*/
- @SystemApi
public void setFadeTowardFront(float value) throws CarNotConnectedException {
try {
mService.setFadeTowardFront(value);
@@ -190,7 +216,6 @@
*
* @see #setFadeTowardFront(float)
*/
- @SystemApi
public void setBalanceTowardRight(float value) throws CarNotConnectedException {
try {
mService.setBalanceTowardRight(value);
@@ -213,7 +238,6 @@
* @see #createAudioPatch(String, int, int)
* @see #releaseAudioPatch(CarAudioPatchHandle)
*/
- @SystemApi
public @NonNull String[] getExternalSources() throws CarNotConnectedException {
try {
return mService.getExternalSources();
@@ -243,7 +267,6 @@
* @see #getExternalSources()
* @see #releaseAudioPatch(CarAudioPatchHandle)
*/
- @SystemApi
public CarAudioPatchHandle createAudioPatch(String sourceAddress,
@AudioAttributes.AttributeUsage int usage, int gainInMillibels)
throws CarNotConnectedException {
@@ -266,7 +289,6 @@
* @see #getExternalSources()
* @see #createAudioPatch(String, int, int)
*/
- @SystemApi
public void releaseAudioPatch(CarAudioPatchHandle patch) throws CarNotConnectedException {
try {
mService.releaseAudioPatch(patch);
@@ -277,16 +299,25 @@
}
/**
+ * Gets the count of available volume groups in primary zone.
+ *
+ * @see {@link #getVolumeGroupCount(int)}
+ */
+ public int getVolumeGroupCount() throws CarNotConnectedException {
+ return getVolumeGroupCount(PRIMARY_AUDIO_ZONE);
+ }
+
+ /**
* Gets the count of available volume groups in the system.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whois count of volume groups is queried.
* @return Count of volume groups
*/
- @SystemApi
- public int getVolumeGroupCount() throws CarNotConnectedException {
+ public int getVolumeGroupCount(int zoneId) throws CarNotConnectedException {
try {
- return mService.getVolumeGroupCount();
+ return mService.getVolumeGroupCount(zoneId);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getVolumeGroupCount failed", e);
throw new CarNotConnectedException(e);
@@ -294,18 +325,28 @@
}
/**
+ * Gets the volume group id for a given {@link AudioAttributes} usage in primary zone.
+ *
+ * @see {@link #getVolumeGroupIdForUsage(int, int)}
+ */
+ public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage)
+ throws CarNotConnectedException {
+ return getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, usage);
+ }
+
+ /**
* Gets the volume group id for a given {@link AudioAttributes} usage.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume group is queried.
* @param usage The {@link AudioAttributes} usage to get a volume group from.
* @return The volume group id where the usage belongs to
*/
- @SystemApi
- public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage)
+ public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage)
throws CarNotConnectedException {
try {
- return mService.getVolumeGroupIdForUsage(usage);
+ return mService.getVolumeGroupIdForUsage(zoneId, usage);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getVolumeGroupIdForUsage failed", e);
throw new CarNotConnectedException(e);
@@ -313,71 +354,98 @@
}
/**
- * Gets array of {@link AudioAttributes} usages for a given volume group id.
+ * Gets array of {@link AudioAttributes} usages for a volume group in primary zone.
+ *
+ * @see {@link #getUsagesForVolumeGroupId(int, int)}
+ */
+ public @NonNull int[] getUsagesForVolumeGroupId(int groupId) throws CarNotConnectedException {
+ return getUsagesForVolumeGroupId(PRIMARY_AUDIO_ZONE, groupId);
+ }
+
+ /**
+ * Gets array of {@link AudioAttributes} usages for a volume group in a zone.
*
* Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
*
+ * @param zoneId The zone id whose volume group is queried.
* @param groupId The volume group id whose associated audio usages is returned.
* @return Array of {@link AudioAttributes} usages for a given volume group id
*/
- @SystemApi
- public @NonNull int[] getUsagesForVolumeGroupId(int groupId) throws CarNotConnectedException {
+ public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId)
+ throws CarNotConnectedException {
try {
- return mService.getUsagesForVolumeGroupId(groupId);
+ return mService.getUsagesForVolumeGroupId(zoneId, groupId);
} catch (RemoteException e) {
Log.e(CarLibLog.TAG_CAR, "getUsagesForVolumeGroupId failed", e);
throw new CarNotConnectedException(e);
}
}
- /**
- * Register {@link ICarVolumeCallback} to receive the volume key events
- *
- * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
- *
- * @param binder {@link IBinder} instance of {@link ICarVolumeCallback} to receive
- * volume key event callbacks
- * @throws CarNotConnectedException
- */
- @SystemApi
- public void registerVolumeCallback(@NonNull IBinder binder)
- throws CarNotConnectedException {
- try {
- mService.registerVolumeCallback(binder);
- } catch (RemoteException e) {
- Log.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e);
- throw new CarNotConnectedException(e);
- }
- }
-
- /**
- * Unregister {@link ICarVolumeCallback} from receiving volume key events
- *
- * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
- *
- * @param binder {@link IBinder} instance of {@link ICarVolumeCallback} to stop receiving
- * volume key event callbacks
- * @throws CarNotConnectedException
- */
- @SystemApi
- public void unregisterVolumeCallback(@NonNull IBinder binder)
- throws CarNotConnectedException {
- try {
- mService.unregisterVolumeCallback(binder);
- } catch (RemoteException e) {
- Log.e(CarLibLog.TAG_CAR, "unregisterVolumeCallback failed", e);
- throw new CarNotConnectedException(e);
- }
- }
-
/** @hide */
@Override
public void onCarDisconnected() {
+ if (mService != null) {
+ try {
+ mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder());
+ } catch (RemoteException e) {
+ Log.e(CarLibLog.TAG_CAR, "unregisterVolumeCallback failed", e);
+ }
+ }
}
/** @hide */
public CarAudioManager(IBinder service, Context context, Handler handler) {
- mContentResolver = context.getContentResolver();
mService = ICarAudio.Stub.asInterface(service);
+ mCarVolumeCallbacks = new ArrayList<>();
+
+ try {
+ mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder());
+ } catch (RemoteException e) {
+ Log.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e);
+ }
+ }
+
+ /**
+ * Registers a {@link CarVolumeCallback} to receive volume change callbacks
+ * @param callback {@link CarVolumeCallback} instance, can not be null
+ */
+ public void registerCarVolumeCallback(@NonNull CarVolumeCallback callback) {
+ mCarVolumeCallbacks.add(callback);
+ }
+
+ /**
+ * Unregisters a {@link CarVolumeCallback} from receiving volume change callbacks
+ * @param callback {@link CarVolumeCallback} instance previously registered, can not be null
+ */
+ public void unregisterCarVolumeCallback(@NonNull CarVolumeCallback callback) {
+ mCarVolumeCallbacks.remove(callback);
+ }
+
+ /**
+ * Callback interface to receive volume change events in a car.
+ * Extend this class and register it with {@link #registerCarVolumeCallback(CarVolumeCallback)}
+ * and unregister it via {@link #unregisterCarVolumeCallback(CarVolumeCallback)}
+ */
+ public abstract static class CarVolumeCallback {
+ /**
+ * This is called whenever a group volume is changed.
+ * The changed-to volume index is not included, the caller is encouraged to
+ * get the current group volume index via CarAudioManager.
+ *
+ * @param zoneId Id of the audio zone that volume change happens
+ * @param groupId Id of the volume group that volume is changed
+ * @param flags see {@link android.media.AudioManager} for flag definitions
+ */
+ public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {}
+
+ /**
+ * This is called whenever the master mute state is changed.
+ * The changed-to master mute state is not included, the caller is encouraged to
+ * get the current master mute state via AudioManager.
+ *
+ * @param zoneId Id of the audio zone that master mute state change happens
+ * @param flags see {@link android.media.AudioManager} for flag definitions
+ */
+ public void onMasterMuteChanged(int zoneId, int flags) {}
}
}
diff --git a/car-lib/src/android/car/media/ICarAudio.aidl b/car-lib/src/android/car/media/ICarAudio.aidl
index 353c168..c343c42 100644
--- a/car-lib/src/android/car/media/ICarAudio.aidl
+++ b/car-lib/src/android/car/media/ICarAudio.aidl
@@ -25,10 +25,10 @@
* @hide
*/
interface ICarAudio {
- void setGroupVolume(int groupId, int index, int flags);
- int getGroupMaxVolume(int groupId);
- int getGroupMinVolume(int groupId);
- int getGroupVolume(int groupId);
+ void setGroupVolume(int zoneId, int groupId, int index, int flags);
+ int getGroupMaxVolume(int zoneId, int groupId);
+ int getGroupMinVolume(int zoneId, int groupId);
+ int getGroupVolume(int zoneId, int groupId);
void setFadeTowardFront(float value);
void setBalanceTowardRight(float value);
@@ -37,9 +37,9 @@
CarAudioPatchHandle createAudioPatch(in String sourceAddress, int usage, int gainInMillibels);
void releaseAudioPatch(in CarAudioPatchHandle patch);
- int getVolumeGroupCount();
- int getVolumeGroupIdForUsage(int usage);
- int[] getUsagesForVolumeGroupId(int groupId);
+ int getVolumeGroupCount(int zoneId);
+ int getVolumeGroupIdForUsage(int zoneId, int usage);
+ int[] getUsagesForVolumeGroupId(int zoneId, int groupId);
/**
* IBinder is ICarVolumeCallback but passed as IBinder due to aidl hidden.
diff --git a/car-lib/src/android/car/media/ICarVolumeCallback.aidl b/car-lib/src/android/car/media/ICarVolumeCallback.aidl
index 8540680..9672983 100644
--- a/car-lib/src/android/car/media/ICarVolumeCallback.aidl
+++ b/car-lib/src/android/car/media/ICarVolumeCallback.aidl
@@ -27,12 +27,12 @@
* The changed-to volume index is not included, the caller is encouraged to
* get the current group volume index via CarAudioManager.
*/
- void onGroupVolumeChanged(int groupId, int flags);
+ void onGroupVolumeChanged(int zoneId, int groupId, int flags);
/**
* This is called whenever the master mute state is changed.
* The changed-to master mute state is not included, the caller is encouraged to
* get the current master mute state via AudioManager.
*/
- void onMasterMuteChanged(int flags);
+ void onMasterMuteChanged(int zoneId, int flags);
}
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index fe9e8c3..1416b41 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -155,11 +155,11 @@
} catch (RemoteException e) {
Log.e(TAG, "Could not connect: ", e);
throw new CarNotConnectedException(e);
- }
-
- synchronized (mClientCallbackLock) {
- mClientCallback = null;
- mExecutor = null;
+ } finally {
+ synchronized (mClientCallbackLock) {
+ mClientCallback = null;
+ mExecutor = null;
+ }
}
}
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 5c61a98..e4b6df3 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
@@ -25,7 +25,9 @@
import android.os.Looper;
import android.os.Message;
import android.util.Log;
+
import com.android.internal.annotations.GuardedBy;
+
import java.util.ArrayList;
import java.util.List;
@@ -90,7 +92,6 @@
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
context.registerReceiver(mUsbBroadcastReceiver, filter);
-
}
private synchronized void setActiveDeviceIfMatch(UsbDevice device) {
@@ -129,11 +130,11 @@
return activeDevice != null && UsbUtil.isDevicesMatching(activeDevice, device);
}
- private String generateTitle() {
- String manufacturer = mActiveDevice.getManufacturerName();
- String product = mActiveDevice.getProductName();
+ private static String generateTitle(Context context, UsbDevice usbDevice) {
+ String manufacturer = usbDevice.getManufacturerName();
+ String product = usbDevice.getProductName();
if (manufacturer == null && product == null) {
- return mContext.getString(R.string.usb_unknown_device);
+ return context.getString(R.string.usb_unknown_device);
}
if (manufacturer != null && product != null) {
return manufacturer + " " + product;
@@ -158,14 +159,14 @@
UsbDeviceSettings settings = mUsbSettingsStorage.getSettings(device);
if (settings != null && mUsbResolver.dispatch(
- mActiveDevice, settings.getHandler(), settings.getAoap())) {
+ device, settings.getHandler(), settings.getAoap())) {
if (LOCAL_LOGV) {
Log.v(TAG, "Usb Device: " + device + " was sent to component: "
+ settings.getHandler());
}
return;
}
- mCallback.titleChanged(generateTitle());
+ mCallback.titleChanged(generateTitle(mContext, device));
mUsbResolver.resolve(device);
}
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 7c08389..3e41d70 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -114,13 +114,6 @@
PRODUCT_COPY_FILES += \
packages/services/Car/car_product/bootanimations/bootanimation-832.zip:system/media/bootanimation.zip
-PRODUCT_PROPERTY_OVERRIDES += \
- fmas.spkr_6ch=35,20,110 \
- fmas.spkr_2ch=35,25 \
- fmas.spkr_angles=10 \
- fmas.spkr_sgain=0 \
- media.aac_51_output_enabled=true
-
PRODUCT_LOCALES := en_US af_ZA am_ET ar_EG bg_BG bn_BD ca_ES cs_CZ da_DK de_DE el_GR en_AU en_GB en_IN es_ES es_US et_EE eu_ES fa_IR fi_FI fr_CA fr_FR gl_ES hi_IN hr_HR hu_HU hy_AM in_ID is_IS it_IT iw_IL ja_JP ka_GE km_KH ko_KR ky_KG lo_LA lt_LT lv_LV km_MH kn_IN mn_MN ml_IN mk_MK mr_IN ms_MY my_MM ne_NP nb_NO nl_NL pl_PL pt_BR pt_PT ro_RO ru_RU si_LK sk_SK sl_SI sr_RS sv_SE sw_TZ ta_IN te_IN th_TH tl_PH tr_TR uk_UA vi_VN zh_CN zh_HK zh_TW zu_ZA en_XA ar_XB
# should add to BOOT_JARS only once
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index 0e5ebab..385a2ea 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -42,6 +42,7 @@
MmsService \
ExternalStorageProvider \
atrace \
+ cameraserver \
libandroidfw \
libaudioutils \
libmdnssd \
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 0ab814f..3ab6085 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -65,9 +65,6 @@
<!-- The action buttons should always take the default color. -->
<bool name="config_tintNotificationActionButtons">false</bool>
- <!-- Home screen(Launcher) app presence -->
- <bool name="config_noHomeScreen">true</bool>
-
<!-- Flag indicating that this device does not rotate and will always remain in its default
orientation. Activities that desire to run in a non-compatible orientation will find that
they are not able to. -->
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
index f343e93..cf09310 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
@@ -17,12 +17,12 @@
*/
-->
<resources>
- <dimen name="status_bar_height">96dp</dimen>
- <dimen name="status_bar_height_landscape">96dp</dimen>
- <dimen name="status_bar_height_portrait">96dp</dimen>
- <dimen name="car_qs_header_system_icons_area_height">96dp</dimen>
- <dimen name="navigation_bar_height">128dp</dimen>
- <dimen name="navigation_bar_height_landscape">128dp</dimen>
+ <dimen name="status_bar_height">76dp</dimen>
+ <dimen name="status_bar_height_landscape">76dp</dimen>
+ <dimen name="status_bar_height_portrait">76dp</dimen>
+ <dimen name="car_qs_header_system_icons_area_height">76dp</dimen>
+ <dimen name="navigation_bar_height">112dp</dimen>
+ <dimen name="navigation_bar_height_landscape">112dp</dimen>
<dimen name="status_bar_icon_size">36dp</dimen>
<!-- The height of the header of a notification. -->
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_backspace.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_backspace.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_backspace.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_backspace.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_done.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_done.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_done.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_done.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/keyguard_button_background.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/keyguard_button_background.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/keyguard_button_background.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/keyguard_button_background.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_bouncer.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_bouncer.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml
new file mode 100644
index 0000000..c230414
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2018, 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.
+*/
+-->
+
+<com.android.keyguard.KeyguardMessageArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ style="@style/Keyguard.TextView"
+ android:id="@+id/keyguard_message_area"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:focusable="true"
+ android:layout_marginBottom="@dimen/car_padding_4"
+ android:textSize="@dimen/car_body2_size" />
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pattern_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pattern_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pin_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pin_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml
new file mode 100644
index 0000000..ca0595d
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2018 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
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <!-- Row 1 -->
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key1"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/one" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key2"
+ style="@style/NumPadKeyButton.MiddleColumn"
+ app:digit="@string/two" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key3"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/three" />
+
+ <!-- Row 2 -->
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key4"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/four" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key5"
+ style="@style/NumPadKeyButton.MiddleColumn"
+ app:digit="@string/five" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key6"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/six" />
+
+ <!-- Row 3 -->
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key7"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/seven" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key8"
+ style="@style/NumPadKeyButton.MiddleColumn"
+ app:digit="@string/eight" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key9"
+ style="@style/NumPadKeyButton"
+ app:digit="@string/nine" />
+
+ <!-- Row 4 -->
+ <ImageButton
+ android:id="@+id/delete_button"
+ style="@style/NumPadKeyButton.LastRow"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_backspace"
+ android:clickable="true"
+ android:tint="@android:color/white"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/keyboardview_keycode_delete" />
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key0"
+ style="@style/NumPadKeyButton.LastRow.MiddleColumn"
+ app:digit="@string/zero" />
+ <ImageButton
+ android:id="@+id/key_enter"
+ style="@style/NumPadKeyButton.LastRow"
+ android:src="@drawable/ic_done"
+ android:tint="@android:color/white"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/keyboardview_keycode_enter" />
+</merge>
+
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values-h1000dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-h1000dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values-h1000dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-h1000dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml
new file mode 100644
index 0000000..805a134
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018, 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.
+-->
+<resources>
+ <dimen name="num_pad_key_margin_horizontal">@dimen/car_padding_5</dimen>
+ <dimen name="num_pad_key_margin_bottom">@dimen/car_padding_4</dimen>
+</resources>
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/colors.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/colors.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/colors.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/colors.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/integers.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/integers.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/integers.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/integers.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/strings.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/strings.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/strings.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/strings.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/styles.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/styles.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-h600dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-h600dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-h600dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-h600dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-sw600dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-sw600dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-sw600dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-sw600dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-w1024dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w1024dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-w1024dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w1024dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-w550dp-land/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w550dp-land/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-w550dp-land/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w550dp-land/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values/config.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values/config.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/dimens.xml
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 4a16579..1f221ac 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -23,7 +23,7 @@
<original-package android:name="com.android.car" />
<permission-group
android:name="android.car.permission-group.CAR_MONITORING"
- android:icon="@drawable/car_ic_mode"
+ android:icon="@drawable/perm_group_car"
android:description="@string/car_permission_desc"
android:label="@string/car_permission_label" />
<permission
@@ -188,29 +188,37 @@
android:description="@string/car_permission_desc_audio_settings" />
<permission
- android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
- android:protectionLevel="signature"
- android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
- android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
+ android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
+ android:protectionLevel="signature"
+ android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
+ android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
<permission
- android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
- android:protectionLevel="signature"
- android:label="@string/car_permission_label_bind_input_service"
- android:description="@string/car_permission_desc_bind_input_service"/>
+ android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
+ android:protectionLevel="signature"
+ android:label="@string/car_permission_label_bind_input_service"
+ android:description="@string/car_permission_desc_bind_input_service"/>
<permission
- android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
- android:protectionLevel="system|signature"
- android:label="@string/car_permission_car_display_in_cluster"
- android:description="@string/car_permission_desc_car_display_in_cluster" />
+ android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_car_display_in_cluster"
+ android:description="@string/car_permission_desc_car_display_in_cluster" />
- <permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
- android:protectionLevel="system|signature"
- android:label="@string/car_permission_car_cluster_control"
- android:description="@string/car_permission_desc_car_cluster_control" />
+ <permission
+ android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_car_cluster_control"
+ android:description="@string/car_permission_desc_car_cluster_control" />
- <permission android:name="android.car.permission.STORAGE_MONITORING"
+ <permission
+ android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_label_car_ux_restrictions_configuration"
+ android:description="@string/car_permission_desc_car_ux_restrictions_configuration" />
+
+ <permission
+ android:name="android.car.permission.STORAGE_MONITORING"
android:protectionLevel="system|signature"
android:label="@string/car_permission_label_storage_monitoring"
android:description="@string/car_permission_desc_storage_monitoring" />
diff --git a/service/res/drawable-hdpi/car_ic_mode.png b/service/res/drawable-hdpi/car_ic_mode.png
deleted file mode 100644
index a8f719f..0000000
--- a/service/res/drawable-hdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable-mdpi/car_ic_mode.png b/service/res/drawable-mdpi/car_ic_mode.png
deleted file mode 100644
index 38a9747..0000000
--- a/service/res/drawable-mdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable-xhdpi/car_ic_mode.png b/service/res/drawable-xhdpi/car_ic_mode.png
deleted file mode 100644
index 58a1aca..0000000
--- a/service/res/drawable-xhdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable/perm_group_car.xml b/service/res/drawable/perm_group_car.xml
new file mode 100644
index 0000000..79e3f03
--- /dev/null
+++ b/service/res/drawable/perm_group_car.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2018 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="192.0"
+ android:viewportWidth="192.0">
+
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M36.9,180.4l5.6,5.6L96,164l53.4,22l5.6,-5.6L96,73L36.9,180.4zM184,145.8L105.7,11.6C103.7,8.1 100,6 96,6c-4,0 -7.7,2.1 -9.8,5.6L7.5,146.5c-2,3.5 -2.1,7.3 0,10.8c2,3.5 5.8,5.7 9.8,5.7H38l54.6,-99h6.8l54.6,99h20.3c6.2,0 11.7,-4.6 11.7,-10.8C186,149.8 185.3,147.6 184,145.8z"/>
+</vector>
diff --git a/service/res/values/attrs.xml b/service/res/values/attrs.xml
index 89c4258..be20c0f 100644
--- a/service/res/values/attrs.xml
+++ b/service/res/values/attrs.xml
@@ -18,11 +18,11 @@
-->
<resources>
- <!-- Defines the attributes and values used in res/xml/car_volume_group.xml -->
- <declare-styleable name="volumeGroups" />
-
+ <!-- Defines the attributes and values used in res/xml/car_volume_groups.xml -->
+ <declare-styleable name="volumeGroups">
+ <attr name="isDeprecated" format="boolean"/>
+ </declare-styleable>
<declare-styleable name="volumeGroups_group"/>
-
<declare-styleable name="volumeGroups_context">
<!-- Align with hardware/interfaces/automotive/audiocontrol/1.0/types.hal:ContextNumber -->
<attr name="context">
@@ -37,6 +37,18 @@
</attr>
</declare-styleable>
+ <!--
+ Defines the attributes and values used in car_audio_configuration.xml
+ This is a superset of car_volume_groups.xml
+ -->
+ <declare-styleable name="carAudioConfiguration">
+ <attr name="version" format="integer"/>
+ <attr name="isPrimary" format="boolean"/>
+ <attr name="name" format="string"/>
+ <attr name="address" format="string"/>
+ <attr name="display" format="string"/>
+ </declare-styleable>
+
<!-- Defines the UX restrictions to be imposed for different driving states of a vehicle -->
<declare-styleable name="UxRestrictions"/>
<!-- 1. UX restriction Mapping from a driving state of the vehicle-->
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 1d72dca..3b05e29 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -24,6 +24,14 @@
dynamic audio routing is disabled and audio works in legacy mode. It may be useful
during initial development where audio hal does not support bus based addressing yet. -->
<bool name="audioUseDynamicRouting">false</bool>
+
+ <!-- Configuration to use the unified audio configuration.
+ This flag has no effect if audioUseDynamicRouting is set to false.
+ When audioUseDynamicRouting is enabled
+ - car_volume_groups.xml will be picked if this flag is false
+ - car_audio_configuration.xml will be used otherwise. -->
+ <bool name="audioUseUnifiedConfiguration">false</bool>
+
<!-- Configuration to persist master mute state. If this is set to true,
Android will restore the master mute state on boot. -->
<bool name="audioPersistMasterMuteState">true</bool>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 599e41f..2a7bcaa 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -90,6 +90,8 @@
<string name="car_permission_desc_car_cluster_control">Launch apps in the instrument cluster</string>
<string name="car_permission_label_bind_instrument_cluster_rendering">Instrument Cluster Rendering</string>
<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>
<!-- Permission text: apps can handle input events [CHAR LIMIT=NONE] -->
<string name="car_permission_label_bind_input_service">Car Input Service</string>
diff --git a/service/res/xml/car_audio_configuration.xml b/service/res/xml/car_audio_configuration.xml
new file mode 100644
index 0000000..e3fd8f6
--- /dev/null
+++ b/service/res/xml/car_audio_configuration.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<!--
+ Defines the audio configuration in a car, including
+ - Audio zones
+ - Display to audio zone mappings
+ - Context to audio bus mappings
+ - Volume groups
+ in the car environment.
+-->
+<carAudioConfiguration
+ xmlns:car="http://schemas.android.com/apk/res-auto"
+ car:version="1">
+ <zones>
+ <zone car:name="primary zone" car:isPrimary="true">
+ <volumeGroups>
+ <group>
+ <device car:address="bus0_media_out">
+ <context car:context="music"/>
+ </device>
+ <device car:address="bus3_call_ring_out">
+ <context car:context="call_ring"/>
+ </device>
+ <device car:address="bus6_notification_out">
+ <context car:context="notification"/>
+ </device>
+ <device car:address="bus7_system_sound_out">
+ <context car:context="system_sound"/>
+ </device>
+ </group>
+ <group>
+ <device car:address="bus1_navigation_out">
+ <context car:context="navigation"/>
+ </device>
+ <device car:address="bus2_voice_command_out">
+ <context car:context="voice_command"/>
+ </device>
+ </group>
+ <group>
+ <device car:address="bus4_call_out">
+ <context car:context="call"/>
+ </device>
+ </group>
+ <group>
+ <device car:address="bus5_alarm_out">
+ <context car:context="alarm"/>
+ </device>
+ </group>
+ </volumeGroups>
+ <displays>
+ <display car:display="primary_display"/>
+ </displays>
+ </zone>
+ <zone car:name="rear seat zone">
+ <volumeGroups>
+ <group>
+ <device car:address="bus100_rear_seat">
+ <context car:context="music"/>
+ <context car:context="navigation"/>
+ <context car:context="voice_command"/>
+ <context car:context="call_ring"/>
+ <context car:context="call"/>
+ <context car:context="alarm"/>
+ <context car:context="notification"/>
+ <context car:context="system_sound"/>
+ </device>
+ </group>
+ </volumeGroups>
+ <displays>
+ <display car:display="rear_seat_display"/>
+ </displays>
+ </zone>
+ </zones>
+</carAudioConfiguration>
diff --git a/service/res/xml/car_volume_groups.xml b/service/res/xml/car_volume_groups.xml
index 9bfc305..c900329 100644
--- a/service/res/xml/car_volume_groups.xml
+++ b/service/res/xml/car_volume_groups.xml
@@ -15,6 +15,18 @@
-->
<!--
+ This configuration is replaced by car_audio_configuration.xml
+
+ Notes on backward compatibility
+ - A new audioUseUnifiedConfiguration flag is added, and the default value
+ is false
+ - If OEM does not explicitly set audioUseUnifiedConfiguration to be true,
+ car_volume_groups.xml will be used, CarAudioService also queries
+ IAudioControl HAL (getBusForContext)
+ - Otherwise, CarAudioService loads the new car_audio_configuration.xml
+
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
Defines the all available volume groups for volume control in a car.
One can overlay this configuration to customize the groups.
@@ -33,7 +45,8 @@
Important note: when overlaying this configuration,
make sure the resources are in the same package as CarAudioService.
-->
-<volumeGroups xmlns:car="http://schemas.android.com/apk/res-auto">
+<volumeGroups xmlns:car="http://schemas.android.com/apk/res-auto"
+ car:isDeprecated="true">
<group>
<context car:context="music"/>
<context car:context="call_ring"/>
diff --git a/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java b/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java
new file mode 100644
index 0000000..95bcb2c
--- /dev/null
+++ b/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2018 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.annotation.Nullable;
+import android.annotation.XmlRes;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * @hide
+ */
+public final class CarUxRestrictionsConfigurationXmlParser {
+ private static final String TAG = "UxRConfigParser";
+ private static final int UX_RESTRICTIONS_UNKNOWN = -1;
+ private static final float INVALID_SPEED = -1f;
+ // XML tags to parse
+ private static final String ROOT_ELEMENT = "UxRestrictions";
+ private static final String RESTRICTION_MAPPING = "RestrictionMapping";
+ private static final String RESTRICTION_PARAMETERS = "RestrictionParameters";
+ private static final String DRIVING_STATE = "DrivingState";
+ private static final String RESTRICTIONS = "Restrictions";
+ private static final String STRING_RESTRICTIONS = "StringRestrictions";
+ private static final String CONTENT_RESTRICTIONS = "ContentRestrictions";
+
+ private final Context mContext;
+
+ private CarUxRestrictionsConfiguration.Builder mConfigBuilder;
+
+ private CarUxRestrictionsConfigurationXmlParser(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Loads the UX restrictions related information from the XML resource.
+ *
+ * @return parsed CarUxRestrictionsConfiguration; {@code null} if the XML is malformed.
+ */
+ @Nullable
+ public static CarUxRestrictionsConfiguration parse(Context context, @XmlRes int xmlResource)
+ throws IOException, XmlPullParserException {
+ return new CarUxRestrictionsConfigurationXmlParser(context).parse(xmlResource);
+ }
+
+ @Nullable
+ private CarUxRestrictionsConfiguration parse(@XmlRes int xmlResource)
+ throws IOException, XmlPullParserException {
+ mConfigBuilder = new CarUxRestrictionsConfiguration.Builder();
+
+ XmlResourceParser parser = mContext.getResources().getXml(xmlResource);
+ if (parser == null) {
+ Log.e(TAG, "Invalid Xml resource");
+ return null;
+ }
+
+ if (!traverseUntilStartTag(parser)) {
+ Log.e(TAG, "XML root element invalid: " + parser.getName());
+ return null;
+ }
+
+ if (!traverseUntilEndOfDocument(parser)) {
+ Log.e(TAG, "Could not parse XML to end");
+ return null;
+ }
+
+ return mConfigBuilder.build();
+ }
+
+ private boolean traverseUntilStartTag(XmlResourceParser parser)
+ throws IOException, XmlPullParserException {
+ int type;
+ // Traverse till we get to the first tag
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && type != XmlResourceParser.START_TAG) {
+ // Do nothing.
+ }
+ return ROOT_ELEMENT.equals(parser.getName());
+ }
+
+ private boolean traverseUntilEndOfDocument(XmlResourceParser parser)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
+ // Every time we hit a start tag, check for the type of the tag
+ // and load the corresponding information.
+ if (parser.next() == XmlResourceParser.START_TAG) {
+ switch (parser.getName()) {
+ case RESTRICTION_MAPPING:
+ if (!mapDrivingStateToRestrictions(parser, attrs)) {
+ Log.e(TAG, "Could not map driving state to restriction.");
+ return false;
+ }
+ break;
+ case RESTRICTION_PARAMETERS:
+ if (!parseRestrictionParameters(parser, attrs)) {
+ // Failure to parse is automatically handled by falling back to
+ // defaults. Just log the information here.
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "Error reading restrictions parameters. "
+ + "Falling back to platform defaults.");
+ }
+ }
+ break;
+ default:
+ Log.w(TAG, "Unknown class:" + parser.getName());
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parses the information in the <restrictionMapping> tag to construct the mapping from
+ * driving state to UX restrictions.
+ */
+ private boolean mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs)
+ throws IOException, XmlPullParserException {
+ if (parser == null || attrs == null) {
+ Log.e(TAG, "Invalid arguments");
+ return false;
+ }
+ // The parser should be at the <RestrictionMapping> tag at this point.
+ if (!RESTRICTION_MAPPING.equals(parser.getName())) {
+ Log.e(TAG, "Parser not at RestrictionMapping element: " + parser.getName());
+ return false;
+ }
+ if (!traverseToTag(parser, DRIVING_STATE)) {
+ Log.e(TAG, "No <" + DRIVING_STATE + "> tag in XML");
+ return false;
+ }
+ // Handle all the <DrivingState> tags.
+ while (DRIVING_STATE.equals(parser.getName())) {
+ if (parser.getEventType() == XmlResourceParser.START_TAG) {
+ // 1. Get the driving state attributes: driving state and speed range
+ TypedArray a = mContext.getResources().obtainAttributes(attrs,
+ R.styleable.UxRestrictions_DrivingState);
+ int drivingState = a
+ .getInt(R.styleable.UxRestrictions_DrivingState_state,
+ CarDrivingStateEvent.DRIVING_STATE_UNKNOWN);
+ float minSpeed = a
+ .getFloat(
+ R.styleable
+ .UxRestrictions_DrivingState_minSpeed,
+ INVALID_SPEED);
+ float maxSpeed = a
+ .getFloat(
+ R.styleable
+ .UxRestrictions_DrivingState_maxSpeed,
+ INVALID_SPEED);
+ a.recycle();
+
+ // 2. Traverse to the <Restrictions> tag
+ if (!traverseToTag(parser, RESTRICTIONS)) {
+ Log.e(TAG, "No <" + RESTRICTIONS + "> tag in XML");
+ return false;
+ }
+
+ // 3. Parse the restrictions for this driving state
+ Pair<Boolean, Integer> restrictions = parseRestrictions(parser, attrs);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Map " + drivingState + " : " + restrictions);
+ }
+
+ // Update the builder if the driving state and restrictions info are valid.
+ if (drivingState != CarDrivingStateEvent.DRIVING_STATE_UNKNOWN
+ && restrictions != null) {
+ addToRestrictions(drivingState, minSpeed, maxSpeed, restrictions.first,
+ restrictions.second);
+ }
+ }
+ parser.next();
+ }
+ return true;
+ }
+
+ /**
+ * Parses the <restrictions> tag nested with the <drivingState>. This provides the restrictions
+ * for the enclosing driving state.
+ */
+ @Nullable
+ private Pair<Boolean, Integer> parseRestrictions(XmlResourceParser parser, AttributeSet attrs)
+ throws IOException, XmlPullParserException {
+ int restrictions = UX_RESTRICTIONS_UNKNOWN;
+ boolean requiresOpt = true;
+ if (parser == null || attrs == null) {
+ Log.e(TAG, "Invalid Arguments");
+ return null;
+ }
+
+ while (RESTRICTIONS.equals(parser.getName())
+ && parser.getEventType() == XmlResourceParser.START_TAG) {
+ TypedArray a = mContext.getResources().obtainAttributes(attrs,
+ R.styleable.UxRestrictions_Restrictions);
+ restrictions = a.getInt(
+ R.styleable.UxRestrictions_Restrictions_uxr,
+ CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
+ requiresOpt = a.getBoolean(
+ R.styleable.UxRestrictions_Restrictions_requiresDistractionOptimization, true);
+ a.recycle();
+ parser.next();
+ }
+ return new Pair<>(requiresOpt, restrictions);
+ }
+
+ private void addToRestrictions(int drivingState, float minSpeed, float maxSpeed,
+ boolean requiresOpt, int restrictions) {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange speedRange = null;
+ if (Float.compare(minSpeed, INVALID_SPEED) != 0) {
+ if (Float.compare(maxSpeed, INVALID_SPEED) == 0) {
+ // Setting min speed but not max implies MAX_SPEED.
+ maxSpeed = CarUxRestrictionsConfiguration.Builder.SpeedRange.MAX_SPEED;
+ }
+ speedRange = new CarUxRestrictionsConfiguration.Builder.SpeedRange(minSpeed, maxSpeed);
+ }
+ mConfigBuilder.setUxRestrictions(drivingState, speedRange, requiresOpt, restrictions);
+ }
+
+ private boolean traverseToTag(XmlResourceParser parser, String tag)
+ throws IOException, XmlPullParserException {
+ if (tag == null || parser == null) {
+ return false;
+ }
+ int type;
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
+ if (type == XmlResourceParser.START_TAG && parser.getName().equals(tag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the information in the <RestrictionParameters> tag to read the parameters for the
+ * applicable UX restrictions
+ */
+ private boolean parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs)
+ throws IOException, XmlPullParserException {
+ if (parser == null || attrs == null) {
+ Log.e(TAG, "Invalid arguments");
+ return false;
+ }
+ // The parser should be at the <RestrictionParameters> tag at this point.
+ if (!RESTRICTION_PARAMETERS.equals(parser.getName())) {
+ Log.e(TAG, "Parser not at RestrictionParameters element: " + parser.getName());
+ return false;
+ }
+ while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
+ int type = parser.next();
+ // Break if we have parsed all <RestrictionParameters>
+ if (type == XmlResourceParser.END_TAG && RESTRICTION_PARAMETERS.equals(
+ parser.getName())) {
+ return true;
+ }
+ if (type == XmlResourceParser.START_TAG) {
+ TypedArray a = null;
+ switch (parser.getName()) {
+ case STRING_RESTRICTIONS:
+ a = mContext.getResources().obtainAttributes(attrs,
+ R.styleable.UxRestrictions_StringRestrictions);
+ mConfigBuilder.setMaxStringLength(a.getInt(
+ R.styleable.UxRestrictions_StringRestrictions_maxLength,
+ UX_RESTRICTIONS_UNKNOWN));
+
+ break;
+ case CONTENT_RESTRICTIONS:
+ a = mContext.getResources().obtainAttributes(attrs,
+ R.styleable.UxRestrictions_ContentRestrictions);
+ mConfigBuilder.setMaxCumulativeContentItems(a.getInt(
+ R.styleable.UxRestrictions_ContentRestrictions_maxCumulativeItems,
+ UX_RESTRICTIONS_UNKNOWN));
+ mConfigBuilder.setMaxContentDepth(a.getInt(
+ R.styleable.UxRestrictions_ContentRestrictions_maxDepth,
+ UX_RESTRICTIONS_UNKNOWN));
+ break;
+ default:
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unsupported Restriction Parameters in XML: "
+ + parser.getName());
+ }
+ break;
+ }
+ if (a != null) {
+ a.recycle();
+ }
+ }
+ }
+ return true;
+ }
+}
+
diff --git a/service/src/com/android/car/CarUxRestrictionsManagerService.java b/service/src/com/android/car/CarUxRestrictionsManagerService.java
index b90939b..868b825 100644
--- a/service/src/com/android/car/CarUxRestrictionsManagerService.java
+++ b/service/src/com/android/car/CarUxRestrictionsManagerService.java
@@ -16,32 +16,54 @@
package com.android.car;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
import android.annotation.Nullable;
+import android.car.Car;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
import android.car.drivingstate.ICarDrivingStateChangeListener;
import android.car.drivingstate.ICarUxRestrictionsChangeListener;
import android.car.drivingstate.ICarUxRestrictionsManager;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.ICarPropertyEventListener;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.JsonReader;
+import android.util.JsonWriter;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParserException;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -49,6 +71,13 @@
/**
* A service that listens to current driving state of the vehicle and maps it to the
* appropriate UX restrictions for that driving state.
+ * <p>
+ * <h1>UX Restrictions Configuration</h1>
+ * When this service starts, it will first try reading the configuration set through
+ * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)}.
+ * If one is not available, it will try reading the configuration saved in
+ * {@code R.xml.car_ux_restrictions_map}. If XML is somehow unavailable, it will
+ * fall back to a hard-coded configuration.
*/
public class CarUxRestrictionsManagerService extends ICarUxRestrictionsManager.Stub implements
CarServiceBase {
@@ -57,55 +86,187 @@
private static final int MAX_TRANSITION_LOG_SIZE = 20;
private static final int PROPERTY_UPDATE_RATE = 5; // Update rate in Hz
private static final float SPEED_NOT_AVAILABLE = -1.0F;
+
+ @VisibleForTesting
+ /* package */ static final String CONFIG_FILENAME_PRODUCTION = "prod_config.json";
+ @VisibleForTesting
+ /* package */ static final String CONFIG_FILENAME_STAGED = "staged_config.json";
+
private final Context mContext;
private final CarDrivingStateService mDrivingStateService;
private final CarPropertyService mCarPropertyService;
- private final CarUxRestrictionsServiceHelper mHelper;
+ private final CarUserManagerHelper mCarUserManagerHelper;
// List of clients listening to UX restriction events.
private final List<UxRestrictionsClient> mUxRClients = new ArrayList<>();
+ private CarUxRestrictionsConfiguration mCarUxRestrictionsConfiguration;
private CarUxRestrictions mCurrentUxRestrictions;
private float mCurrentMovingSpeed;
- private boolean mFallbackToDefaults;
// Flag to disable broadcasting UXR changes - for development purposes
@GuardedBy("this")
private boolean mUxRChangeBroadcastEnabled = true;
// For dumpsys logging
private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>();
+ // When UXR service boots up the context does not have access to storage yet, so it
+ // will likely read configuration from XML resource. Register to receive broadcast to
+ // attempt to read saved configuration when it becomes available.
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
+ // If the system user is not headless, then we can read config as soon as the
+ // system has completed booting.
+ if (!mCarUserManagerHelper.isHeadlessSystemUser()) {
+ logd("not headless on boot complete");
+ PendingResult pendingResult = goAsync();
+ LoadRestrictionsTask task = new LoadRestrictionsTask(pendingResult);
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ logd("USER_SWITCHED: " + userHandle);
+ if (mCarUserManagerHelper.isHeadlessSystemUser()
+ && userHandle > UserHandle.USER_SYSTEM) {
+ PendingResult pendingResult = goAsync();
+ LoadRestrictionsTask task = new LoadRestrictionsTask(pendingResult);
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+ }
+ };
+
+ private class LoadRestrictionsTask extends AsyncTask<Void, Void, Void> {
+ private final BroadcastReceiver.PendingResult mPendingResult;
+
+ private LoadRestrictionsTask(BroadcastReceiver.PendingResult pendingResult) {
+ mPendingResult = pendingResult;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mCarUxRestrictionsConfiguration = loadConfig();
+ handleDispatchUxRestrictions(mDrivingStateService.getCurrentDrivingState().eventValue,
+ getCurrentSpeed());
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ mPendingResult.finish();
+ }
+ }
public CarUxRestrictionsManagerService(Context context, CarDrivingStateService drvService,
- CarPropertyService propertyService) {
+ CarPropertyService propertyService, CarUserManagerHelper carUserManagerHelper) {
mContext = context;
mDrivingStateService = drvService;
mCarPropertyService = propertyService;
- mHelper = new CarUxRestrictionsServiceHelper(mContext, R.xml.car_ux_restrictions_map);
+ mCarUserManagerHelper = carUserManagerHelper;
+ // NOTE: during boot phase context cannot access file system so we most likely will
+ // use XML config. If prod config is set, it will be loaded in broadcast receiver.
+ mCarUxRestrictionsConfiguration = loadConfig();
// Unrestricted until driving state information is received. During boot up, we don't want
// everything to be blocked until data is available from CarPropertyManager. If we start
// driving and we don't get speed or gear information, we have bigger problems.
- mCurrentUxRestrictions = mHelper.createUxRestrictionsEvent(false,
- CarUxRestrictions.UX_RESTRICTIONS_BASELINE);
+ mCurrentUxRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */ false,
+ CarUxRestrictions.UX_RESTRICTIONS_BASELINE, SystemClock.elapsedRealtimeNanos())
+ .build();
}
@Override
public synchronized void init() {
- try {
- if (!mHelper.loadUxRestrictionsFromXml()) {
- Log.e(TAG, "Error reading Ux Restrictions Mapping. Falling back to defaults");
- mFallbackToDefaults = true;
- }
- } catch (IOException | XmlPullParserException e) {
- Log.e(TAG, "Exception reading UX restrictions XML mapping", e);
- mFallbackToDefaults = true;
- }
// subscribe to driving State
mDrivingStateService.registerDrivingStateChangeListener(
mICarDrivingStateChangeEventListener);
// subscribe to property service for speed
mCarPropertyService.registerListener(VehicleProperty.PERF_VEHICLE_SPEED,
PROPERTY_UPDATE_RATE, mICarPropertyEventListener);
+ registerReceiverToLoadConfig();
initializeUxRestrictions();
}
+ private void registerReceiverToLoadConfig() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ /* package */ CarUxRestrictionsConfiguration getConfig() {
+ return mCarUxRestrictionsConfiguration;
+ }
+
+ /**
+ * Loads a UX restrictions configuration and returns it.
+ * <p>Reads config from the following sources in order:
+ * <ol>
+ * <li>saved config set by
+ * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)};
+ * <li>XML resource config from {@code R.xml.car_ux_restrictions_map};
+ * <li>hardcoded default config.
+ * </ol>
+ */
+ @VisibleForTesting
+ /* package */ synchronized CarUxRestrictionsConfiguration loadConfig() {
+ promoteStagedConfig();
+
+ CarUxRestrictionsConfiguration config = null;
+ // Production config, if available, is the first choice.
+ File prodConfig = mContext.getFileStreamPath(CONFIG_FILENAME_PRODUCTION);
+ if (prodConfig.exists()) {
+ logd("Attempting to read production config");
+ config = readPersistedConfig(prodConfig);
+ if (config != null) {
+ return config;
+ }
+ }
+
+ // XML config is the second choice.
+ logd("Attempting to read config from XML resource");
+ config = readXmlConfig();
+ if (config != null) {
+ return config;
+ }
+
+ // This should rarely happen.
+ Log.w(TAG, "Creating default config");
+ return createDefaultConfig();
+ }
+
+ @Nullable
+ private CarUxRestrictionsConfiguration readXmlConfig() {
+ try {
+ return CarUxRestrictionsConfigurationXmlParser.parse(mContext,
+ R.xml.car_ux_restrictions_map);
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Could not read config from XML resource", e);
+ }
+ return null;
+ }
+
+ private void promoteStagedConfig() {
+ Path stagedConfig = mContext.getFileStreamPath(CONFIG_FILENAME_STAGED).toPath();
+
+ CarDrivingStateEvent currentDrivingStateEvent =
+ mDrivingStateService.getCurrentDrivingState();
+ // Only promote staged config when car is parked.
+ if (currentDrivingStateEvent != null
+ && currentDrivingStateEvent.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED
+ && Files.exists(stagedConfig)) {
+ Path prod = mContext.getFileStreamPath(CONFIG_FILENAME_PRODUCTION).toPath();
+ try {
+ logd("Attempting to promote stage config");
+ Files.move(stagedConfig, prod, REPLACE_EXISTING);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not promote state config", e);
+ }
+ }
+ }
+
// Update current restrictions by getting the current driving state and speed.
private void initializeUxRestrictions() {
CarDrivingStateEvent currentDrivingStateEvent =
@@ -159,9 +320,7 @@
public synchronized void registerUxRestrictionsChangeListener(
ICarUxRestrictionsChangeListener listener) {
if (listener == null) {
- if (DBG) {
- Log.e(TAG, "registerUxRestrictionsChangeListener(): listener null");
- }
+ Log.e(TAG, "registerUxRestrictionsChangeListener(): listener null");
throw new IllegalArgumentException("Listener is null");
}
// If a new client is registering, create a new DrivingStateClient and add it to the list
@@ -232,6 +391,56 @@
return mCurrentUxRestrictions;
}
+ @Override
+ public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
+ CarUxRestrictionsConfiguration config) {
+ ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION);
+ return persistConfig(config, CONFIG_FILENAME_STAGED);
+ }
+
+ /**
+ * Writes configuration into the specified file.
+ *
+ * IO access on file is not thread safe. Caller should ensure threading protection.
+ */
+ private boolean persistConfig(CarUxRestrictionsConfiguration config, String filename) {
+ AtomicFile stagedFile = new AtomicFile(mContext.getFileStreamPath(filename));
+ FileOutputStream fos;
+ try {
+ fos = stagedFile.startWrite();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not open file to persist config", e);
+ return false;
+ }
+ try (JsonWriter jsonWriter = new JsonWriter(
+ new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
+ config.writeJson(jsonWriter);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not persist config", e);
+ stagedFile.failWrite(fos);
+ return false;
+ }
+ stagedFile.finishWrite(fos);
+ return true;
+ }
+
+ @Nullable
+ private CarUxRestrictionsConfiguration readPersistedConfig(File file) {
+ if (!file.exists()) {
+ Log.e(TAG, "Could not find config file: " + file.getName());
+ return null;
+ }
+
+ AtomicFile config = new AtomicFile(file);
+ try (JsonReader reader = new JsonReader(
+ new InputStreamReader(config.openRead(), StandardCharsets.UTF_8))) {
+ return CarUxRestrictionsConfiguration.readJson(reader);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not read persisted config file " + file.getName(), e);
+ }
+ return null;
+ }
+
/**
* Enable/disable UX restrictions change broadcast blocking.
* Setting this to true will stop broadcasts of UX restriction change to listeners.
@@ -284,9 +493,7 @@
@Override
public void binderDied() {
- if (DBG) {
- Log.d(TAG, "Binder died " + listenerBinder);
- }
+ logd("Binder died " + listenerBinder);
listenerBinder.unlinkToDeath(this, 0);
synchronized (CarUxRestrictionsManagerService.this) {
mUxRClients.remove(this);
@@ -315,9 +522,7 @@
try {
listener.onUxRestrictionsChanged(event);
} catch (RemoteException e) {
- if (DBG) {
- Log.d(TAG, "Dispatch to listener failed");
- }
+ Log.e(TAG, "Dispatch to listener failed", e);
}
}
}
@@ -330,7 +535,7 @@
if (isDebugBuild()) {
writer.println("mUxRChangeBroadcastEnabled? " + mUxRChangeBroadcastEnabled);
}
- mHelper.dump(writer);
+ mCarUxRestrictionsConfiguration.dump(writer);
writer.println("UX Restriction change log:");
for (Utils.TransitionLog tlog : mTransitionLogs) {
writer.println(tlog);
@@ -345,9 +550,7 @@
new ICarDrivingStateChangeListener.Stub() {
@Override
public void onDrivingStateChanged(CarDrivingStateEvent event) {
- if (DBG) {
- Log.d(TAG, "Driving State Changed:" + event.eventValue);
- }
+ logd("Driving State Changed:" + event.eventValue);
handleDrivingStateEvent(event);
}
};
@@ -370,9 +573,7 @@
|| drivingState == CarDrivingStateEvent.DRIVING_STATE_UNKNOWN) {
// If speed is unavailable, but the driving state is parked or unknown, it can still be
// handled.
- if (DBG) {
- Log.d(TAG, "Speed null when driving state is: " + drivingState);
- }
+ logd("Speed null when driving state is: " + drivingState);
mCurrentMovingSpeed = 0;
} else {
// If we get here with driving state != parked or unknown && speed == null,
@@ -430,14 +631,8 @@
return;
}
- CarUxRestrictions uxRestrictions;
- // Get UX restrictions from the parsed configuration XML or fall back to defaults if not
- // available.
- if (mFallbackToDefaults) {
- uxRestrictions = getDefaultRestrictions(currentDrivingState);
- } else {
- uxRestrictions = mHelper.getUxRestrictions(currentDrivingState, speed);
- }
+ CarUxRestrictions uxRestrictions =
+ mCarUxRestrictionsConfiguration.getUxRestrictions(currentDrivingState, speed);
if (DBG) {
Log.d(TAG, String.format("DO old->new: %b -> %b",
@@ -464,31 +659,23 @@
extraInfo.toString());
mCurrentUxRestrictions = uxRestrictions;
- if (DBG) {
- Log.d(TAG, "dispatching to " + mUxRClients.size() + " clients");
- }
+ logd("dispatching to " + mUxRClients.size() + " clients");
for (UxRestrictionsClient client : mUxRClients) {
client.dispatchEventToClients(uxRestrictions);
}
}
- private CarUxRestrictions getDefaultRestrictions(@CarDrivingState int drivingState) {
- int restrictions;
- boolean requiresOpt = false;
- switch (drivingState) {
- case CarDrivingStateEvent.DRIVING_STATE_PARKED:
- restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
- break;
- case CarDrivingStateEvent.DRIVING_STATE_IDLING:
- restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
- requiresOpt = false;
- break;
- case CarDrivingStateEvent.DRIVING_STATE_MOVING:
- default:
- restrictions = CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
- requiresOpt = true;
- }
- return mHelper.createUxRestrictionsEvent(requiresOpt, restrictions);
+ CarUxRestrictionsConfiguration createDefaultConfig() {
+ return new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_PARKED,
+ false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
+ .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_IDLING,
+ false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
+ .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_MOVING,
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN,
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
}
private void addTransitionLog(String name, int from, int to, long timestamp, String extra) {
@@ -500,4 +687,9 @@
mTransitionLogs.add(tLog);
}
+ private static void logd(String msg) {
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
+ }
}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 736ffc8..e3381a3 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -111,13 +111,14 @@
mSystemInterface = systemInterface;
mHal = new VehicleHal(vehicle);
mVehicleInterfaceName = vehicleInterfaceName;
+ mUserManagerHelper = new CarUserManagerHelper(serviceContext);
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
systemInterface);
mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
- mCarDrivingStateService, mCarPropertyService);
+ mCarDrivingStateService, mCarPropertyService, mUserManagerHelper);
mCarPackageManagerService = new CarPackageManagerService(serviceContext,
mCarUXRestrictionsService,
mSystemActivityMonitoringService);
@@ -141,7 +142,6 @@
systemInterface);
mCarConfigurationService =
new CarConfigurationService(serviceContext, new JsonReaderImpl());
- mUserManagerHelper = new CarUserManagerHelper(serviceContext);
mCarLocationService = new CarLocationService(
mContext, mCarPropertyService, mUserManagerHelper);
diff --git a/service/src/com/android/car/audio/CarAudioDeviceInfo.java b/service/src/com/android/car/audio/CarAudioDeviceInfo.java
index 2e35c56..f45f48e 100644
--- a/service/src/com/android/car/audio/CarAudioDeviceInfo.java
+++ b/service/src/com/android/car/audio/CarAudioDeviceInfo.java
@@ -38,6 +38,26 @@
*/
/* package */ class CarAudioDeviceInfo {
+ /**
+ * Parse device address. Expected format is BUS%d_%s, address, usage hint
+ * @return valid address (from 0 to positive) or -1 for invalid address.
+ */
+ static int parseDeviceAddress(String address) {
+ String[] words = address.split("_");
+ int addressParsed = -1;
+ if (words[0].toLowerCase().startsWith("bus")) {
+ try {
+ addressParsed = Integer.parseInt(words[0].substring(3));
+ } catch (NumberFormatException e) {
+ //ignore
+ }
+ }
+ if (addressParsed < 0) {
+ return -1;
+ }
+ return addressParsed;
+ }
+
private final AudioDeviceInfo mAudioDeviceInfo;
private final int mBusNumber;
private final int mSampleRate;
@@ -143,26 +163,6 @@
}
}
- /**
- * Parse device address. Expected format is BUS%d_%s, address, usage hint
- * @return valid address (from 0 to positive) or -1 for invalid address.
- */
- private int parseDeviceAddress(String address) {
- String[] words = address.split("_");
- int addressParsed = -1;
- if (words[0].toLowerCase().startsWith("bus")) {
- try {
- addressParsed = Integer.parseInt(words[0].substring(3));
- } catch (NumberFormatException e) {
- //ignore
- }
- }
- if (addressParsed < 0) {
- return -1;
- }
- return addressParsed;
- }
-
private int getMaxSampleRate(AudioDeviceInfo info) {
int[] sampleRates = info.getSampleRates();
if (sampleRates == null || sampleRates.length == 0) {
@@ -247,12 +247,12 @@
+ " minGain: " + mMinGain;
}
- void dump(PrintWriter writer) {
- writer.printf("Bus Number (%d) / address (%s)\n ",
- mBusNumber, mAudioDeviceInfo.getAddress());
- writer.printf("\tsample rate / encoding format / channel count: %d %d %d\n",
- getSampleRate(), getEncodingFormat(), getChannelCount());
- writer.printf("\tGain in millibel (min / max / default/ current): %d %d %d %d\n",
- mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
+ void dump(String indent, PrintWriter writer) {
+ writer.printf("%sCarAudioDeviceInfo Bus(%d: %s)\n ",
+ indent, mBusNumber, mAudioDeviceInfo.getAddress());
+ writer.printf("%s\tsample rate / encoding format / channel count: %d %d %d\n",
+ indent, getSampleRate(), getEncodingFormat(), getChannelCount());
+ writer.printf("%s\tGain values (min / max / default/ current): %d %d %d %d\n",
+ indent, mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
}
}
diff --git a/service/src/com/android/car/audio/CarAudioDynamicRouting.java b/service/src/com/android/car/audio/CarAudioDynamicRouting.java
new file mode 100644
index 0000000..73b810c
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioDynamicRouting.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2018 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.audio;
+
+import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.car.CarLog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Builds dynamic audio routing in a car from audio zone configuration.
+ */
+/* package */ class CarAudioDynamicRouting {
+
+ static final int[] CONTEXT_NUMBERS = new int[] {
+ ContextNumber.MUSIC,
+ ContextNumber.NAVIGATION,
+ ContextNumber.VOICE_COMMAND,
+ ContextNumber.CALL_RING,
+ ContextNumber.CALL,
+ ContextNumber.ALARM,
+ ContextNumber.NOTIFICATION,
+ ContextNumber.SYSTEM_SOUND
+ };
+
+ static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray();
+
+ static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
+
+ // For legacy stream type based volume control.
+ // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
+ static final int[] STREAM_TYPES = new int[] {
+ AudioManager.STREAM_MUSIC,
+ AudioManager.STREAM_ALARM,
+ AudioManager.STREAM_RING
+ };
+ static final int[] STREAM_TYPE_USAGES = new int[] {
+ AudioAttributes.USAGE_MEDIA,
+ AudioAttributes.USAGE_ALARM,
+ AudioAttributes.USAGE_NOTIFICATION_RINGTONE
+ };
+
+ static {
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
+ ContextNumber.CALL);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
+ ContextNumber.NOTIFICATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
+ ContextNumber.NOTIFICATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
+ ContextNumber.NOTIFICATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
+ ContextNumber.VOICE_COMMAND);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+ ContextNumber.NAVIGATION);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
+ ContextNumber.SYSTEM_SOUND);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID);
+ USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND);
+ }
+
+ private final CarAudioZone[] mCarAudioZones;
+
+ CarAudioDynamicRouting(CarAudioZone[] carAudioZones) {
+ mCarAudioZones = carAudioZones;
+ }
+
+ void setupAudioDynamicRouting(AudioPolicy.Builder builder) {
+ for (CarAudioZone zone : mCarAudioZones) {
+ for (CarVolumeGroup group : zone.getVolumeGroups()) {
+ setupAudioDynamicRoutingForGroup(group, builder);
+ }
+ }
+ }
+
+ /**
+ * Enumerates all physical buses in a given volume group and attach the mixing rules.
+ * @param group {@link CarVolumeGroup} instance to enumerate the buses with
+ * @param builder {@link AudioPolicy.Builder} to attach the mixing rules
+ */
+ private void setupAudioDynamicRoutingForGroup(CarVolumeGroup group,
+ AudioPolicy.Builder builder) {
+ // Note that one can not register audio mix for same bus more than once.
+ for (int busNumber : group.getBusNumbers()) {
+ boolean hasContext = false;
+ CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForBus(busNumber);
+ AudioFormat mixFormat = new AudioFormat.Builder()
+ .setSampleRate(info.getSampleRate())
+ .setEncoding(info.getEncodingFormat())
+ .setChannelMask(info.getChannelCount())
+ .build();
+ AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
+ for (int contextNumber : group.getContextsForBus(busNumber)) {
+ hasContext = true;
+ int[] usages = getUsagesForContext(contextNumber);
+ for (int usage : usages) {
+ mixingRuleBuilder.addRule(
+ new AudioAttributes.Builder().setUsage(usage).build(),
+ AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+ }
+ Log.d(CarLog.TAG_AUDIO, "Bus number: " + busNumber
+ + " contextNumber: " + contextNumber
+ + " sampleRate: " + info.getSampleRate()
+ + " channels: " + info.getChannelCount()
+ + " usages: " + Arrays.toString(usages));
+ }
+ if (hasContext) {
+ // It's a valid case that an audio output bus is defined in
+ // audio_policy_configuration and no context is assigned to it.
+ // In such case, do not build a policy mix with zero rules.
+ AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
+ .setFormat(mixFormat)
+ .setDevice(info.getAudioDeviceInfo())
+ .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
+ .build();
+ builder.addMix(audioMix);
+ }
+ }
+ }
+
+ private int[] getUsagesForContext(int contextNumber) {
+ final List<Integer> usages = new ArrayList<>();
+ for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
+ if (CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i) == contextNumber) {
+ usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
+ }
+ }
+ return usages.stream().mapToInt(i -> i).toArray();
+ }
+}
diff --git a/service/src/com/android/car/audio/CarAudioFocus.java b/service/src/com/android/car/audio/CarAudioFocus.java
new file mode 100644
index 0000000..c4aff05
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioFocus.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.audio;
+
+import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
+import android.media.AudioAttributes;
+import android.media.AudioFocusInfo;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioPolicy;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+
+public class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
+
+ private static final String TAG = "CarAudioFocus";
+
+ private final AudioManager mAudioManager;
+ private CarAudioService mCarAudioService; // Dynamically assigned just after construction
+ private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
+
+
+ // Values for the internal interaction matrix we use to make focus decisions
+ private static final int INTERACTION_REJECT = 0; // Focus not granted
+ private static final int INTERACTION_EXCLUSIVE = 1; // Focus granted, others loose focus
+ private static final int INTERACTION_CONCURRENT = 2; // Focus granted, others keep focus
+
+ // TODO: Make this an overlayable resource...
+ // MUSIC = 1, // Music playback
+ // NAVIGATION = 2, // Navigation directions
+ // VOICE_COMMAND = 3, // Voice command session
+ // CALL_RING = 4, // Voice call ringing
+ // CALL = 5, // Voice call
+ // ALARM = 6, // Alarm sound from Android
+ // NOTIFICATION = 7, // Notifications
+ // SYSTEM_SOUND = 8, // User interaction sounds (button clicks, etc)
+ private static int sInteractionMatrix[][] = {
+ // Row selected by playing sound (labels along the right)
+ // Column selected by incoming request (labels along the top)
+ // Cell value is one of INTERACTION_REJECT, INTERACTION_EXCLUSIVE, INTERACTION_CONCURRENT
+ // Invalid, Music, Nav, Voice, Ring, Call, Alarm, Notification, System
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 1, 2, 1, 1, 1, 1, 2, 2 }, // Music
+ { 0, 2, 2, 1, 2, 1, 2, 2, 2 }, // Nav
+ { 0, 2, 0, 2, 1, 1, 0, 0, 0 }, // Voice
+ { 0, 0, 2, 2, 2, 2, 0, 0, 2 }, // Ring
+ { 0, 0, 2, 0, 2, 2, 2, 2, 0 }, // Context
+ { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Alarm
+ { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Notification
+ { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // System
+ };
+
+
+ private class FocusEntry {
+ // Requester info
+ final AudioFocusInfo mAfi; // never null
+
+ final int mAudioContext; // Which HAL level context does this affect
+ final ArrayList<FocusEntry> mBlockers; // List of requests that block ours
+
+ FocusEntry(AudioFocusInfo afi,
+ int context) {
+ mAfi = afi;
+ mAudioContext = context;
+ mBlockers = new ArrayList<FocusEntry>();
+ }
+
+ public String getClientId() {
+ return mAfi.getClientId();
+ }
+
+ public boolean wantsPauseInsteadOfDucking() {
+ return (mAfi.getFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0;
+ }
+ }
+
+
+ // We keep track of all the focus requesters in this map, with their clientId as the key.
+ // This is used both for focus dispatch and death handling
+ // Note that the clientId reflects the AudioManager instance and listener object (if any)
+ // so that one app can have more than one unique clientId by setting up distinct listeners.
+ // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
+ // it expects to request focus concurrently for different USAGEs so it knows which USAGE
+ // gained or lost focus at any given moment. If the SAME listener is used for requests of
+ // different USAGE while the earlier request is still in the focus stack (whether holding
+ // focus or pending), the new request will be REJECTED so as to avoid any confusion about
+ // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
+ // request that was already active or pending).
+ private HashMap<String, FocusEntry> mFocusHolders = new HashMap<String, FocusEntry>();
+ private HashMap<String, FocusEntry> mFocusLosers = new HashMap<String, FocusEntry>();
+
+
+ CarAudioFocus(AudioManager audioManager) {
+ mAudioManager = audioManager;
+ }
+
+
+ // This has to happen after the construction to avoid a chicken and egg problem when setting up
+ // the AudioPolicy which must depend on this object.
+ public void setOwningPolicy(CarAudioService audioService, AudioPolicy parentPolicy) {
+ mCarAudioService = audioService;
+ mAudioPolicy = parentPolicy;
+ }
+
+
+ // This sends a focus loss message to the targeted requester.
+ private void sendFocusLoss(FocusEntry loser, boolean permanent) {
+ int lossType = (permanent ? AudioManager.AUDIOFOCUS_LOSS :
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+ Log.i(TAG, "sendFocusLoss to " + loser.getClientId());
+ int result = mAudioManager.dispatchAudioFocusChange(loser.mAfi, lossType, mAudioPolicy);
+ if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ // TODO: Is this actually an error, or is it okay for an entry in the focus stack
+ // to NOT have a listener? If that's the case, should we even keep it in the focus
+ // stack?
+ Log.e(TAG, "Failure to signal loss of audio focus with error: " + result);
+ }
+ }
+
+
+ /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
+ // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
+ // engine as of Android P.
+ // Besides the interaction matrix which allows concurrent focus for multiple requestors, which
+ // is the reason for this module, we also treat repeated requests from the same clientId
+ // slightly differently.
+ // If a focus request for the same listener (clientId) is received while that listener is
+ // already in the focus stack, we REJECT it outright unless it is for the same USAGE.
+ // The default audio framework's behavior is to remove the previous entry in the stack (no-op
+ // if the requester is already holding focus).
+ int evaluateFocusRequest(AudioFocusInfo afi) {
+ Log.i(TAG, "Evaluating focus request for client " + afi.getClientId());
+
+ // Is this a request for premanant focus?
+ // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
+ // AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss
+ // AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us)
+ // NOTE: We expect that in practice it will be permanent for all media requests and
+ // transient for everything else, but that isn't currently an enforced requirement.
+ final boolean permanent =
+ (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN);
+ final boolean allowDucking =
+ (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
+
+
+ // Convert from audio attributes "usage" to HAL level "context"
+ final int requestedContext = mCarAudioService.getContextForUsage(
+ afi.getAttributes().getUsage());
+
+ // If we happen find an entry that this new request should replace, we'll store it here.
+ FocusEntry deprecatedBlockedEntry = null;
+
+ // Scan all active and pending focus requests. If any should cause rejection of
+ // this new request, then we're done. Keep a list of those against whom we're exclusive
+ // so we can update the relationships if/when we are sure we won't get rejected.
+ Log.i(TAG, "Scanning focus holders...");
+ final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>();
+ for (FocusEntry entry : mFocusHolders.values()) {
+ Log.i(TAG, entry.mAfi.getClientId());
+
+ // If this request is for Notifications and a current focus holder has specified
+ // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
+ // This matches the hardwired behavior in the default audio policy engine which apps
+ // might expect (The interaction matrix doesn't have any provision for dealing with
+ // override flags like this).
+ if ((requestedContext == ContextNumber.NOTIFICATION) &&
+ (entry.mAfi.getGainRequest() ==
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ // We don't allow sharing listeners (client IDs) between two concurrent requests
+ // (because the app would have no way to know to which request a later event applied)
+ if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+ if (entry.mAudioContext == requestedContext) {
+ // Trivially accept if this request is a duplicate
+ Log.i(TAG, "Duplicate request from focus holder is accepted");
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ } else {
+ // Trivially reject a request for a different USAGE
+ Log.i(TAG, "Different request from focus holder is rejected");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+ }
+
+ // Check the interaction matrix for the relationship between this entry and the request
+ switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
+ case INTERACTION_REJECT:
+ // This request is rejected, so nothing further to do
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ case INTERACTION_EXCLUSIVE:
+ // The new request will cause this existing entry to lose focus
+ losers.add(entry);
+ break;
+ default:
+ // If ducking isn't allowed by the focus requestor, then everybody else
+ // must get a LOSS.
+ // If a focus holder has set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
+ // they must get a LOSS message even if ducking would otherwise be allowed.
+ if ((!allowDucking) ||
+ (entry.mAfi.getFlags() &
+ AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) {
+ // The new request will cause audio book to lose focus and pause
+ losers.add(entry);
+ }
+ }
+ }
+ Log.i(TAG, "Scanning those who've already lost focus...");
+ final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>();
+ for (FocusEntry entry : mFocusLosers.values()) {
+ Log.i(TAG, entry.mAfi.getClientId());
+
+ // If this request is for Notifications and a pending focus holder has specified
+ // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
+ if ((requestedContext == ContextNumber.NOTIFICATION) &&
+ (entry.mAfi.getGainRequest() ==
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ // We don't allow sharing listeners (client IDs) between two concurrent requests
+ // (because the app would have no way to know to which request a later event applied)
+ if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+ if (entry.mAudioContext == requestedContext) {
+ // This is a repeat of a request that is currently blocked.
+ // Evaluate it as if it were a new request, but note that we should remove
+ // the old pending request, and move it.
+ // We do not want to evaluate the new request against itself.
+ Log.i(TAG, "Duplicate request while waiting is being evaluated");
+ deprecatedBlockedEntry = entry;
+ continue;
+ } else {
+ // Trivially reject a request for a different USAGE
+ Log.i(TAG, "Different request while waiting is rejected");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+ }
+
+ // Check the interaction matrix for the relationship between this entry and the request
+ switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
+ case INTERACTION_REJECT:
+ // Even though this entry has currently lost focus, the fact that it is
+ // waiting to play means we'll reject this new conflicting request.
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ case INTERACTION_EXCLUSIVE:
+ // The new request is yet another reason this entry cannot regain focus (yet)
+ blocked.add(entry);
+ break;
+ default:
+ // If ducking is not allowed by the requester, or the pending focus holder had
+ // set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
+ // then the pending holder must stay "lost" until this requester goes away.
+ if ((!allowDucking) || entry.wantsPauseInsteadOfDucking()) {
+ // The new request is yet another reason this entry cannot regain focus yet
+ blocked.add(entry);
+ }
+ }
+ }
+
+
+ // Now that we've decided we'll grant focus, construct our new FocusEntry
+ FocusEntry newEntry = new FocusEntry(afi, requestedContext);
+
+
+ // Now that we're sure we'll accept this request, update any requests which we would
+ // block but are already out of focus but waiting to come back
+ for (FocusEntry entry : blocked) {
+ // If we're out of focus it must be because somebody is blocking us
+ assert !entry.mBlockers.isEmpty();
+
+ if (permanent) {
+ // This entry has now lost focus forever
+ sendFocusLoss(entry, permanent);
+ final FocusEntry deadEntry = mFocusLosers.remove(entry.mAfi.getClientId());
+ assert deadEntry != null;
+ } else {
+ // Note that this new request is yet one more reason we can't (yet) have focus
+ entry.mBlockers.add(newEntry);
+ }
+ }
+
+ // Notify and update any requests which are now losing focus as a result of the new request
+ for (FocusEntry entry : losers) {
+ // If we have focus (but are about to loose it), nobody should be blocking us yet
+ assert entry.mBlockers.isEmpty();
+
+ sendFocusLoss(entry, permanent);
+
+ // The entry no longer holds focus, so take it out of the holders list
+ mFocusHolders.remove(entry.mAfi.getClientId());
+
+ if (!permanent) {
+ // Add ourselves to the list of requests waiting to get focus back and
+ // note why we lost focus so we can tell when it's time to get it back
+ mFocusLosers.put(entry.mAfi.getClientId(), entry);
+ entry.mBlockers.add(newEntry);
+ }
+ }
+
+ // If we encountered a duplicate of this request that was pending, but now we're going to
+ // grant focus, we need to remove the old pending request (without sending a LOSS message).
+ if (deprecatedBlockedEntry != null) {
+ mFocusLosers.remove(deprecatedBlockedEntry.mAfi.getClientId());
+ }
+
+ // Finally, add the request we're granting to the focus holders' list
+ mFocusHolders.put(afi.getClientId(), newEntry);
+
+ Log.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+
+ @Override
+ public synchronized void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
+ Log.i(TAG, "onAudioFocusRequest " + afi);
+
+ int response = evaluateFocusRequest(afi);
+
+ // Post our reply for delivery to the original focus requester
+ mAudioManager.setFocusRequestResult(afi, response, mAudioPolicy);
+ }
+
+
+ /**
+ * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
+ * Note that we'll get this call for a focus holder that dies while in the focus statck, so
+ * we don't need to watch for death notifications directly.
+ * */
+ @Override
+ public synchronized void onAudioFocusAbandon(AudioFocusInfo afi) {
+ Log.i(TAG, "onAudioFocusAbandon " + afi);
+
+ // Remove this entry from our active or pending list
+ FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId());
+ if (deadEntry == null) {
+ deadEntry = mFocusLosers.remove(afi.getClientId());
+ if (deadEntry == null) {
+ // Caller is providing an unrecognzied clientId!?
+ Log.w(TAG, "Audio focus abandoned by unrecognized client id: " + afi.getClientId());
+ // This probably means an app double released focused for some reason. One
+ // harmless possibility is a race between an app being told it lost focus and the
+ // app voluntarily abandoning focus. More likely the app is just sloppy. :)
+ // The more nefarious possibility is that the clientId is actually corrupted
+ // somehow, in which case we might have a real focus entry that we're going to fail
+ // to remove. If that were to happen, I'd expect either the app to swallow it
+ // silently, or else take unexpected action (eg: resume playing spontaneously), or
+ // else to see "Failure to signal ..." gain/loss error messages in the log from
+ // this module when a focus change tries to take action on a truly zombie entry.
+ }
+ }
+
+ // Remove this entry from the blocking list of any pending requests
+ Iterator<FocusEntry> it = mFocusLosers.values().iterator();
+ while (it.hasNext()) {
+ FocusEntry entry = it.next();
+
+ // Remove the retiring entry from all blocker lists
+ entry.mBlockers.remove(deadEntry);
+
+ // Any entry whose blocking list becomes empty should regain focus
+ if (entry.mBlockers.isEmpty()) {
+ // Pull this entry out of the focus losers list
+ it.remove();
+
+ // Add it back into the focus holders list
+ mFocusHolders.put(entry.getClientId(), entry);
+
+ // Send the focus (re)gain notification
+ int result = mAudioManager.dispatchAudioFocusChange(
+ entry.mAfi,
+ entry.mAfi.getGainRequest(),
+ mAudioPolicy);
+ if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ // TODO: Is this actually an error, or is it okay for an entry in the focus
+ // stack to NOT have a listener? If that's the case, should we even keep
+ // it in the focus stack?
+ Log.e(TAG, "Failure to signal gain of audio focus with error: " + result);
+ }
+ }
+ }
+ }
+
+
+ public synchronized void dump(PrintWriter writer) {
+ writer.println("*CarAudioFocus*");
+
+ writer.println(" Current Focus Holders:");
+ for (String clientId : mFocusHolders.keySet()) {
+ System.out.println(clientId);
+ }
+
+ writer.println(" Transient Focus Losers:");
+ for (String clientId : mFocusLosers.keySet()) {
+ System.out.println(clientId);
+ }
+ }
+}
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index ebfe93e..324c5ac 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -27,7 +27,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
@@ -40,8 +39,6 @@
import android.media.AudioPlaybackConfiguration;
import android.media.AudioPortConfig;
import android.media.AudioSystem;
-import android.media.audiopolicy.AudioMix;
-import android.media.audiopolicy.AudioMixingRule;
import android.media.audiopolicy.AudioPolicy;
import android.os.IBinder;
import android.os.Looper;
@@ -51,7 +48,6 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
-import android.util.SparseIntArray;
import android.view.KeyEvent;
import com.android.car.BinderInterfaceContainer;
@@ -63,7 +59,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
@@ -74,59 +69,30 @@
*/
public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
- private static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
+ // Turning this off will result in falling back to the default focus policy of Android
+ // (which boils down to "grant if not in a phone call, else deny").
+ // Aside from the obvious effect of ignoring the logic in CarAudioFocus, this will also
+ // result in the framework taking over responsibility for ducking in TRANSIENT_LOSS cases.
+ // Search for "DUCK_VSHAPE" in PLaybackActivityMonitor.java to see where this happens.
+ private static boolean sUseCarAudioFocus = true;
- private static final int[] CONTEXT_NUMBERS = new int[] {
- ContextNumber.MUSIC,
- ContextNumber.NAVIGATION,
- ContextNumber.VOICE_COMMAND,
- ContextNumber.CALL_RING,
- ContextNumber.CALL,
- ContextNumber.ALARM,
- ContextNumber.NOTIFICATION,
- ContextNumber.SYSTEM_SOUND
- };
+ // Key to persist master mute state in system settings
+ private static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
- private static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray();
+ // The trailing slash forms a directory-liked hierarchy and
+ // allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
+ private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
- // For legacy stream type based volume control.
- // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
- private static final int[] STREAM_TYPES = new int[] {
- AudioManager.STREAM_MUSIC,
- AudioManager.STREAM_ALARM,
- AudioManager.STREAM_RING
- };
- private static final int[] STREAM_TYPE_USAGES = new int[] {
- AudioAttributes.USAGE_MEDIA,
- AudioAttributes.USAGE_ALARM,
- AudioAttributes.USAGE_NOTIFICATION_RINGTONE
- };
-
- static {
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
- ContextNumber.CALL);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
- ContextNumber.NOTIFICATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
- ContextNumber.NOTIFICATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
- ContextNumber.NOTIFICATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
- ContextNumber.VOICE_COMMAND);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
- ContextNumber.NAVIGATION);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
- ContextNumber.SYSTEM_SOUND);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID);
- USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND);
+ /**
+ * Gets the key to persist volume for a volume group in settings
+ *
+ * @param zoneId The audio zone id
+ * @param groupId The volume group id
+ * @return Key to persist volume index for volume group in system settings
+ */
+ static String getVolumeSettingsKeyForGroup(int zoneId, int groupId) {
+ final int maskedGroupId = (zoneId << 8) + groupId;
+ return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + maskedGroupId;
}
private final Object mImplLock = new Object();
@@ -135,9 +101,8 @@
private final TelephonyManager mTelephonyManager;
private final AudioManager mAudioManager;
private final boolean mUseDynamicRouting;
+ private final boolean mUseUnifiedConfiguration;
private final boolean mPersistMasterMuteState;
- private final SparseIntArray mContextToBus = new SparseIntArray();
- private final SparseArray<CarAudioDeviceInfo> mCarAudioDeviceInfos = new SparseArray<>();
private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
new AudioPolicy.AudioPolicyVolumeCallback() {
@@ -147,29 +112,31 @@
Log.v(CarLog.TAG_AUDIO,
"onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
+ " suggested usage: " + AudioAttributes.usageToString(usage));
- final int groupId = getVolumeGroupIdForUsage(usage);
- final int currentVolume = getGroupVolume(groupId);
+ // TODO: Pass zone id into this callback.
+ final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+ final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
+ final int currentVolume = getGroupVolume(zoneId, groupId);
final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
switch (adjustment) {
case AudioManager.ADJUST_LOWER:
- int minValue = Math.max(currentVolume - 1, getGroupMinVolume(groupId));
- setGroupVolume(groupId, minValue , flags);
+ int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
+ setGroupVolume(zoneId, groupId, minValue , flags);
break;
case AudioManager.ADJUST_RAISE:
- int maxValue = Math.min(currentVolume + 1, getGroupMaxVolume(groupId));
- setGroupVolume(groupId, maxValue, flags);
+ int maxValue = Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
+ setGroupVolume(zoneId, groupId, maxValue, flags);
break;
case AudioManager.ADJUST_MUTE:
setMasterMute(true, flags);
- callbackMasterMuteChange(flags);
+ callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_UNMUTE:
setMasterMute(false, flags);
- callbackMasterMuteChange(flags);
+ callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_TOGGLE_MUTE:
setMasterMute(!mAudioManager.isMasterMute(), flags);
- callbackMasterMuteChange(flags);
+ callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_SAME:
default:
@@ -183,10 +150,12 @@
/**
* Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
+ * This receiver assumes the intent is sent to {@link CarAudioManager#PRIMARY_AUDIO_ZONE}.
*/
private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
+ final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
switch (intent.getAction()) {
case AudioManager.VOLUME_CHANGED_ACTION:
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
@@ -194,24 +163,27 @@
if (groupId == -1) {
Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
} else {
- callbackGroupVolumeChange(groupId, 0);
+ callbackGroupVolumeChange(zoneId, groupId, 0);
}
break;
case AudioManager.MASTER_MUTE_CHANGED_ACTION:
- callbackMasterMuteChange(0);
+ callbackMasterMuteChange(zoneId, 0);
break;
}
}
};
private AudioPolicy mAudioPolicy;
- private CarVolumeGroup[] mCarVolumeGroups;
+ private CarAudioFocus mFocusHandler;
+ private CarAudioZone[] mCarAudioZones;
public CarAudioService(Context context) {
mContext = context;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
+ mUseUnifiedConfiguration = mContext.getResources().getBoolean(
+ R.bool.audioUseUnifiedConfiguration);
mPersistMasterMuteState = mContext.getResources().getBoolean(
R.bool.audioPersistMasterMuteState);
}
@@ -223,18 +195,38 @@
@Override
public void init() {
synchronized (mImplLock) {
- if (!mUseDynamicRouting) {
- Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not configured, run in legacy mode");
- setupLegacyVolumeChangedListener();
+ if (mUseDynamicRouting) {
+ // Enumerate all output bus device ports
+ AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (deviceInfos.length == 0) {
+ Log.e(CarLog.TAG_AUDIO, "No output device available, ignore");
+ return;
+ }
+ SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>();
+ for (AudioDeviceInfo info : deviceInfos) {
+ Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
+ info.getId(), info.getAddress(), info.getType()));
+ if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
+ final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
+ // See also the audio_policy_configuration.xml,
+ // the bus number should be no less than zero.
+ if (carInfo.getBusNumber() >= 0) {
+ busToCarAudioDeviceInfo.put(carInfo.getBusNumber(), carInfo);
+ Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
+ }
+ }
+ }
+ setupDynamicRouting(busToCarAudioDeviceInfo);
} else {
- setupDynamicRouting();
- setupVolumeGroups();
+ Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
+ setupLegacyVolumeChangedListener();
}
// Restore master mute state if applicable
if (mPersistMasterMuteState) {
boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(),
- CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
+ VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
setMasterMute(storedMasterMute, 0);
}
}
@@ -247,6 +239,8 @@
if (mAudioPolicy != null) {
mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
mAudioPolicy = null;
+ mFocusHandler.setOwningPolicy(null, null);
+ mFocusHandler = null;
}
} else {
mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
@@ -260,42 +254,44 @@
public void dump(PrintWriter writer) {
writer.println("*CarAudioService*");
writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting));
+ writer.println("\tUse unified configuration? " + mUseUnifiedConfiguration);
writer.println("\tPersist master mute state? " + mPersistMasterMuteState);
writer.println("\tMaster muted? " + mAudioManager.isMasterMute());
// Empty line for comfortable reading
writer.println();
if (mUseDynamicRouting) {
- for (CarVolumeGroup group : mCarVolumeGroups) {
- group.dump(writer);
+ for (CarAudioZone zone : mCarAudioZones) {
+ zone.dump("\t", writer);
}
}
}
/**
- * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int)}
+ * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int, int)}
*/
@Override
- public void setGroupVolume(int groupId, int index, int flags) {
+ public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
- callbackGroupVolumeChange(groupId, flags);
+ callbackGroupVolumeChange(zoneId, groupId, flags);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
- mAudioManager.setStreamVolume(STREAM_TYPES[groupId], index, flags);
+ mAudioManager.setStreamVolume(
+ CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
return;
}
- CarVolumeGroup group = getCarVolumeGroup(groupId);
+ CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
group.setCurrentGainIndex(index);
}
}
- private void callbackGroupVolumeChange(int groupId, int flags) {
+ private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
mVolumeCallbackContainer.getInterfaces()) {
try {
- callback.binderInterface.onGroupVolumeChanged(groupId, flags);
+ callback.binderInterface.onGroupVolumeChanged(zoneId, groupId, flags);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
}
@@ -312,11 +308,11 @@
mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode));
}
- private void callbackMasterMuteChange(int flags) {
+ private void callbackMasterMuteChange(int zoneId, int flags) {
for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
mVolumeCallbackContainer.getInterfaces()) {
try {
- callback.binderInterface.onMasterMuteChanged(flags);
+ callback.binderInterface.onMasterMuteChanged(zoneId, flags);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e);
}
@@ -325,70 +321,73 @@
// Persists master mute state if applicable
if (mPersistMasterMuteState) {
Settings.Global.putInt(mContext.getContentResolver(),
- CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE,
+ VOLUME_SETTINGS_KEY_MASTER_MUTE,
mAudioManager.isMasterMute() ? 1 : 0);
}
}
/**
- * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int)}
+ * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int, int)}
*/
@Override
- public int getGroupMaxVolume(int groupId) {
+ public int getGroupMaxVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
- return mAudioManager.getStreamMaxVolume(STREAM_TYPES[groupId]);
+ return mAudioManager.getStreamMaxVolume(
+ CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
- CarVolumeGroup group = getCarVolumeGroup(groupId);
+ CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getMaxGainIndex();
}
}
/**
- * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int)}
+ * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int, int)}
*/
@Override
- public int getGroupMinVolume(int groupId) {
+ public int getGroupMinVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
- return mAudioManager.getStreamMinVolume(STREAM_TYPES[groupId]);
+ return mAudioManager.getStreamMinVolume(
+ CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
- CarVolumeGroup group = getCarVolumeGroup(groupId);
+ CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getMinGainIndex();
}
}
/**
- * @see {@link android.car.media.CarAudioManager#getGroupVolume(int)}
+ * @see {@link android.car.media.CarAudioManager#getGroupVolume(int, int)}
*/
@Override
- public int getGroupVolume(int groupId) {
+ public int getGroupVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
- return mAudioManager.getStreamVolume(STREAM_TYPES[groupId]);
+ return mAudioManager.getStreamVolume(
+ CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
- CarVolumeGroup group = getCarVolumeGroup(groupId);
+ CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getCurrentGainIndex();
}
}
- private CarVolumeGroup getCarVolumeGroup(int groupId) {
- Preconditions.checkNotNull(mCarVolumeGroups);
- Preconditions.checkArgument(groupId >= 0 && groupId < mCarVolumeGroups.length,
- "groupId out of range: " + groupId);
- return mCarVolumeGroups[groupId];
+ private CarVolumeGroup getCarVolumeGroup(int zoneId, int groupId) {
+ Preconditions.checkNotNull(mCarAudioZones);
+ Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+ "zoneId out of range: " + zoneId);
+ return mCarAudioZones[zoneId].getVolumeGroup(groupId);
}
private void setupLegacyVolumeChangedListener() {
@@ -398,186 +397,67 @@
mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
}
- private void setupDynamicRouting() {
- final IAudioControl audioControl = getAudioControl();
- if (audioControl == null) {
- return;
+ private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
+ final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
+ builder.setLooper(Looper.getMainLooper());
+
+ final CarAudioZonesLoader zonesLoader;
+ if (mUseUnifiedConfiguration) {
+ zonesLoader = new CarAudioZonesHelper(mContext, R.xml.car_audio_configuration,
+ busToCarAudioDeviceInfo);
+ } else {
+ // In legacy mode, context -> bus mapping is done by querying IAudioControl HAL.
+ final IAudioControl audioControl = getAudioControl();
+ if (audioControl == null) {
+ throw new RuntimeException(
+ "Dynamic routing requested but audioControl HAL not available");
+ }
+ zonesLoader = new CarAudioZonesHelperLegacy(mContext, R.xml.car_volume_groups,
+ busToCarAudioDeviceInfo, audioControl);
}
- AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl);
- int r = mAudioManager.registerAudioPolicy(audioPolicy);
+ mCarAudioZones = zonesLoader.loadAudioZones();
+ for (CarAudioZone zone : mCarAudioZones) {
+ if (!zone.validateVolumeGroups()) {
+ throw new RuntimeException("Invalid volume groups configuration");
+ }
+ // Ensure HAL gets our initial value
+ zone.synchronizeCurrentGainIndex();
+ Log.v(CarLog.TAG_AUDIO, "Processed audio zone: " + zone);
+ }
+
+ // Setup dynamic routing rules by usage
+ final CarAudioDynamicRouting dynamicRouting = new CarAudioDynamicRouting(mCarAudioZones);
+ dynamicRouting.setupAudioDynamicRouting(builder);
+
+ // Attach the {@link AudioPolicyVolumeCallback}
+ builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
+
+ if (sUseCarAudioFocus) {
+ // Configure our AudioPolicy to handle focus events.
+ // This gives us the ability to decide which audio focus requests to accept and bypasses
+ // the framework ducking logic.
+ mFocusHandler = new CarAudioFocus(mAudioManager);
+ builder.setAudioPolicyFocusListener(mFocusHandler);
+ builder.setIsAudioFocusPolicy(true);
+ }
+
+ mAudioPolicy = builder.build();
+ if (sUseCarAudioFocus) {
+ // Connect the AudioPolicy and the focus listener
+ mFocusHandler.setOwningPolicy(this, mAudioPolicy);
+ }
+
+ int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
if (r != AudioManager.SUCCESS) {
throw new RuntimeException("registerAudioPolicy failed " + r);
}
- mAudioPolicy = audioPolicy;
- }
-
- private void setupVolumeGroups() {
- Preconditions.checkArgument(mCarAudioDeviceInfos.size() > 0,
- "No bus device is configured to setup volume groups");
- final CarVolumeGroupsHelper helper = new CarVolumeGroupsHelper(
- mContext, R.xml.car_volume_groups);
- mCarVolumeGroups = helper.loadVolumeGroups();
- for (CarVolumeGroup group : mCarVolumeGroups) {
- for (int contextNumber : group.getContexts()) {
- int busNumber = mContextToBus.get(contextNumber);
- group.bind(contextNumber, busNumber, mCarAudioDeviceInfos.get(busNumber));
- }
-
- // Now that we have all our contexts, ensure the HAL gets our intial value
- group.setCurrentGainIndex(group.getCurrentGainIndex());
-
- Log.v(CarLog.TAG_AUDIO, "Processed volume group: " + group);
- }
- // Perform validation after all volume groups are processed
- if (!validateVolumeGroups()) {
- throw new RuntimeException("Invalid volume groups configuration");
- }
}
/**
- * Constraints applied here:
- *
- * - One context should not appear in two groups
- * - All contexts are assigned
- * - One bus should not appear in two groups
- * - All gain controllers in the same group have same step value
- *
- * Note that it is fine that there are buses not appear in any group, those buses may be
- * reserved for other usages.
- * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
- *
- * See also the car_volume_groups.xml configuration
+ * @return Context number for a given audio usage, 0 if the given usage is unrecognized.
*/
- private boolean validateVolumeGroups() {
- Set<Integer> contextSet = new HashSet<>();
- Set<Integer> busNumberSet = new HashSet<>();
- for (CarVolumeGroup group : mCarVolumeGroups) {
- // One context should not appear in two groups
- for (int context : group.getContexts()) {
- if (contextSet.contains(context)) {
- Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
- return false;
- }
- contextSet.add(context);
- }
-
- // One bus should not appear in two groups
- for (int busNumber : group.getBusNumbers()) {
- if (busNumberSet.contains(busNumber)) {
- Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
- return false;
- }
- busNumberSet.add(busNumber);
- }
- }
-
- // All contexts are assigned
- if (contextSet.size() != CONTEXT_NUMBERS.length) {
- Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
- Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
- + Arrays.toString(contextSet.toArray(new Integer[contextSet.size()])));
- Log.e(CarLog.TAG_AUDIO, "All contexts " + Arrays.toString(CONTEXT_NUMBERS));
- return false;
- }
-
- return true;
- }
-
- @Nullable
- private AudioPolicy getDynamicAudioPolicy(@NonNull IAudioControl audioControl) {
- AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
- builder.setLooper(Looper.getMainLooper());
-
- // 1st, enumerate all output bus device ports
- AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- if (deviceInfos.length == 0) {
- Log.e(CarLog.TAG_AUDIO, "getDynamicAudioPolicy, no output device available, ignore");
- return null;
- }
- for (AudioDeviceInfo info : deviceInfos) {
- Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
- info.getId(), info.getAddress(), info.getType()));
- if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
- final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
- // See also the audio_policy_configuration.xml and getBusForContext in
- // audio control HAL, the bus number should be no less than zero.
- if (carInfo.getBusNumber() >= 0) {
- mCarAudioDeviceInfos.put(carInfo.getBusNumber(), carInfo);
- Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
- }
- }
- }
-
- // 2nd, map context to physical bus
- try {
- for (int contextNumber : CONTEXT_NUMBERS) {
- int busNumber = audioControl.getBusForContext(contextNumber);
- mContextToBus.put(contextNumber, busNumber);
- CarAudioDeviceInfo info = mCarAudioDeviceInfos.get(busNumber);
- if (info == null) {
- Log.w(CarLog.TAG_AUDIO, "No bus configured for context: " + contextNumber);
- }
- }
- } catch (RemoteException e) {
- Log.e(CarLog.TAG_AUDIO, "Error mapping context to physical bus", e);
- }
-
- // 3rd, enumerate all physical buses and build the routing policy.
- // Note that one can not register audio mix for same bus more than once.
- for (int i = 0; i < mCarAudioDeviceInfos.size(); i++) {
- int busNumber = mCarAudioDeviceInfos.keyAt(i);
- boolean hasContext = false;
- CarAudioDeviceInfo info = mCarAudioDeviceInfos.valueAt(i);
- AudioFormat mixFormat = new AudioFormat.Builder()
- .setSampleRate(info.getSampleRate())
- .setEncoding(info.getEncodingFormat())
- .setChannelMask(info.getChannelCount())
- .build();
- AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
- for (int j = 0; j < mContextToBus.size(); j++) {
- if (mContextToBus.valueAt(j) == busNumber) {
- hasContext = true;
- int contextNumber = mContextToBus.keyAt(j);
- int[] usages = getUsagesForContext(contextNumber);
- for (int usage : usages) {
- mixingRuleBuilder.addRule(
- new AudioAttributes.Builder().setUsage(usage).build(),
- AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
- }
- Log.i(CarLog.TAG_AUDIO, "Bus number: " + busNumber
- + " contextNumber: " + contextNumber
- + " sampleRate: " + info.getSampleRate()
- + " channels: " + info.getChannelCount()
- + " usages: " + Arrays.toString(usages));
- }
- }
- if (hasContext) {
- // It's a valid case that an audio output bus is defined in
- // audio_policy_configuration and no context is assigned to it.
- // In such case, do not build a policy mix with zero rules.
- AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
- .setFormat(mixFormat)
- .setDevice(info.getAudioDeviceInfo())
- .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
- .build();
- builder.addMix(audioMix);
- }
- }
-
- // 4th, attach the {@link AudioPolicyVolumeCallback}
- builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
-
- return builder.build();
- }
-
- private int[] getUsagesForContext(int contextNumber) {
- final List<Integer> usages = new ArrayList<>();
- for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
- if (USAGE_TO_CONTEXT.valueAt(i) == contextNumber) {
- usages.add(USAGE_TO_CONTEXT.keyAt(i));
- }
- }
- return usages.stream().mapToInt(i -> i).toArray();
+ int getContextForUsage(int audioUsage) {
+ return CarAudioDynamicRouting.USAGE_TO_CONTEXT.get(audioUsage);
}
@Override
@@ -650,7 +530,7 @@
}
}
- return sourceAddresses.toArray(new String[sourceAddresses.size()]);
+ return sourceAddresses.toArray(new String[0]);
}
}
@@ -726,8 +606,9 @@
Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]);
// Ensure the initial volume on output device port
- int groupId = getVolumeGroupIdForUsage(usage);
- setGroupVolume(groupId, getGroupVolume(groupId), 0);
+ int groupId = getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE, usage);
+ setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId,
+ getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId), 0);
return new CarAudioPatchHandle(patch[0]);
}
@@ -761,30 +642,30 @@
}
@Override
- public int getVolumeGroupCount() {
+ public int getVolumeGroupCount(int zoneId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
-
// For legacy stream type based volume control
- if (!mUseDynamicRouting) return STREAM_TYPES.length;
+ if (!mUseDynamicRouting) return CarAudioDynamicRouting.STREAM_TYPES.length;
- return mCarVolumeGroups == null ? 0 : mCarVolumeGroups.length;
+ Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+ "zoneId out of range: " + zoneId);
+ return mCarAudioZones[zoneId].getVolumeGroupCount();
}
}
@Override
- public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) {
+ public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+ Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+ "zoneId out of range: " + zoneId);
- if (mCarVolumeGroups == null) {
- return -1;
- }
-
- for (int i = 0; i < mCarVolumeGroups.length; i++) {
- int[] contexts = mCarVolumeGroups[i].getContexts();
+ CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
+ for (int i = 0; i < groups.length; i++) {
+ int[] contexts = groups[i].getContexts();
for (int context : contexts) {
- if (USAGE_TO_CONTEXT.get(usage) == context) {
+ if (getContextForUsage(usage) == context) {
return i;
}
}
@@ -794,31 +675,28 @@
}
@Override
- public @NonNull int[] getUsagesForVolumeGroupId(int groupId) {
+ public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
- return new int[] { STREAM_TYPE_USAGES[groupId] };
+ return new int[] { CarAudioDynamicRouting.STREAM_TYPE_USAGES[groupId] };
}
- CarVolumeGroup group = getCarVolumeGroup(groupId);
+ CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
Set<Integer> contexts =
Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet());
final List<Integer> usages = new ArrayList<>();
- for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
- if (contexts.contains(USAGE_TO_CONTEXT.valueAt(i))) {
- usages.add(USAGE_TO_CONTEXT.keyAt(i));
+ for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
+ if (contexts.contains(CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i))) {
+ usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
}
}
return usages.stream().mapToInt(i -> i).toArray();
}
}
- /**
- * See {@link android.car.media.CarAudioManager#registerVolumeCallback(IBinder)}
- */
@Override
public void registerVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
@@ -828,9 +706,6 @@
}
}
- /**
- * See {@link android.car.media.CarAudioManager#unregisterVolumeCallback(IBinder)}
- */
@Override
public void unregisterVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
@@ -852,11 +727,13 @@
* Multiple usages may share one {@link AudioDevicePort}
*/
private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) {
- final int groupId = getVolumeGroupIdForUsage(usage);
- final CarVolumeGroup group = Preconditions.checkNotNull(mCarVolumeGroups[groupId],
+ int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+ final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
+ final CarVolumeGroup group = Preconditions.checkNotNull(
+ mCarAudioZones[zoneId].getVolumeGroup(groupId),
"Can not find CarVolumeGroup by usage: "
+ AudioAttributes.usageToString(usage));
- return group.getAudioDevicePortForContext(USAGE_TO_CONTEXT.get(usage));
+ return group.getAudioDevicePortForContext(getContextForUsage(usage));
}
/**
@@ -879,7 +756,7 @@
return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage();
} else {
// TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
- return DEFAULT_AUDIO_USAGE;
+ return CarAudioDynamicRouting.DEFAULT_AUDIO_USAGE;
}
}
}
@@ -891,8 +768,8 @@
*/
private int getVolumeGroupIdForStreamType(int streamType) {
int groupId = -1;
- for (int i = 0; i < STREAM_TYPES.length; i++) {
- if (streamType == STREAM_TYPES[i]) {
+ for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
+ if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
groupId = i;
break;
}
@@ -911,4 +788,8 @@
}
return null;
}
+
+ interface CarAudioZonesLoader {
+ CarAudioZone[] loadAudioZones();
+ }
}
diff --git a/service/src/com/android/car/audio/CarAudioZone.java b/service/src/com/android/car/audio/CarAudioZone.java
new file mode 100644
index 0000000..b827906
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioZone.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2018 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.audio;
+
+import android.car.media.CarAudioManager;
+import android.util.Log;
+
+import com.android.car.CarLog;
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A class encapsulates an audio zone in car.
+ *
+ * An audio zone can contain multiple {@link CarVolumeGroup}s, and each zone has its own
+ * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys
+ * attached to each zone.
+ *
+ * See also the unified car_audio_configuration.xml
+ */
+/* package */ class CarAudioZone {
+
+ private final int mId;
+ private final String mName;
+ private final List<CarVolumeGroup> mVolumeGroups;
+
+ CarAudioZone(int id, String name) {
+ mId = id;
+ mName = name;
+ mVolumeGroups = new ArrayList<>();
+ }
+
+ int getId() {
+ return mId;
+ }
+
+ String getName() {
+ return mName;
+ }
+
+ boolean isPrimaryZone() {
+ return mId == CarAudioManager.PRIMARY_AUDIO_ZONE;
+ }
+
+ void addVolumeGroup(CarVolumeGroup volumeGroup) {
+ mVolumeGroups.add(volumeGroup);
+ }
+
+ CarVolumeGroup getVolumeGroup(int groupId) {
+ Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1,
+ "groupId(" + groupId + ") is out of range");
+ return mVolumeGroups.get(groupId);
+ }
+
+ int getVolumeGroupCount() {
+ return mVolumeGroups.size();
+ }
+
+ /**
+ * @return Snapshot of available {@link CarVolumeGroup}s in array.
+ */
+ CarVolumeGroup[] getVolumeGroups() {
+ return mVolumeGroups.toArray(new CarVolumeGroup[0]);
+ }
+
+ /**
+ * Constraints applied here:
+ *
+ * - One context should not appear in two groups
+ * - All contexts are assigned
+ * - One bus should not appear in two groups
+ * - All gain controllers in the same group have same step value
+ *
+ * Note that it is fine that there are buses not appear in any group, those buses may be
+ * reserved for other usages.
+ * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
+ */
+ boolean validateVolumeGroups() {
+ Set<Integer> contextSet = new HashSet<>();
+ Set<Integer> busNumberSet = new HashSet<>();
+ for (CarVolumeGroup group : mVolumeGroups) {
+ // One context should not appear in two groups
+ for (int context : group.getContexts()) {
+ if (contextSet.contains(context)) {
+ Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
+ return false;
+ }
+ contextSet.add(context);
+ }
+
+ // One bus should not appear in two groups
+ for (int busNumber : group.getBusNumbers()) {
+ if (busNumberSet.contains(busNumber)) {
+ Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
+ return false;
+ }
+ busNumberSet.add(busNumber);
+ }
+ }
+
+ // All contexts are assigned
+ if (contextSet.size() != CarAudioDynamicRouting.CONTEXT_NUMBERS.length) {
+ Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
+ Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
+ + Arrays.toString(contextSet.toArray(new Integer[0])));
+ Log.e(CarLog.TAG_AUDIO,
+ "All contexts " + Arrays.toString(CarAudioDynamicRouting.CONTEXT_NUMBERS));
+ return false;
+ }
+
+ return true;
+ }
+
+ void synchronizeCurrentGainIndex() {
+ for (CarVolumeGroup group : mVolumeGroups) {
+ group.setCurrentGainIndex(group.getCurrentGainIndex());
+ }
+ }
+
+ void dump(String indent, PrintWriter writer) {
+ writer.printf("%sCarAudioZone(%s:%d) isPrimary? %b\n", indent, mName, mId, isPrimaryZone());
+ for (CarVolumeGroup group : mVolumeGroups) {
+ group.dump(indent + "\t", writer);
+ }
+ writer.println();
+ }
+}
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelper.java
new file mode 100644
index 0000000..ba7b93c
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioZonesHelper.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2018 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.audio;
+
+import android.annotation.NonNull;
+import android.annotation.XmlRes;
+import android.car.media.CarAudioManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.car.CarLog;
+import com.android.car.R;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class loads all audio zones from the configuration XML file.
+ */
+/* package */ class CarAudioZonesHelper implements CarAudioService.CarAudioZonesLoader {
+
+ private static final String TAG_ROOT = "carAudioConfiguration";
+ private static final String TAG_AUDIO_ZONES = "zones";
+ private static final String TAG_AUDIO_ZONE = "zone";
+ private static final String TAG_VOLUME_GROUPS = "volumeGroups";
+ private static final String TAG_VOLUME_GROUP = "group";
+ private static final String TAG_AUDIO_DEVICE = "device";
+ private static final String TAG_CONTEXT = "context";
+ private static final int SUPPORTED_VERSION = 1;
+
+ private final Context mContext;
+ private final int mXmlConfiguration;
+ private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
+
+ private int mNextSecondaryZoneId;
+
+ CarAudioZonesHelper(Context context, @XmlRes int xmlConfiguration,
+ @NonNull SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
+ mContext = context;
+ mXmlConfiguration = xmlConfiguration;
+ mBusToCarAudioDeviceInfo = busToCarAudioDeviceInfo;
+
+ mNextSecondaryZoneId = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
+ }
+
+ @Override
+ public CarAudioZone[] loadAudioZones() {
+ List<CarAudioZone> carAudioZones = new ArrayList<>();
+ try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ // Traverse to the first start tag, <carAudioConfiguration> in this case
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && type != XmlResourceParser.START_TAG) {
+ // ignored
+ }
+ if (!TAG_ROOT.equals(parser.getName())) {
+ throw new RuntimeException("Meta-data does not start with " + TAG_ROOT);
+ }
+
+ // Version check
+ TypedArray c = mContext.getResources().obtainAttributes(
+ attrs, R.styleable.carAudioConfiguration);
+ final int versionNumber = c.getInt(R.styleable.carAudioConfiguration_version, -1);
+ if (versionNumber != SUPPORTED_VERSION) {
+ throw new RuntimeException("Support version:"
+ + SUPPORTED_VERSION + " only, got version:" + versionNumber);
+ }
+ c.recycle();
+
+ // And follows with the <zones> tag
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && type != XmlResourceParser.START_TAG) {
+ // ignored
+ }
+ if (!TAG_AUDIO_ZONES.equals(parser.getName())) {
+ throw new RuntimeException("Configuration should begin with a <zones> tag");
+ }
+ int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlResourceParser.END_TAG) {
+ continue;
+ }
+ if (TAG_AUDIO_ZONE.equals(parser.getName())) {
+ carAudioZones.add(parseAudioZone(attrs, parser));
+ }
+ }
+ } catch (Exception e) {
+ Log.e(CarLog.TAG_AUDIO, "Error parsing unified car audio configuration", e);
+
+ }
+ return carAudioZones.toArray(new CarAudioZone[0]);
+ }
+
+ private CarAudioZone parseAudioZone(AttributeSet attrs, XmlResourceParser parser)
+ throws XmlPullParserException, IOException {
+ TypedArray c = mContext.getResources().obtainAttributes(
+ attrs, R.styleable.carAudioConfiguration);
+ final boolean isPrimary = c.getBoolean(R.styleable.carAudioConfiguration_isPrimary, false);
+ final String zoneName = c.getString(R.styleable.carAudioConfiguration_name);
+ c.recycle();
+
+ CarAudioZone zone = new CarAudioZone(
+ isPrimary ? CarAudioManager.PRIMARY_AUDIO_ZONE : getNextSecondaryZoneId(),
+ zoneName);
+ int type;
+ // Traverse to the first start tag, <volumeGroups> in this case
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && type != XmlResourceParser.START_TAG) {
+ // ignored
+ }
+
+ if (!TAG_VOLUME_GROUPS.equals(parser.getName())) {
+ throw new RuntimeException("Audio zone does not start with <volumeGroups> tag");
+ }
+ int outerDepth = parser.getDepth();
+ int groupId = 0;
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlResourceParser.END_TAG) {
+ continue;
+ }
+ if (TAG_VOLUME_GROUP.equals(parser.getName())) {
+ zone.addVolumeGroup(parseVolumeGroup(zone.getId(), groupId, attrs, parser));
+ groupId += 1;
+ }
+ }
+ return zone;
+ }
+
+ private CarVolumeGroup parseVolumeGroup(
+ int zoneId, int groupId, AttributeSet attrs, XmlResourceParser parser)
+ throws XmlPullParserException, IOException {
+ final CarVolumeGroup group = new CarVolumeGroup(mContext, zoneId, groupId);
+ int type;
+ int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlResourceParser.END_TAG) {
+ continue;
+ }
+ if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
+ TypedArray c = mContext.getResources().obtainAttributes(
+ attrs, R.styleable.carAudioConfiguration);
+ final String address = c.getString(R.styleable.carAudioConfiguration_address);
+ parseVolumeGroupContexts(group,
+ CarAudioDeviceInfo.parseDeviceAddress(address), attrs, parser);
+ c.recycle();
+ }
+ }
+ return group;
+ }
+
+ private void parseVolumeGroupContexts(
+ CarVolumeGroup group, int busNumber, AttributeSet attrs, XmlResourceParser parser)
+ throws XmlPullParserException, IOException {
+ int type;
+ int innerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+ && (type != XmlResourceParser.END_TAG || parser.getDepth() > innerDepth)) {
+ if (type == XmlResourceParser.END_TAG) {
+ continue;
+ }
+ if (TAG_CONTEXT.equals(parser.getName())) {
+ TypedArray c = mContext.getResources().obtainAttributes(
+ attrs, R.styleable.volumeGroups_context);
+ final int contextNumber = c.getInt(
+ R.styleable.volumeGroups_context_context, -1);
+ c.recycle();
+ group.bind(contextNumber, busNumber, mBusToCarAudioDeviceInfo.get(busNumber));
+ }
+ }
+ }
+
+ private int getNextSecondaryZoneId() {
+ int zoneId = mNextSecondaryZoneId;
+ mNextSecondaryZoneId += 1;
+ return zoneId;
+ }
+}
diff --git a/service/src/com/android/car/audio/CarVolumeGroupsHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
similarity index 63%
rename from service/src/com/android/car/audio/CarVolumeGroupsHelper.java
rename to service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
index eddcd4f..fb5481d 100644
--- a/service/src/com/android/car/audio/CarVolumeGroupsHelper.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
@@ -15,12 +15,18 @@
*/
package com.android.car.audio;
+import android.annotation.NonNull;
import android.annotation.XmlRes;
+import android.car.media.CarAudioManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
+import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
import android.util.Xml;
import com.android.car.CarLog;
@@ -33,9 +39,12 @@
import java.util.List;
/**
- * A helper class loads all volume groups from the configuration XML file.
+ * A helper class loads volume groups from car_volume_groups.xml configuration into one zone.
+ *
+ * @deprecated This is replaced by {@link CarAudioZonesHelper}.
*/
-/* package */ class CarVolumeGroupsHelper {
+@Deprecated
+/* package */ class CarAudioZonesHelperLegacy implements CarAudioService.CarAudioZonesLoader {
private static final String TAG_VOLUME_GROUPS = "volumeGroups";
private static final String TAG_GROUP = "group";
@@ -43,16 +52,47 @@
private final Context mContext;
private final @XmlRes int mXmlConfiguration;
+ private final SparseIntArray mContextToBus;
+ private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
- CarVolumeGroupsHelper(Context context, @XmlRes int xmlConfiguration) {
+ CarAudioZonesHelperLegacy(Context context, @XmlRes int xmlConfiguration,
+ @NonNull SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo,
+ @NonNull IAudioControl audioControl) {
mContext = context;
mXmlConfiguration = xmlConfiguration;
+ mBusToCarAudioDeviceInfo = busToCarAudioDeviceInfo;
+
+ // Initialize context => bus mapping once.
+ mContextToBus = new SparseIntArray();
+ try {
+ for (int contextNumber : CarAudioDynamicRouting.CONTEXT_NUMBERS) {
+ mContextToBus.put(contextNumber, audioControl.getBusForContext(contextNumber));
+ }
+ } catch (RemoteException e) {
+ Log.e(CarLog.TAG_AUDIO, "Failed to query IAudioControl HAL", e);
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public CarAudioZone[] loadAudioZones() {
+ final CarAudioZone zone = new CarAudioZone(CarAudioManager.PRIMARY_AUDIO_ZONE,
+ "Primary zone");
+ for (CarVolumeGroup group : loadVolumeGroups()) {
+ zone.addVolumeGroup(group);
+ // Binding audio device to volume group.
+ for (int contextNumber : group.getContexts()) {
+ int busNumber = mContextToBus.get(contextNumber);
+ group.bind(contextNumber, busNumber, mBusToCarAudioDeviceInfo.get(busNumber));
+ }
+ }
+ return new CarAudioZone[] { zone };
}
/**
* @return all {@link CarVolumeGroup} read from configuration.
*/
- CarVolumeGroup[] loadVolumeGroups() {
+ private List<CarVolumeGroup> loadVolumeGroups() {
List<CarVolumeGroup> carVolumeGroups = new ArrayList<>();
try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) {
AttributeSet attrs = Xml.asAttributeSet(parser);
@@ -81,14 +121,13 @@
} catch (Exception e) {
Log.e(CarLog.TAG_AUDIO, "Error parsing volume groups configuration", e);
}
- return carVolumeGroups.toArray(new CarVolumeGroup[carVolumeGroups.size()]);
+ return carVolumeGroups;
}
private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser)
throws XmlPullParserException, IOException {
- int type;
-
List<Integer> contexts = new ArrayList<>();
+ int type;
int innerDepth = parser.getDepth();
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& (type != XmlResourceParser.END_TAG || parser.getDepth() > innerDepth)) {
@@ -103,7 +142,7 @@
}
}
- return new CarVolumeGroup(mContext, id,
+ return new CarVolumeGroup(mContext, CarAudioManager.PRIMARY_AUDIO_ZONE, id,
contexts.stream().mapToInt(i -> i).filter(i -> i >= 0).toArray());
}
}
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index eff3b4c..eb3cc4f 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -29,7 +29,9 @@
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* A class encapsulates a volume group in car.
@@ -41,10 +43,10 @@
/* package */ final class CarVolumeGroup {
private final ContentResolver mContentResolver;
+ private final int mZoneId;
private final int mId;
- private final int[] mContexts;
private final SparseIntArray mContextToBus = new SparseIntArray();
- private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfos = new SparseArray<>();
+ private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo = new SparseArray<>();
private int mDefaultGain = Integer.MIN_VALUE;
private int mMaxGain = Integer.MIN_VALUE;
@@ -53,30 +55,78 @@
private int mStoredGainIndex;
private int mCurrentGainIndex = -1;
- CarVolumeGroup(Context context, int id, @NonNull int[] contexts) {
+ /**
+ * Constructs a {@link CarVolumeGroup} instance
+ * @param context {@link Context} instance
+ * @param zoneId Audio zone this volume group belongs to
+ * @param id ID of this volume group
+ */
+ CarVolumeGroup(Context context, int zoneId, int id) {
mContentResolver = context.getContentResolver();
+ mZoneId = zoneId;
mId = id;
- mContexts = contexts;
-
mStoredGainIndex = Settings.Global.getInt(mContentResolver,
- CarAudioManager.getVolumeSettingsKeyForGroup(mId), -1);
+ CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), -1);
}
- int getId() {
- return mId;
+ /**
+ * Constructs a {@link CarVolumeGroup} instance
+ * @param context {@link Context} instance
+ * @param zoneId Audio zone this volume group belongs to
+ * @param id ID of this volume group
+ * @param contexts Pre-populated array of car contexts, for legacy car_volume_groups.xml only
+ * @deprecated In favor of {@link #CarVolumeGroup(Context, int, int)}
+ */
+ @Deprecated
+ CarVolumeGroup(Context context, int zoneId, int id, @NonNull int[] contexts) {
+ this(context, zoneId, id);
+ // Deal with the pre-populated car audio contexts
+ for (int audioContext : contexts) {
+ mContextToBus.put(audioContext, -1);
+ }
}
+ /**
+ * @param busNumber Physical bus number for the audio device port
+ * @return {@link CarAudioDeviceInfo} associated with a given bus number
+ */
+ CarAudioDeviceInfo getCarAudioDeviceInfoForBus(int busNumber) {
+ return mBusToCarAudioDeviceInfo.get(busNumber);
+ }
+
+ /**
+ * @return Array of context numbers in this {@link CarVolumeGroup}
+ */
int[] getContexts() {
- return mContexts;
+ final int[] contextNumbers = new int[mContextToBus.size()];
+ for (int i = 0; i < contextNumbers.length; i++) {
+ contextNumbers[i] = mContextToBus.keyAt(i);
+ }
+ return contextNumbers;
+ }
+
+ /**
+ * @param busNumber Physical bus number for the audio device port
+ * @return Array of context numbers assigned to a given bus number
+ */
+ int[] getContextsForBus(int busNumber) {
+ List<Integer> contextNumbers = new ArrayList<>();
+ for (int i = 0; i < mContextToBus.size(); i++) {
+ int value = mContextToBus.valueAt(i);
+ if (value == busNumber) {
+ contextNumbers.add(mContextToBus.keyAt(i));
+ }
+ }
+ return contextNumbers.stream().mapToInt(i -> i).toArray();
}
/**
* @return Array of bus numbers in this {@link CarVolumeGroup}
*/
int[] getBusNumbers() {
- final int[] busNumbers = new int[mBusToCarAudioDeviceInfos.size()];
+ final int[] busNumbers = new int[mBusToCarAudioDeviceInfo.size()];
for (int i = 0; i < busNumbers.length; i++) {
- busNumbers[i] = mBusToCarAudioDeviceInfos.keyAt(i);
+ busNumbers[i] = mBusToCarAudioDeviceInfo.keyAt(i);
}
return busNumbers;
}
@@ -92,7 +142,7 @@
* @param info {@link CarAudioDeviceInfo} instance relates to the physical bus
*/
void bind(int contextNumber, int busNumber, CarAudioDeviceInfo info) {
- if (mBusToCarAudioDeviceInfos.size() == 0) {
+ if (mBusToCarAudioDeviceInfo.size() == 0) {
mStepSize = info.getAudioGain().stepValue();
} else {
Preconditions.checkArgument(
@@ -101,7 +151,7 @@
}
mContextToBus.put(contextNumber, busNumber);
- mBusToCarAudioDeviceInfos.put(busNumber, info);
+ mBusToCarAudioDeviceInfo.put(busNumber, info);
if (info.getDefaultGain() > mDefaultGain) {
// We're arbitrarily selecting the highest bus default gain as the group's default.
@@ -156,14 +206,14 @@
+ gainInMillibels + "index "
+ gainIndex);
- for (int i = 0; i < mBusToCarAudioDeviceInfos.size(); i++) {
- CarAudioDeviceInfo info = mBusToCarAudioDeviceInfos.valueAt(i);
+ for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
+ CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.valueAt(i);
info.setCurrentGain(gainInMillibels);
}
mCurrentGainIndex = gainIndex;
Settings.Global.putInt(mContentResolver,
- CarAudioManager.getVolumeSettingsKeyForGroup(mId), gainIndex);
+ CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), gainIndex);
}
// Given a group level gain index, return the computed gain in millibells
@@ -186,33 +236,35 @@
@Nullable
AudioDevicePort getAudioDevicePortForContext(int contextNumber) {
final int busNumber = mContextToBus.get(contextNumber, -1);
- if (busNumber < 0 || mBusToCarAudioDeviceInfos.get(busNumber) == null) {
+ if (busNumber < 0 || mBusToCarAudioDeviceInfo.get(busNumber) == null) {
return null;
}
- return mBusToCarAudioDeviceInfos.get(busNumber).getAudioDevicePort();
+ return mBusToCarAudioDeviceInfo.get(busNumber).getAudioDevicePort();
}
@Override
public String toString() {
return "CarVolumeGroup id: " + mId
+ " currentGainIndex: " + mCurrentGainIndex
- + " contexts: " + Arrays.toString(mContexts)
+ + " contexts: " + Arrays.toString(getContexts())
+ " buses: " + Arrays.toString(getBusNumbers());
}
/** Writes to dumpsys output */
- void dump(PrintWriter writer) {
- writer.println("CarVolumeGroup " + mId);
- writer.printf("\tGain in millibel (min / max / default/ current): %d %d %d %d\n",
- mMinGain, mMaxGain, mDefaultGain, getGainForIndex(mCurrentGainIndex));
- writer.printf("\tGain in index (min / max / default / current): %d %d %d %d\n",
- getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex);
+ void dump(String indent, PrintWriter writer) {
+ writer.printf("%sCarVolumeGroup(%d)\n", indent, mId);
+ writer.printf("%sGain values (min / max / default/ current): %d %d %d %d\n",
+ indent, mMinGain, mMaxGain,
+ mDefaultGain, getGainForIndex(mCurrentGainIndex));
+ writer.printf("%sGain indexes (min / max / default / current): %d %d %d %d\n",
+ indent, getMinGainIndex(), getMaxGainIndex(),
+ getDefaultGainIndex(), mCurrentGainIndex);
for (int i = 0; i < mContextToBus.size(); i++) {
- writer.printf("\tContext: %s -> Bus: %d\n",
+ writer.printf("%sContext: %s -> Bus: %d\n", indent,
ContextNumber.toString(mContextToBus.keyAt(i)), mContextToBus.valueAt(i));
}
- for (int i = 0; i < mBusToCarAudioDeviceInfos.size(); i++) {
- mBusToCarAudioDeviceInfos.valueAt(i).dump(writer);
+ for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
+ mBusToCarAudioDeviceInfo.valueAt(i).dump(indent, writer);
}
// Empty line for comfortable reading
writer.println();
diff --git a/tests/DirectRenderingClusterSample/AndroidManifest.xml b/tests/DirectRenderingClusterSample/AndroidManifest.xml
index be7026c..a5f117c 100644
--- a/tests/DirectRenderingClusterSample/AndroidManifest.xml
+++ b/tests/DirectRenderingClusterSample/AndroidManifest.xml
@@ -43,14 +43,15 @@
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:directBootAware="true">
- <service android:name=".SampleClusterServiceImpl"
+ <service android:name=".ClusterRenderingServiceImpl"
android:exported="false"
android:singleUser="true"
android:permission="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"/>
<activity android:name=".MainClusterActivity"
android:exported="false"
- android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ android:showForAllUsers="true"
+ android:theme="@style/Theme.ClusterTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.DEFAULT"/>
diff --git a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
index 393db54..2cb9662 100644
--- a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
@@ -60,29 +60,12 @@
android:layout_width="20dp"
android:layout_height="1dp" />
- <ImageView
- android:id="@+id/maneuver"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_margin="10dp"
- android:tint="@android:color/white"/>
+ <include
+ android:id="@+id/navigation_state"
+ layout="@layout/include_navigation_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
- <LinearLayout
- android:layout_width="250dp"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/distance"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:textSize="30sp"/>
- <TextView
- android:id="@+id/segment"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:textSize="18sp"/>
- </LinearLayout>
</LinearLayout>
</LinearLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml b/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml
new file mode 100644
index 0000000..1946f0c
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/maneuver"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_margin="10dp"
+ android:tint="@android:color/white"/>
+
+ <LinearLayout
+ android:layout_width="250dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/distance"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textSize="30sp"/>
+ <TextView
+ android:id="@+id/segment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textSize="18sp"/>
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/values/styles.xml b/tests/DirectRenderingClusterSample/res/values/styles.xml
index f11f745..cd5f552 100644
--- a/tests/DirectRenderingClusterSample/res/values/styles.xml
+++ b/tests/DirectRenderingClusterSample/res/values/styles.xml
@@ -1,3 +1,5 @@
<resources>
-
+ <style name="noAnimTheme" parent="android:Theme">
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
</resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/themes.xml b/tests/DirectRenderingClusterSample/res/values/themes.xml
new file mode 100644
index 0000000..972a036
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/themes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+-->
+<resources>
+ <style name="Theme.ClusterTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
similarity index 94%
rename from tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
rename to tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index f8a5b06..66d2d58 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -69,7 +69,7 @@
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
* virtual display that is transmitted to an external screen.
*/
-public class SampleClusterServiceImpl extends InstrumentClusterRenderingService {
+public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService {
private static final String TAG = "Cluster.SampleService";
private static final int NO_DISPLAY = -1;
@@ -103,7 +103,7 @@
public void onDisplayAdded(int displayId) {
Log.i(TAG, "Cluster display found, displayId: " + displayId);
mDisplayId = displayId;
- tryLaunchActivity();
+ launchMainActivity();
}
@Override
@@ -118,9 +118,9 @@
};
private static class UserReceiver extends BroadcastReceiver {
- private WeakReference<SampleClusterServiceImpl> mService;
+ private WeakReference<ClusterRenderingServiceImpl> mService;
- UserReceiver(SampleClusterServiceImpl service) {
+ UserReceiver(ClusterRenderingServiceImpl service) {
mService = new WeakReference<>(service);
}
@@ -136,16 +136,16 @@
@Override
public void onReceive(Context context, Intent intent) {
- SampleClusterServiceImpl service = mService.get();
+ ClusterRenderingServiceImpl service = mService.get();
Log.d(TAG, "Broadcast received: " + intent);
- service.tryLaunchActivity();
+ service.tryLaunchNavigationActivity();
}
}
private static class MessageHandler extends Handler {
- private final WeakReference<SampleClusterServiceImpl> mService;
+ private final WeakReference<ClusterRenderingServiceImpl> mService;
- MessageHandler(SampleClusterServiceImpl service) {
+ MessageHandler(ClusterRenderingServiceImpl service) {
mService = new WeakReference<>(service);
}
@@ -204,21 +204,13 @@
mUserReceiver.register(this);
}
- private void tryLaunchActivity() {
- int userHandle = ActivityManager.getCurrentUser();
- if (userHandle == UserHandle.USER_SYSTEM || mDisplayId == NO_DISPLAY) {
- Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)", userHandle,
- mDisplayId));
- // Not ready to launch yet.
- return;
- }
+ private void launchMainActivity() {
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(mDisplayId);
Intent intent = new Intent(this, MainClusterActivity.class);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
- startActivityAsUser(intent, options.toBundle(), UserHandle.of(userHandle));
- Log.i(TAG, String.format("launching main activity: %s (user: %d, display: %d)", intent,
- userHandle, mDisplayId));
+ startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM);
+ Log.i(TAG, String.format("launching main activity: %s (display: %d)", intent, mDisplayId));
}
@Override
@@ -364,7 +356,7 @@
}
case "destroyOverlayDisplay": {
Settings.Global.putString(getContentResolver(),
- Global.OVERLAY_DISPLAY_DEVICES, "");
+ Global.OVERLAY_DISPLAY_DEVICES, "");
break;
}
@@ -407,6 +399,15 @@
* have a default navigation activity selected yet.
*/
private void tryLaunchNavigationActivity() {
+ int userHandle = ActivityManager.getCurrentUser();
+ if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
+ userHandle, mNavigationDisplayId));
+ }
+ // Not ready to launch yet.
+ return;
+ }
mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
Intent intent = getNavigationActivityIntent();
@@ -466,8 +467,7 @@
intent.setPackage(navigationApp.activityInfo.packageName);
intent.setComponent(new ComponentName(candidate.activityInfo.packageName,
candidate.activityInfo.name));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 2e38054..7464552 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -16,17 +16,20 @@
package android.car.cluster.sample;
import static android.car.cluster.CarInstrumentClusterManager.CATEGORY_NAVIGATION;
-import static android.car.cluster.sample.SampleClusterServiceImpl.LOCAL_BINDING_ACTION;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_ACTIVITY_STATE;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_ON_KEY_EVENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_ON_NAVIGATION_STATE_CHANGED;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_REGISTER_CLIENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_UNREGISTER_CLIENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.LOCAL_BINDING_ACTION;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_NAVIGATION_STATE_CHANGED;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_REGISTER_CLIENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
+import android.car.Car;
+import android.car.CarAppFocusManager;
+import android.car.CarNotConnectedException;
import android.car.cluster.ClusterActivityState;
import android.content.ComponentName;
import android.content.Intent;
@@ -74,7 +77,8 @@
private Messenger mService;
private Messenger mServiceCallbacks = new Messenger(new MessageHandler(this));
private VirtualDisplay mPendingVirtualDisplay = null;
- private final Handler mHandler = new Handler();
+ private Car mCar;
+ private CarAppFocusManager mCarAppFocusManager;
public static class VirtualDisplay {
public final int mDisplayId;
@@ -96,7 +100,7 @@
}
};
- private ServiceConnection mServiceConnection = new ServiceConnection() {
+ private ServiceConnection mClusterRenderingServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
@@ -117,6 +121,32 @@
}
};
+ private ServiceConnection mCarServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ try {
+ Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
+ mCarAppFocusManager = (CarAppFocusManager) mCar.getCarManager(
+ Car.APP_FOCUS_SERVICE);
+ if (mCarAppFocusManager == null) {
+ Log.e(TAG, "onServiceConnected: unable to obtain CarAppFocusManager");
+ return;
+ }
+ mCarAppFocusManager.addFocusListener((appType, active) -> {
+ onNavigationFocusChanged(active);
+ }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "onServiceConnected: error obtaining manager", e);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.i(TAG, "onServiceDisconnected, name: " + name);
+ mCarAppFocusManager = null;
+ }
+ };
+
private static class MessageHandler extends Handler {
private final WeakReference<MainClusterActivity> mActivity;
@@ -138,7 +168,7 @@
data.setClassLoader(ParcelUtils.class.getClassLoader());
NavigationState navState = NavigationState
.fromParcelable(data.getParcelable(
- SampleClusterServiceImpl.NAV_STATE_BUNDLE_KEY));
+ ClusterRenderingServiceImpl.NAV_STATE_BUNDLE_KEY));
mActivity.get().onNavigationStateChange(navState);
}
break;
@@ -156,9 +186,9 @@
mInputMethodManager = getSystemService(InputMethodManager.class);
- Intent intent = new Intent(this, SampleClusterServiceImpl.class);
+ Intent intent = new Intent(this, ClusterRenderingServiceImpl.class);
intent.setAction(LOCAL_BINDING_ACTION);
- bindService(intent, mServiceConnection, 0);
+ bindService(intent, mClusterRenderingServiceConnection, 0);
registerFacets(
new Facet<>(findViewById(R.id.btn_nav), 0, NavigationFragment.class),
@@ -169,19 +199,23 @@
mPager = findViewById(R.id.pager);
mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
mOrderToFacet.get(0).button.requestFocus();
- mNavStateController = new NavStateController(findViewById(R.id.maneuver),
- findViewById(R.id.distance), findViewById(R.id.segment));
+ mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
+
+ mCar = Car.createCar(this, mCarServiceConnection);
+ mCar.connect();
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
+ mCar.disconnect();
+ mCarAppFocusManager = null;
if (mService != null) {
sendServiceMessage(MSG_UNREGISTER_CLIENT, null, mServiceCallbacks);
mService = null;
}
- unbindService(mServiceConnection);
+ unbindService(mClusterRenderingServiceConnection);
}
private void onKeyEvent(KeyEvent event) {
@@ -201,6 +235,12 @@
}
}
+ private void onNavigationFocusChanged(boolean active) {
+ if (mNavStateController != null) {
+ mNavStateController.setActive(active);
+ }
+ }
+
public void updateNavDisplay(VirtualDisplay virtualDisplay) {
if (mService == null) {
// Service is not bound yet. Hold the information and notify when the service is bound.
@@ -223,8 +263,8 @@
}
/**
- * Sends a message to the {@link SampleClusterServiceImpl}, which runs on a different process.
-
+ * Sends a message to the {@link ClusterRenderingServiceImpl}, which runs on a different
+ * process.
* @param what action to perform
* @param data action data
* @param replyTo {@link Messenger} where to reply back
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index b45a806..44b6268 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
+import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
@@ -36,21 +37,21 @@
private ImageView mManeuver;
private TextView mDistance;
private TextView mSegment;
+ private View mNavigationState;
private Context mContext;
/**
* Creates a controller to coordinate updates to the views displaying navigation state
* data.
*
- * @param maneuver {@link ImageView} used to display the immediate navigation maneuver
- * @param distance {@link TextView} displaying distance to the maneuver
- * @param segment {@link TextView} displaying the current street.
+ * @param container {@link View} containing the navigation state views
*/
- public NavStateController(ImageView maneuver, TextView distance, TextView segment) {
- mManeuver = maneuver;
- mDistance = distance;
- mSegment = segment;
- mContext = maneuver.getContext();
+ public NavStateController(View container) {
+ mNavigationState = container;
+ mManeuver = container.findViewById(R.id.maneuver);
+ mDistance = container.findViewById(R.id.distance);
+ mSegment = container.findViewById(R.id.segment);
+ mContext = container.getContext();
}
/**
@@ -63,6 +64,18 @@
mDistance.setText(formatDistance(step != null ? step.getDistance() : null));
}
+ /**
+ * Updates whether turn-by-turn display is active or not. Turn-by-turn would be active whenever
+ * a navigation application has focus.
+ */
+ public void setActive(boolean active) {
+ Log.i(TAG, "Navigation status active: " + active);
+ if (!active) {
+ mManeuver.setImageDrawable(null);
+ mDistance.setText(null);
+ }
+ }
+
private Drawable getManeuverIcon(@Nullable Maneuver maneuver) {
if (maneuver == null) {
return null;
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
index a9c76a3..f5f535e 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
@@ -68,7 +68,7 @@
private static final int MSG_START = 0;
private static final int MSG_STOP = 1;
- private static final int MSG_RESUBMIT_FRAME = 2;
+ private static final int MSG_SEND_FRAME = 2;
private VirtualDisplay mVirtualDisplay;
private MediaCodec mVideoEncoder;
@@ -209,7 +209,7 @@
}
private void doOutputBufferAvailable(int index, @NonNull BufferInfo info) {
- mHandler.removeMessages(MSG_RESUBMIT_FRAME);
+ mHandler.removeMessages(MSG_SEND_FRAME);
ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index);
if (encodedData == null) {
@@ -227,20 +227,17 @@
encodedData.get(mBuffer, 0, mLastFrameLength);
mVideoEncoder.releaseOutputBuffer(index, false);
- sendFrame(mBuffer, mLastFrameLength);
-
- // If nothing happens in Virtual Display we won't receive new frames. If we won't keep
- // sending frames it could be a problem for the receiver because it needs certain
- // number of frames in order to start decoding.
- scheduleResendingLastFrame(1000 / FPS);
+ // Send this frame asynchronously (avoiding blocking on the socket). We might miss
+ // frames if the consumer is not fast enough, but this is acceptable.
+ sendFrameAsync(0);
} else {
Log.e(TAG, "Skipping empty buffer");
mVideoEncoder.releaseOutputBuffer(index, false);
}
}
- private void scheduleResendingLastFrame(long delayMs) {
- Message msg = mHandler.obtainMessage(MSG_RESUBMIT_FRAME);
+ private void sendFrameAsync(long delayMs) {
+ Message msg = mHandler.obtainMessage(MSG_SEND_FRAME);
mHandler.sendMessageDelayed(msg, delayMs);
}
@@ -324,12 +321,12 @@
stopCasting();
break;
- case MSG_RESUBMIT_FRAME:
+ case MSG_SEND_FRAME:
if (mServerSocket != null && mOutputStream != null) {
sendFrame(mBuffer, mLastFrameLength);
}
// We will keep sending last frame every second as a heartbeat.
- scheduleResendingLastFrame(1000L);
+ sendFrameAsync(1000L);
break;
}
}
@@ -356,6 +353,9 @@
try {
Log.i(TAG, "Listening for incoming connections on port: " + PORT);
Socket socket = serverSocket.accept();
+ socket.setTcpNoDelay(true);
+ socket.setKeepAlive(true);
+ socket.setSoLinger(true, 0);
Log.i(TAG, "Receiver connected: " + socket);
listenReceiverDisconnected(socket.getInputStream());
diff --git a/tests/EmbeddedKitchenSinkApp/Android.mk b/tests/EmbeddedKitchenSinkApp/Android.mk
index e6f457e..db59ddf 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.mk
+++ b/tests/EmbeddedKitchenSinkApp/Android.mk
@@ -43,7 +43,6 @@
LOCAL_STATIC_ANDROID_LIBRARIES += \
car-service-lib-for-test \
car-apps-common \
- androidx.car_car \
androidx.car_car-cluster
LOCAL_STATIC_JAVA_LIBRARIES += \
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
index d64d4fc..0ef8e43 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
@@ -22,5 +22,5 @@
android:viewportHeight="24.0">
<path
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
- android:fillColor="#000000"/>
+ android:fillColor="#FFFFFF"/>
</vector>
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
index 96246a3..b05cb48 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
@@ -22,5 +22,5 @@
android:viewportHeight="24.0">
<path
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
- android:fillColor="#000000"/>
+ android:fillColor="#FFFFFF"/>
</vector>
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
index 0468870..2171286 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
@@ -21,7 +21,7 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="#000000"
+ android:fillColor="#FFFFFF"
android:pathData="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3
3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6
6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" />
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
index 15422b1..cdf437e 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
@@ -19,37 +19,68 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <!-- List(s) of networks (right now only have view all, may do a search UI)
+ -->
<LinearLayout
- android:orientation="horizontal"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- Header for table -->
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="4dp"
+ android:background="#3C3F41"
+ android:weightSum="12">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="32sp"
+ android:textColor="#d4d4d4"
+ android:textStyle="bold"
+ android:text="Net ID">
+ </TextView>
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="9"
+ android:textSize="32sp"
+ android:textColor="#d4d4d4"
+ android:textStyle="bold"
+ android:text="Details">
+ </TextView>
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:textSize="32sp"
+ android:textColor="#d4d4d4"
+ android:textStyle="bold"
+ android:text="Functions">
+ </TextView>
+
+ </LinearLayout>
+
+ <!-- Wrapped list view to implement swipe to refresh -->
+ <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+ android:id="@+id/refreshNetworksList"
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <ListView
- android:id="@+id/networks"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- </ListView>
- </LinearLayout>
- <LinearLayout
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="4dp">
- <Button android:id="@+id/networksRefresh"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Refresh"/>
- <Button android:id="@+id/networkRequestOemPaid"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Request OEM-paid"/>
- <Button android:id="@+id/networkRequestEth1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Request eth1"/>
- <Button android:id="@+id/networkReleaseNetwork"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Release Request"/>
- </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+ <!-- Filled in code with network_item.xml -->
+ <ListView
+ android:id="@+id/networks"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ </ListView>
+
+ </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
index 45b38dc..2ebc1c6 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
@@ -17,7 +17,6 @@
<!-- We use this container to place kitchen app fragments. It insets the fragment contents -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/kitchen_content"
- android:background="#A8A9AA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="56dp"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml b/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml
new file mode 100644
index 0000000..84d9d34
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2018 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.
+-->
+
+<!-- Used for the list of networks in the KS Connectivity Tab -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="fill_vertical"
+ android:orientation="horizontal"
+ android:weightSum="12">
+
+ <!-- Requested Active indicator -->
+ <View
+ android:id="@+id/network_active"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".1"
+ android:background="#ff3d3d">
+ </View>
+
+ <!-- Item NetId indicator -->
+ <TextView
+ android:id="@+id/network_id"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".9"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <!-- Set of network values we want to view -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_weight="9">
+
+ <TextView
+ android:id="@+id/network_type"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_connected"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_available"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_roaming"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_iface"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/hw_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_ip_addresses"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_dns"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_domains"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_routes"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_transports"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_capabilities"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ <TextView
+ android:id="@+id/network_bandwidth"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:textColor="#d4d4d4">
+ </TextView>
+
+ </LinearLayout>
+
+ <!-- Buttons to trigger a request or something else -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:weightSum="3">
+
+ <Button android:id="@+id/network_request"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginBottom="2dp"
+ android:background="#505050"
+ android:text="Request"
+ android:textColor="#d4d4d4"/>
+
+ <Button android:id="@+id/network_default"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginBottom="2dp"
+ android:background="#505050"
+ android:text="Set Default"
+ android:textColor="#d4d4d4"/>
+
+ <Button android:id="@+id/network_report"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginBottom="2dp"
+ android:background="#505050"
+ android:text="Report"
+ android:textColor="#d4d4d4"/>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
index b9b8e8b..1dc40f6 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
@@ -65,4 +65,21 @@
android:layout_height="wrap_content"
android:text="Category: CATEGORY_MESSAGE"
android:textSize="35sp"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/category_car_emerg_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Category: CATEGORY_EMERG"
+ android:textSize="35sp"/>
+ <Button
+ android:id="@+id/category_car_warning_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Category: CATEGORY_WARN"
+ android:textSize="35sp"/>
+ </LinearLayout>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
index 4d8c246..af1866a 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
@@ -23,7 +23,7 @@
android:layout_height="110dp"
android:orientation="vertical">
<TextView
- android:id="@string/location_title"
+ android:id="@+id/location_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/location_title" />
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
index cf98695..52a7763 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
@@ -23,7 +23,6 @@
android:layout_height="wrap_content"
android:text="Storage Wear Information"
android:textSize="24dp"
- android:textColor="#ff0000"
android:minLines="5"/>
<ListView
android:id="@+id/storage_events_list"
@@ -58,7 +57,6 @@
android:layout_height="wrap_content"
android:text="Free Disk Space: 1 byte"
android:textSize="24dp"
- android:textColor="#ff0000"
android:minLines="4"/>
<ScrollView
android:id="@+id/scroll_view"
@@ -72,7 +70,6 @@
android:layout_height="fill_parent"
android:text="No I/O activity on record"
android:textSize="20dp"
- android:textColor="#ff0000"
android:minLines="10"/>
</ScrollView>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml
new file mode 100644
index 0000000..6200ddc
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="40dp"
+ android:layout_marginEnd="40dp"
+ android:id="@+id/buttons">
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_marginTop="40dp"
+ android:text="@string/weblink_google"
+ android:tag="@string/weblink_google" />
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_marginTop="40dp"
+ android:text="@string/weblink_nytimes"
+ android:tag="@string/weblink_nytimes" />
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_marginTop="40dp"
+ android:text="@string/weblink_support_name"
+ android:tag="@string/weblink_support" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index e8ac7d3..35d3ec2 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -293,4 +293,9 @@
<!-- Virtual Display -->
<string name="av_start_activity">Start Activity</string>
<string name="av_resize">Resize</string>
+
+ <string name="weblink_google" translatable="false">www.google.com</string>
+ <string name="weblink_nytimes" translatable="false">www.nytimes.com</string>
+ <string name="weblink_support_name" translatable="false">support.google.com</string>
+ <string name="weblink_support" translatable="false">https://support.google.com/chrome/answer/95414?hl=en&ref_topic=7438008</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/styles.xml b/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
index f16d19d..1dcaa55 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
@@ -30,13 +30,10 @@
<item name="android:layout_width">@dimen/overview_icon_size</item>
<item name="android:layout_height">@dimen/overview_icon_size</item>
<item name="android:padding">6dp</item>
- <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
- <item name="android:tint">@color/car_button_tint</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:clickable">true</item>
</style>
- <style name="KitchenSinkActivityTheme" parent="Theme.Car.Light.NoActionBar.Drawer">
- <item name="android:colorPrimary">@android:color/transparent</item>
+ <style name="KitchenSinkActivityTheme" parent="Theme.NoActionBar.Drawer">
</style>
</resources>
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 3b8b435..acc651d 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -33,11 +33,12 @@
import android.os.IBinder;
import android.util.Log;
-import androidx.car.drawer.CarDrawerActivity;
import androidx.car.drawer.CarDrawerAdapter;
import androidx.car.drawer.DrawerItemViewHolder;
import androidx.fragment.app.Fragment;
+import com.android.car.apps.common.DrawerActivity;
+
import com.google.android.car.kitchensink.activityview.ActivityViewTestFragment;
import com.google.android.car.kitchensink.alertdialog.AlertDialogTestFragment;
import com.google.android.car.kitchensink.assistant.CarAssistantFragment;
@@ -61,11 +62,13 @@
import com.google.android.car.kitchensink.touch.TouchTestFragment;
import com.google.android.car.kitchensink.vhal.VehicleHalFragment;
import com.google.android.car.kitchensink.volume.VolumeTestFragment;
+import com.google.android.car.kitchensink.weblinks.WebLinksTestFragment;
import java.util.ArrayList;
import java.util.List;
-public class KitchenSinkActivity extends CarDrawerActivity {
+
+public class KitchenSinkActivity extends DrawerActivity {
private static final String TAG = "KitchenSinkActivity";
private interface ClickHandler {
@@ -170,6 +173,7 @@
});
add("activity view", ActivityViewTestFragment.class);
add("connectivity", ConnectivityFragment.class);
+ add("web links", WebLinksTestFragment.class);
add("quit", KitchenSinkActivity.this::finish);
}
@@ -207,14 +211,14 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setToolbarElevation(0f);
- setMainContent(R.layout.kitchen_content);
- getDrawerController().setRootAdapter(new DrawerAdapter());
+ setContentView(R.layout.kitchen_content);
+
// Connection to Car Service does not work for non-automotive yet.
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
initCarApi();
}
Log.i(TAG, "onCreate");
+ getDrawerController().setRootAdapter(new DrawerAdapter());
}
private void initCarApi() {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
index 74345aa..c13b0be 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
@@ -50,27 +50,37 @@
@Override
public void onAudioFocusChange(int focusChange) {
- Log.i(TAG, "audio focus change " + focusChange);
if (mPlayer == null) {
+ Log.e(TAG, "mPlayer is null");
return;
}
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ Log.i(TAG, "Audio focus change AUDIOFOCUS_GAIN for usage " + mAttrib.getUsage());
mPlayer.setVolume(1.0f, 1.0f);
if (mRepeat && isPlaying()) {
- doResume();
+ // Resume
+ Log.i(TAG, "resuming player");
+ mPlayer.start();
}
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
- if (isPlaying()) {
- // Duck to 20% volume (which matches system ducking as of this date)
- mPlayer.setVolume(0.2f, 0.2f);
- }
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT && mRepeat) {
- if (isPlaying()) {
- doPause();
+ // While we used to setVolume on the player to 20%, we don't do this anymore
+ // because we expect the car's audio hal do handle ducking as it sees fit.
+ Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> do nothing");
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+ Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT for usage "
+ + mAttrib.getUsage());
+ if (mRepeat && isPlaying()) {
+ Log.i(TAG, "pausing repeating player");
+ mPlayer.pause();
+ } else {
+ Log.i(TAG, "stopping one shot player");
+ stop();
}
} else {
+ Log.e(TAG, "Unrecognized audio focus change " + focusChange);
if (isPlaying()) {
- doStop();
+ Log.i(TAG, "stopping player");
+ stop();
}
}
}
@@ -102,13 +112,18 @@
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
int ret = AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
if (mHandleFocus) {
+ // NOTE: We are CONSCIOUSLY asking for focus again even if already playing in order
+ // exercise the framework's focus logic when faced with a (sloppy) application which
+ // might do this.
+ Log.i(TAG, "Asking for focus for usage " + mAttrib.getUsage());
ret = mAudioManager.requestAudioFocus(mFocusListener, mAttrib,
focusRequest, 0);
}
if (ret == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ Log.i(TAG, "MediaPlayer got focus for usage " + mAttrib.getUsage());
doStart();
} else {
- Log.i(TAG, "no focus");
+ Log.i(TAG, "MediaPlayer denied focus for usage " + mAttrib.getUsage());
}
}
@@ -144,10 +159,10 @@
mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
+ Log.i(TAG, "AudioPlayer onCompletion");
mPlaying.set(false);
if (!mRepeat && mHandleFocus) {
mPlayer.stop();
- mPlayer.release();
mPlayer = null;
mAudioManager.abandonAudioFocus(mFocusListener);
if (mListener != null) {
@@ -163,7 +178,7 @@
AssetFileDescriptor afd =
mContext.getResources().openRawResourceFd(mResourceId);
if (afd == null) {
- throw new RuntimeException("no res");
+ throw new RuntimeException("resource not found");
}
mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
afd.getLength());
@@ -176,35 +191,18 @@
}
public void stop() {
- doStop();
- if (mHandleFocus) {
- mAudioManager.abandonAudioFocus(mFocusListener);
- }
- }
-
- public void release() {
- if (isPlaying()) {
- stop();
- }
- }
-
- private void doStop() {
if (!mPlaying.getAndSet(false)) {
Log.i(TAG, "already stopped");
return;
}
- Log.i(TAG, "doStop audio");
+ Log.i(TAG, "stop");
+
mPlayer.stop();
- mPlayer.release();
mPlayer = null;
- }
- private void doPause() {
- mPlayer.pause();
- }
-
- private void doResume() {
- mPlayer.start();
+ if (mHandleFocus) {
+ mAudioManager.abandonAudioFocus(mFocusListener);
+ }
}
public boolean isPlaying() {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
index 6325512..54c77a0 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
@@ -132,7 +132,7 @@
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.build();
mVrAudioAttrib = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+ .setUsage(AudioAttributes.USAGE_ASSISTANT)
.build();
mRadioAudioAttrib = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
@@ -147,7 +147,6 @@
mMusicAudioAttrib);
mNavGuidancePlayer = new AudioPlayer(mContext, R.raw.turnright,
mNavAudioAttrib);
- // no Usage for voice command yet.
mVrPlayer = new AudioPlayer(mContext, R.raw.one2six,
mVrAudioAttrib);
mSystemPlayer = new AudioPlayer(mContext, R.raw.ring_classic_01,
@@ -197,6 +196,7 @@
view.findViewById(R.id.button_wav_play_stop).setOnClickListener(v -> mWavPlayer.stop());
view.findViewById(R.id.button_nav_play_once).setOnClickListener(v -> {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
@@ -217,6 +217,7 @@
});
view.findViewById(R.id.button_vr_play_once).setOnClickListener(v -> {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
@@ -300,6 +301,7 @@
private void handleNavStart() {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
@@ -317,6 +319,7 @@
private void handleNavEnd() {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
@@ -329,6 +332,7 @@
private void handleVrStart() {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
@@ -346,6 +350,7 @@
private void handleVrEnd() {
if (mAppFocusManager == null) {
+ Log.e(TAG, "mAppFocusManager is null");
return;
}
if (DBG) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
index 3c04bcb..29c1d4c 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
@@ -18,8 +18,10 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
+import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
@@ -27,49 +29,396 @@
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
+import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
import android.widget.ListView;
+import android.widget.TextView;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.car.kitchensink.R;
-import java.util.ArrayList;
+import java.net.NetworkInterface;
+import java.net.SocketException;
@SuppressLint("SetTextI18n")
public class ConnectivityFragment extends Fragment {
private static final String TAG = ConnectivityFragment.class.getSimpleName();
-
private final Handler mHandler = new Handler();
- private final ArrayList<String> mNetworks = new ArrayList<>();
private ConnectivityManager mConnectivityManager;
- private ArrayAdapter<String> mNetworksAdapter;
- private final NetworkCallback mNetworkCallback = new NetworkCallback() {
- @Override
- public void onAvailable(Network network) {
- showToast("onAvailable, netId: " + network);
- refreshNetworks();
+ // Sort out current Network objects (NetId -> Network)
+ private SparseArray<Network> mNetworks = new SparseArray<Network>();
+
+ /**
+ * Create our own network callback object to use with NetworkRequests. Contains a reference to
+ * a Network so we can be sure to only surface updates on the network we want to see them on.
+ * We have to do this because there isn't a way to say "give me this SPECIFIC network." There's
+ * only "give me A network with these capabilities/transports."
+ */
+ public class NetworkByIdCallback extends NetworkCallback {
+ private final Network mNetwork;
+
+ NetworkByIdCallback(Network n) {
+ mNetwork = n;
}
@Override
- public void onLost(Network network) {
- showToast("onLost, netId: " + network);
- refreshNetworks();
+ public void onAvailable(Network n) {
+ if (mNetwork.equals(n)) {
+ showToast("onAvailable(), netId: " + n);
+ }
}
- };
+
+ @Override
+ public void onLosing(Network n, int maxMsToLive) {
+ if (mNetwork.equals(n)) {
+ showToast("onLosing(), netId: " + n);
+ }
+ }
+
+ @Override
+ public void onLost(Network n) {
+ if (mNetwork.equals(n)) {
+ showToast("onLost(), netId: " + n);
+ }
+ }
+ }
+
+ // Map of NetId -> NetworkByIdCallback Objects -- Used to release requested networks
+ SparseArray<NetworkByIdCallback> mNetworkCallbacks = new SparseArray<NetworkByIdCallback>();
+
+ /**
+ * Implement a swipe-to-refresh list of available networks. NetworkListAdapter takes an array
+ * of NetworkItems that it cascades to the view. SwipeRefreshLayout wraps the adapter.
+ */
+ public static class NetworkItem {
+ public int mNetId;
+ public String mType;
+ public String mState;
+ public String mConnected;
+ public String mAvailable;
+ public String mRoaming;
+ public String mInterfaceName;
+ public String mHwAddress;
+ public String mIpAddresses;
+ public String mDnsAddresses;
+ public String mDomains;
+ public String mRoutes;
+ public String mTransports;
+ public String mCapabilities;
+ public String mBandwidth;
+ public boolean mDefault;
+ public boolean mRequested;
+ }
+
+ private NetworkItem[] mNetworkItems = new NetworkItem[0];
+ private NetworkListAdapter mNetworksAdapter;
+ private SwipeRefreshLayout mNetworkListRefresher;
+
+ /**
+ * Builds a NetworkRequest fit to a given network in the hope that we just get updates on that
+ * one network. This is the best way to get single network updates right now, as the request
+ * system works only on transport and capability requirements. There aaaare "network
+ * specifiers" but those only work based on the transport (i.e "eth0" would ask type ETHERNET
+ * for the correct interface where as "GoogleGuest" might ask type WIFI for the Network on SSID
+ * "GoogleGuest"). Ends up being paired with the custom callback above to only surface events
+ * for the specific network in question as well.
+ */
+ private NetworkRequest getRequestForNetwork(Network n) {
+ NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(n);
+
+ NetworkRequest.Builder b = new NetworkRequest.Builder();
+ b.clearCapabilities();
+
+ for (int transportType : nc.getTransportTypes()) {
+ b.addTransportType(transportType);
+ }
+
+ for (int capability : nc.getCapabilities()) {
+ // Not all capabilities are requestable. According to source, all mutable capabilities
+ // except trusted are not requestable. Trying to request them results in an error being
+ // thrown
+ if (isRequestableCapability(capability)) {
+ b.addCapability(capability);
+ }
+ }
+
+ return b.build();
+ }
+
+ private boolean isRequestableCapability(int c) {
+ if (c == NetworkCapabilities.NET_CAPABILITY_VALIDATED
+ || c == NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+ || c == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+ || c == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+ || c == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+ || c == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) {
+ return false;
+ }
+ return true;
+ }
+
+ public void requestNetworkById(int netId) {
+ if (mNetworkCallbacks.get(netId) != null) {
+ return;
+ }
+
+ Network network = mNetworks.get(netId);
+ if (network == null) {
+ return;
+ }
+
+ NetworkRequest request = getRequestForNetwork(network);
+ NetworkByIdCallback cb = new NetworkByIdCallback(network);
+ mNetworkCallbacks.put(netId, cb);
+ mConnectivityManager.requestNetwork(request, cb);
+ showToast("Requesting Network " + netId);
+ }
+
+ public void releaseNetworkById(int netId) {
+ NetworkByIdCallback cb = mNetworkCallbacks.get(netId);
+ if (cb != null) {
+ mConnectivityManager.unregisterNetworkCallback(cb);
+ mNetworkCallbacks.remove(netId);
+ showToast("Released Network " + netId);
+ }
+ }
+
+ public void releaseAllNetworks() {
+ for (NetworkItem n : mNetworkItems) {
+ releaseNetworkById(n.mNetId);
+ }
+ }
+
+ public void bindToNetwork(int netId) {
+ Network network = mNetworks.get(netId);
+ if (network == null) {
+ return;
+ }
+
+ Network def = mConnectivityManager.getBoundNetworkForProcess();
+ if (def != null && def.netId != netId) {
+ clearBoundNetwork();
+ }
+ mConnectivityManager.bindProcessToNetwork(network);
+ showToast("Set process default network " + netId);
+ }
+
+ public void clearBoundNetwork() {
+ mConnectivityManager.bindProcessToNetwork(null);
+ showToast("Clear process default network");
+ }
+
+ public void reportNetworkbyId(int netId) {
+ Network network = mNetworks.get(netId);
+ if (network == null) {
+ return;
+ }
+ mConnectivityManager.reportNetworkConnectivity(network, false);
+ showToast("Reporting Network " + netId);
+ }
+
+ /**
+ * Maps of NET_CAPABILITY_* and TRANSPORT_* to string representations. A network having these
+ * capabilities will have the following strings print on their list entry.
+ */
+ private static final SparseArray<String> sTransportNames = new SparseArray<String>();
+ private static final SparseArray<String> sCapabilityNames = new SparseArray<String>();
+ static {
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_LOWPAN, "[LOWPAN]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE, "[WIFI-AWARE]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_VPN, "[VPN]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_ETHERNET, "[ETHERNET]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_BLUETOOTH, "[BLUETOOTH]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI, "[WIFI]");
+ sTransportNames.put(NetworkCapabilities.TRANSPORT_CELLULAR, "[CELLULAR]");
+
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL, "[CAPTIVE PORTAL]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CBS, "[CBS]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_DUN, "[DUN]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_EIMS, "[EIMS]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOREGROUND, "[FOREGROUND]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOTA, "[FOTA]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IA, "[IA]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IMS, "[IMS]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_INTERNET, "[INTERNET]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_MMS, "[MMS]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED, "[NOT CONGESTED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, "[NOT METERED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED, "[NOT RESTRICTED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, "[NOT ROAMING]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED, "[NOT SUSPENDED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_VPN, "[NOT VPN]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_RCS, "[RCS]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_SUPL, "[SUPL]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_TRUSTED, "[TRUSTED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_VALIDATED, "[VALIDATED]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P, "[WIFI P2P]");
+ sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_XCAP, "[XCAP]");
+ }
+
+ /**
+ * Builds a string out of the possible transports that can be applied to a
+ * NetworkCapabilities object.
+ */
+ private String getTransportString(NetworkCapabilities nCaps) {
+ String transports = "";
+ for (int transport : nCaps.getTransportTypes()) {
+ transports += sTransportNames.get(transport, "");
+ }
+ return transports;
+ }
+
+ /**
+ * Builds a string out of the possible capabilities that can be applied to
+ * a NetworkCapabilities object.
+ */
+ private String getCapabilitiesString(NetworkCapabilities nCaps) {
+ String caps = "";
+ for (int capability : nCaps.getCapabilities()) {
+ caps += sCapabilityNames.get(capability, "");
+ }
+ return caps;
+ }
+
+ // Gets the string representation of a MAC address from a given NetworkInterface object
+ private String getMacAddress(NetworkInterface ni) {
+ if (ni == null) {
+ return "??:??:??:??:??:??";
+ }
+
+ byte[] mac = null;
+ try {
+ mac = ni.getHardwareAddress();
+ } catch (SocketException exception) {
+ Log.e(TAG, "SocketException -- Failed to get interface MAC address");
+ return "??:??:??:??:??:??";
+ }
+
+ if (mac == null) {
+ return "??:??:??:??:??:??";
+ }
+
+ StringBuilder sb = new StringBuilder(18);
+ for (byte b : mac) {
+ if (sb.length() > 0) {
+ sb.append(':');
+ }
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Builds a NetworkItem object from a given Network object, aggregating info across Network,
+ * NetworkCapabilities, NetworkInfo, NetworkInterface, and LinkProperties objects and pass it
+ * all as a string for the UI to use
+ */
+ private NetworkItem getNetworkItem(Network n) {
+
+ // Get default network to assign the button text correctly
+ // NOTE: activeNetwork != ProcessDefault when you set one, active is tracking the default
+ // request regardless of your process's default
+ // Network defNetwork = mConnectivityManager.getActiveNetwork();
+ Network defNetwork = mConnectivityManager.getBoundNetworkForProcess();
+
+ // Used to get network state
+ NetworkInfo nInfo = mConnectivityManager.getNetworkInfo(n);
+
+ // Used to get transport type(s), capabilities
+ NetworkCapabilities nCaps = mConnectivityManager.getNetworkCapabilities(n);
+
+ // Properties of the actual physical link
+ LinkProperties nLink = mConnectivityManager.getLinkProperties(n);
+
+ // Object representing the actual interface
+ NetworkInterface nIface = null;
+ try {
+ nIface = NetworkInterface.getByName(nLink.getInterfaceName());
+ } catch (SocketException exception) {
+ Log.e(TAG, "SocketException -- Failed to get interface info");
+ }
+
+ // Pack NetworkItem with all values
+ NetworkItem ni = new NetworkItem();
+
+ // Row key
+ ni.mNetId = n.netId;
+
+ // LinkProperties/NetworkInterface
+ ni.mInterfaceName = "Interface: " + nLink.getInterfaceName()
+ + (nIface != null ? " (" + nIface.getName() + ")" : " ()");
+ ni.mHwAddress = "HwAddress: " + getMacAddress(nIface);
+ ni.mIpAddresses = "IP Addresses: " + nLink.getLinkAddresses().toString();
+ ni.mDnsAddresses = "DNS: " + nLink.getDnsServers().toString();
+ ni.mDomains = "Domains: " + nLink.getDomains();
+ ni.mRoutes = "Routes: " + nLink.getRoutes().toString();
+
+ // NetworkInfo
+ ni.mType = "Type: " + nInfo.getTypeName() + " (" + nInfo.getSubtypeName() + ")";
+ ni.mState = "State: " + nInfo.getState().name() + "/" + nInfo.getDetailedState().name();
+ ni.mConnected = "Connected: " + (nInfo.isConnected() ? "Connected" : "Disconnected");
+ ni.mAvailable = "Available: " + (nInfo.isAvailable() ? "Yes" : "No");
+ ni.mRoaming = "Roaming: " + (nInfo.isRoaming() ? "Yes" : "No");
+
+ // NetworkCapabilities
+ ni.mTransports = "Transports: " + getTransportString(nCaps);
+ ni.mCapabilities = "Capabilities: " + getCapabilitiesString(nCaps);
+ ni.mBandwidth = "Bandwidth (Down/Up): " + nCaps.getLinkDownstreamBandwidthKbps()
+ + " Kbps/" + nCaps.getLinkUpstreamBandwidthKbps() + " Kbps";
+
+ // Other inferred values
+ ni.mDefault = sameNetworkId(n, defNetwork);
+ ni.mRequested = (mNetworkCallbacks.get(n.netId) != null);
+
+ return ni;
+ }
+
+ // Refresh the networks content and prompt the user that we did it
+ private void refreshNetworksAndPrompt() {
+ refreshNetworks();
+ showToast("Refreshed Networks (" + mNetworkItems.length + ")");
+ }
+
+ /**
+ * Gets the current set of networks from the connectivity manager and 1) stores the network
+ * objects 2) builds NetworkItem objects for the view to render and 3) If a network we were
+ * tracking disappears then it kills its callback.
+ */
+ private void refreshNetworks() {
+ Log.i(TAG, "refreshNetworks()");
+ Network[] networks = mConnectivityManager.getAllNetworks();
+ mNetworkItems = new NetworkItem[networks.length];
+ mNetworks.clear();
+
+ // Add each network to the network info set, turning each field to a string
+ for (int i = 0; i < networks.length; i++) {
+ mNetworkItems[i] = getNetworkItem(networks[i]);
+ mNetworks.put(networks[i].netId, networks[i]);
+ }
+
+ // Check for callbacks that belong to networks that don't exist anymore
+ for (int i = 0; i < mNetworkCallbacks.size(); i++) {
+ int key = mNetworkCallbacks.keyAt(i);
+ if (mNetworks.get(key) == null) {
+ mNetworkCallbacks.remove(key);
+ }
+ }
+
+ // Update the view
+ mNetworksAdapter.refreshNetworks(mNetworkItems);
+ }
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
mConnectivityManager = getActivity().getSystemService(ConnectivityManager.class);
-
mConnectivityManager.addDefaultNetworkActiveListener(() -> refreshNetworks());
}
@@ -79,77 +428,41 @@
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.connectivity_fragment, container, false);
+ // Create the ListView of all networks
ListView networksView = view.findViewById(R.id.networks);
- mNetworksAdapter = new ArrayAdapter<>(getActivity(), R.layout.list_item, mNetworks);
+ mNetworksAdapter = new NetworkListAdapter(getContext(), mNetworkItems, this);
networksView.setAdapter(mNetworksAdapter);
- setClickAction(view, R.id.networksRefresh, this::refreshNetworks);
- setClickAction(view, R.id.networkRequestOemPaid, this::requestOemPaid);
- setClickAction(view, R.id.networkRequestEth1, this::requestEth1);
- setClickAction(view, R.id.networkReleaseNetwork, this::releaseNetworkRequest);
+ // Find all networks ListView refresher and set the refresh callback
+ mNetworkListRefresher = (SwipeRefreshLayout) view.findViewById(R.id.refreshNetworksList);
+ mNetworkListRefresher.setOnRefreshListener(() -> {
+ refreshNetworksAndPrompt();
+ mNetworkListRefresher.setRefreshing(false);
+ });
return view;
}
- private void releaseNetworkRequest() {
- mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
- showToast("Release request sent");
- }
-
- private void requestEth1() {
- NetworkRequest request = new NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .setNetworkSpecifier("eth1")
- .build();
- mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler);
- }
-
- private void requestOemPaid() {
- NetworkRequest request = new NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)
- .build();
-
- mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler);
- }
-
@Override
public void onResume() {
super.onResume();
refreshNetworks();
}
- private void setClickAction(View view, int id, Runnable action) {
- view.findViewById(id).setOnClickListener(v -> action.run());
+ @Override
+ public void onPause() {
+ super.onPause();
+ releaseAllNetworks();
}
- private void refreshNetworks() {
- mNetworks.clear();
-
- for (Network network : mConnectivityManager.getAllNetworks()) {
- boolean isDefault = sameNetworkId(network, mConnectivityManager.getActiveNetwork());
- NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(network);
- boolean isOemPaid = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID);
- boolean isInternet = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-
- NetworkInfo networkInfo = mConnectivityManager.getNetworkInfo(network);
-
- mNetworks.add("netId: " + network.netId
- + (isInternet ? " [INTERNET]" : "")
- + (isDefault ? " [DEFAULT]" : "")
- + (isOemPaid ? " [OEM-paid]" : "") + nc + " " + networkInfo);
- }
-
- mNetworksAdapter.notifyDataSetChanged();
- }
-
- private void showToast(String text) {
- Log.d(TAG, "showToast: " + text);
- Toast.makeText(getContext(), text, Toast.LENGTH_LONG).show();
+ public void showToast(String text) {
+ Toast toast = Toast.makeText(getContext(), text, Toast.LENGTH_SHORT);
+ TextView v = (TextView) toast.getView().findViewById(android.R.id.message);
+ v.setTextColor(Color.WHITE);
+ toast.show();
}
private static boolean sameNetworkId(Network net1, Network net2) {
return net1 != null && net2 != null && net1.netId == net2.netId;
-
}
}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java
new file mode 100644
index 0000000..b726654
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java
@@ -0,0 +1,208 @@
+/*
+ * 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 com.google.android.car.kitchensink.connectivity;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.google.android.car.kitchensink.R;
+import com.google.android.car.kitchensink.connectivity.ConnectivityFragment.NetworkItem;
+
+public class NetworkListAdapter extends ArrayAdapter<NetworkItem> {
+ private static final String TAG = NetworkListAdapter.class.getSimpleName();
+
+ private Context mContext;
+ private NetworkItem[] mNetworkList; // keep list of objects
+ private ConnectivityFragment mFragment; // for calling things on button press
+
+ public NetworkListAdapter(Context context, NetworkItem[] items,
+ ConnectivityFragment fragment) {
+ super(context, R.layout.network_item, items);
+ mContext = context;
+ mFragment = fragment;
+ mNetworkList = items;
+
+ Log.i(TAG, "Created NetworkListAdaptor");
+ }
+
+ // Returns a list item view for each position
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder vh;
+ if (convertView == null) {
+ vh = new ViewHolder();
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ convertView = inflater.inflate(R.layout.network_item, parent, false);
+ vh.netActive = convertView.findViewById(R.id.network_active);
+ vh.netId = convertView.findViewById(R.id.network_id);
+ vh.netType = convertView.findViewById(R.id.network_type);
+ vh.netState = convertView.findViewById(R.id.network_state);
+ vh.connected = convertView.findViewById(R.id.network_connected);
+ vh.available = convertView.findViewById(R.id.network_available);
+ vh.roaming = convertView.findViewById(R.id.network_roaming);
+ vh.netIface = convertView.findViewById(R.id.network_iface);
+ vh.hwAddress = convertView.findViewById(R.id.hw_address);
+ vh.ipAddresses = convertView.findViewById(R.id.network_ip_addresses);
+ vh.dns = convertView.findViewById(R.id.network_dns);
+ vh.domains = convertView.findViewById(R.id.network_domains);
+ vh.routes = convertView.findViewById(R.id.network_routes);
+ vh.transports = convertView.findViewById(R.id.network_transports);
+ vh.capabilities = convertView.findViewById(R.id.network_capabilities);
+ vh.bandwidth = convertView.findViewById(R.id.network_bandwidth);
+ vh.requestButton = convertView.findViewById(R.id.network_request);
+ vh.defaultButton = convertView.findViewById(R.id.network_default);
+ vh.reportButton = convertView.findViewById(R.id.network_report);
+
+ convertView.setTag(vh);
+ } else {
+ vh = (ViewHolder) convertView.getTag();
+ }
+
+ // If there's data to fill for the given position in the list
+ if (position < getCount()) {
+ vh.netId.setText("" + mNetworkList[position].mNetId);
+ vh.netType.setText(mNetworkList[position].mType);
+ vh.netState.setText(mNetworkList[position].mState);
+ vh.connected.setText(mNetworkList[position].mConnected);
+ vh.available.setText(mNetworkList[position].mAvailable);
+ vh.roaming.setText(mNetworkList[position].mRoaming);
+ vh.netIface.setText(mNetworkList[position].mInterfaceName);
+ vh.hwAddress.setText(mNetworkList[position].mHwAddress);
+ vh.ipAddresses.setText(mNetworkList[position].mIpAddresses);
+ vh.dns.setText(mNetworkList[position].mDnsAddresses);
+ vh.domains.setText(mNetworkList[position].mDomains);
+ vh.routes.setText(mNetworkList[position].mRoutes);
+ vh.transports.setText(mNetworkList[position].mTransports);
+ vh.capabilities.setText(mNetworkList[position].mCapabilities);
+ vh.bandwidth.setText(mNetworkList[position].mBandwidth);
+
+ // Active request indicator
+ vh.netActive.setBackgroundColor(mNetworkList[position].mRequested
+ ? Color.parseColor("#5fdd6e")
+ : Color.parseColor("#ff3d3d"));
+
+ // Request to track button
+ setToggleButton(position, vh.requestButton, mNetworkList[position].mRequested,
+ "Release", "Request", this::onRequestClicked);
+
+ // Process default button
+ setToggleButton(position, vh.defaultButton, mNetworkList[position].mDefault,
+ "Remove Default", "Set Default", this::onDefaultClicked);
+
+ // Report network button
+ setPositionTaggedCallback(position, vh.reportButton, this::onReportClicked);
+ }
+
+ // Alternate table row background color to make it easier to view
+ convertView.setBackgroundColor(((position % 2) != 0)
+ ? Color.parseColor("#2A2E2D")
+ : Color.parseColor("#1E1E1E"));
+
+ return convertView;
+ }
+
+ // Tags a button with its element position and assigned it's callback. The callback can then
+ // get the tag and use it as a position to know which data is associated with it
+ private void setPositionTaggedCallback(int position, Button button, View.OnClickListener l) {
+ button.setTag(position);
+ button.setOnClickListener(l);
+ }
+
+ private void setToggleButton(int position, Button button, boolean on, String ifOn, String ifOff,
+ View.OnClickListener l) {
+ // Manage button text based on status
+ if (on) {
+ button.setText(ifOn);
+ } else {
+ button.setText(ifOff);
+ }
+ setPositionTaggedCallback(position, button, l);
+ }
+
+ private void onRequestClicked(View view) {
+ int position = (int) view.getTag();
+ if (mNetworkList[position].mRequested) {
+ mFragment.releaseNetworkById(mNetworkList[position].mNetId);
+ mNetworkList[position].mRequested = false;
+ } else {
+ mFragment.requestNetworkById(mNetworkList[position].mNetId);
+ mNetworkList[position].mRequested = true;
+ }
+ notifyDataSetChanged();
+ }
+
+ private void onDefaultClicked(View view) {
+ int position = (int) view.getTag();
+ if (mNetworkList[position].mDefault) {
+ mFragment.clearBoundNetwork();
+ mNetworkList[position].mDefault = false;
+ } else {
+ for (int i = 0; i < mNetworkList.length; i++) {
+ if (i != position) {
+ mNetworkList[i].mDefault = false;
+ }
+ }
+ mFragment.bindToNetwork(mNetworkList[position].mNetId);
+ mNetworkList[position].mDefault = true;
+ }
+ notifyDataSetChanged();
+ }
+
+ private void onReportClicked(View view) {
+ int position = (int) view.getTag();
+ mFragment.reportNetworkbyId(mNetworkList[position].mNetId);
+ }
+
+ public void refreshNetworks(NetworkItem[] networksIn) {
+ mNetworkList = networksIn;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mNetworkList.length;
+ }
+
+ static class ViewHolder {
+ public View netActive;
+ public TextView netId;
+ public TextView netType;
+ public TextView netState;
+ public TextView connected;
+ public TextView available;
+ public TextView roaming;
+ public TextView netIface;
+ public TextView hwAddress;
+ public TextView ipAddresses;
+ public TextView dns;
+ public TextView domains;
+ public TextView routes;
+ public TextView transports;
+ public TextView capabilities;
+ public TextView bandwidth;
+ public Button requestButton;
+ public Button defaultButton;
+ public Button reportButton;
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
index ac0bdae..e199821 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
@@ -32,7 +32,7 @@
import com.google.android.car.kitchensink.R;
/**
- * Shows alert dialogs
+ * Displays info about the display this is run on.
*/
public class DisplayInfoFragment extends Fragment {
@@ -95,11 +95,18 @@
}
private void addDimenText(String dimenName) {
- addTextView(dimenName + " : " + convertPixelsToDp(
- getResources().getDimensionPixelSize(
- getResources().getIdentifier(
- dimenName, "dimen", getContext().getPackageName())),
- getContext()));
+ String value;
+ try {
+ float dimen = convertPixelsToDp(
+ getResources().getDimensionPixelSize(
+ getResources().getIdentifier(
+ dimenName, "dimen", getContext().getPackageName())),
+ getContext());
+ value = Float.toString(dimen);
+ } catch (Resources.NotFoundException e) {
+ value = "Resource Not Found";
+ }
+ addTextView(dimenName + " : " + value);
}
private void addTextView(String text) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
index 5128816..34ea1cf 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
@@ -30,7 +30,6 @@
import android.hardware.automotive.vehicle.V2_0.VehicleArea;
import android.hardware.automotive.vehicle.V2_0.VehicleDisplay;
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
-import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyGroup;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType;
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
index 7a26f93..d7230fc 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
@@ -45,6 +45,8 @@
Button importanceDefaultButton = view.findViewById(R.id.importance_default_button);
Button ongoingButton = view.findViewById(R.id.ongoing_button);
Button messageButton = view.findViewById(R.id.category_message_button);
+ Button emerg = view.findViewById(R.id.category_car_emerg_button);
+ Button warn = view.findViewById(R.id.category_car_warning_button);
NotificationManager manager =
(NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
@@ -184,6 +186,28 @@
manager.notify(3, notification);
});
+ emerg.setOnClickListener(v -> {
+
+ Notification notification = new Notification.Builder(getActivity(), CHANNEL_ID_1)
+ .setContentTitle("OMG")
+ .setContentText("This is of top importance")
+ .setCategory(Notification.CATEGORY_CAR_EMERGENCY)
+ .setSmallIcon(R.drawable.car_ic_mode)
+ .build();
+ manager.notify(10, notification);
+ });
+
+ warn.setOnClickListener(v -> {
+
+ Notification notification = new Notification.Builder(getActivity(), CHANNEL_ID_1)
+ .setContentTitle("OMG -ish ")
+ .setContentText("This is of less importance but still")
+ .setCategory(Notification.CATEGORY_CAR_WARNING)
+ .setSmallIcon(R.drawable.car_ic_mode)
+ .build();
+ manager.notify(11, notification);
+ });
+
return view;
}
}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
index b99f5da..5d0c02e 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
@@ -71,9 +71,9 @@
mSensorMgr.registerListener(mSensorListener, magneticFieldSensor,
SensorManager.SENSOR_DELAY_FASTEST);
- mTextUpdateHandler.setAccelField("waiting to hear from SensorManager");
- mTextUpdateHandler.setGyroField("waiting to hear from SensorManager");
- mTextUpdateHandler.setMagField("waiting to hear from SensorManager");
+ mTextUpdateHandler.setAccelField("Accel waiting to hear from SensorManager");
+ mTextUpdateHandler.setGyroField("Gyro waiting to hear from SensorManager");
+ mTextUpdateHandler.setMagField("Mag waiting to hear from SensorManager");
} else {
mTextUpdateHandler.setAccelField("SensorManager not available");
mTextUpdateHandler.setGyroField("SensorManager not available");
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 77a6e1c..82a8cb2 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
@@ -43,7 +43,6 @@
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -343,11 +342,11 @@
if (event == null) {
return mNaString;
}
- return mDateFormat.format(new Date(event.timestamp / (1000L * 1000L)));
+ return Double.toString(event.timestamp / (1000L * 1000L * 1000L)) + " seconds";
}
private String getTimestampNow() {
- return mDateFormat.format(new Date(System.nanoTime() / (1000L * 1000L)));
+ return Double.toString(System.nanoTime() / (1000L * 1000L * 1000L)) + " seconds";
}
private String getFuelLevel(CarSensorEvent event) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java
new file mode 100644
index 0000000..2d2f864
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 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.weblinks;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+
+/**
+ * This fragment just has a few links to web pages.
+ */
+public class WebLinksTestFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.weblinks_fragment, container, false);
+
+ LinearLayout buttons = root.findViewById(R.id.buttons);
+ for (int i = 0; i < buttons.getChildCount(); i++) {
+ buttons.getChildAt(i).setOnClickListener(this::onClick);
+ }
+
+ return root;
+ }
+
+ private void onClick(View view) {
+ String url = view.getTag().toString();
+
+ if (!url.startsWith("http")) {
+ url = "http://" + url;
+ }
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
+ }
+}
diff --git a/tests/UxRestrictionsSample/AndroidManifest.xml b/tests/UxRestrictionsSample/AndroidManifest.xml
index 43a6362..bd28e0d 100644
--- a/tests/UxRestrictionsSample/AndroidManifest.xml
+++ b/tests/UxRestrictionsSample/AndroidManifest.xml
@@ -19,7 +19,7 @@
<uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
<application android:label="UxRestrictions Sample">
- <activity android:name=".MainActivity" android:theme="@style/Theme.Car.NoActionBar">
+ <activity android:name=".MainActivity" android:theme="@style/AppTheme" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -27,7 +27,7 @@
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
<activity android:name=".SampleMessageActivity"
- android:theme="@style/Theme.Car.NoActionBar">
+ android:theme="@style/AppTheme">
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
</application>
diff --git a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml b/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
index 20c6465..a778975 100644
--- a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
+++ b/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
@@ -13,29 +13,24 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<androidx.car.moderator.SpeedBumpView
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <Button
- android:id="@+id/home_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/return_home"
- android:textAllCaps="false"
- android:textAppearance="?android:textAppearanceLarge"/>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <androidx.car.widget.PagedListView
- android:id="@+id/paged_list_view"
- android:layout_height="match_parent"
- android:layout_width="wrap_content"
- android:layout_weight="4"/>
- </LinearLayout>
- </LinearLayout>
-</androidx.car.moderator.SpeedBumpView>
+ <Button
+ android:id="@+id/home_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/return_home"
+ android:textAllCaps="false"
+ android:textAppearance="?android:textAppearanceLarge"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <androidx.car.widget.PagedListView
+ android:id="@+id/paged_list_view"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_weight="4"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/tests/UxRestrictionsSample/res/layout/main_activity.xml b/tests/UxRestrictionsSample/res/layout/main_activity.xml
index 695e93b..851497a 100644
--- a/tests/UxRestrictionsSample/res/layout/main_activity.xml
+++ b/tests/UxRestrictionsSample/res/layout/main_activity.xml
@@ -13,114 +13,137 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<androidx.car.moderator.SpeedBumpView
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout
- android:orientation="horizontal"
+
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:text="@string/status_header"
+ android:layout_gravity="center"
+ android:padding="@dimen/section_padding"
+ android:textSize="@dimen/header_text_size"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
- <LinearLayout
- android:layout_height="match_parent"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:orientation="vertical">
+ <TextView
+ android:id="@+id/driving_state"
+ android:text="@string/driving_state"
+ android:textSize="@dimen/info_text_size"
+ android:layout_gravity="center"
+ android:padding="@dimen/section_padding"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
- <TextView
- android:text="@string/status_header"
- android:layout_gravity="center"
- android:padding="@dimen/section_padding"
- android:textSize="@dimen/header_text_size"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/do_status"
+ android:text="@string/is_do_reqd"
+ android:textSize="@dimen/info_text_size"
+ android:padding="@dimen/section_padding"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
- <TextView
- android:id="@+id/driving_state"
- android:text="@string/driving_state"
- android:textSize="@dimen/info_text_size"
- android:layout_gravity="center"
- android:padding="@dimen/section_padding"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/uxr_status"
+ android:text="@string/active_restrictions"
+ android:padding="@dimen/section_padding"
+ android:textSize="@dimen/info_text_size"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
- <TextView
- android:id="@+id/do_status"
- android:text="@string/is_do_reqd"
- android:textSize="@dimen/info_text_size"
- android:padding="@dimen/section_padding"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:padding="@dimen/section_padding"
+ android:layout_marginBottom="10dp"
+ android:background="@android:color/darker_gray"/>
- <TextView
- android:id="@+id/uxr_status"
- android:text="@string/active_restrictions"
- android:padding="@dimen/section_padding"
- android:textSize="@dimen/info_text_size"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
+ <TextView
+ android:text="@string/action_header"
+ android:padding="@dimen/section_padding"
+ android:textSize="@dimen/header_text_size"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:padding="@dimen/section_padding"
- android:layout_marginBottom="10dp"
- android:background="@android:color/darker_gray"/>
-
- <TextView
- android:text="@string/action_header"
- android:padding="@dimen/section_padding"
- android:textSize="@dimen/header_text_size"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <Button
- android:id="@+id/toggle_status"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="@dimen/section_padding"
- android:text="@string/disable_uxr"
- android:textAllCaps="false"
- android:textSize="@dimen/info_text_size" />
- </LinearLayout>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginTop="@dimen/section_padding"
- android:layout_marginBottom="10dp"
- android:background="@android:color/darker_gray"/>
-
- <TextView
- android:text="@string/sample_header"
- android:padding="@dimen/section_padding"
- android:textSize="@dimen/header_text_size"
- android:layout_width="match_parent"
- android:textAppearance="?android:textAppearanceLarge"
- android:layout_height="wrap_content" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <Button
- android:id="@+id/launch_message"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/sample_msg_activity"
- android:textAllCaps="false"
- android:textSize="@dimen/info_text_size" />
- </LinearLayout>
- </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <Button
+ android:id="@+id/toggle_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/section_padding"
+ android:text="@string/disable_uxr"
+ android:textAllCaps="false"
+ android:textSize="@dimen/info_text_size"/>
</LinearLayout>
-</androidx.car.moderator.SpeedBumpView>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="@dimen/section_padding"
+ android:layout_marginBottom="10dp"
+ android:background="@android:color/darker_gray"/>
+
+ <TextView
+ android:text="@string/sample_header"
+ android:padding="@dimen/section_padding"
+ android:textSize="@dimen/header_text_size"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <Button
+ android:id="@+id/launch_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_msg_activity"
+ android:textAllCaps="false"
+ android:textSize="@dimen/info_text_size"/>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="@dimen/section_padding"
+ android:layout_marginBottom="10dp"
+ android:background="@android:color/darker_gray"/>
+
+ <TextView
+ android:text="@string/save_uxr_config_header"
+ android:padding="@dimen/section_padding"
+ android:textSize="@dimen/header_text_size"
+ android:layout_width="match_parent"
+ android:textAppearance="?android:textAppearanceLarge"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <Button
+ android:id="@+id/save_uxr_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/save_uxr_config"
+ android:textSize="@dimen/info_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/tests/UxRestrictionsSample/res/values/strings.xml b/tests/UxRestrictionsSample/res/values/strings.xml
index 612e251..3b0b69b 100644
--- a/tests/UxRestrictionsSample/res/values/strings.xml
+++ b/tests/UxRestrictionsSample/res/values/strings.xml
@@ -25,4 +25,9 @@
<string name="sample_header"><u>Sample Activities</u></string>
<string name="sample_msg_activity">Sample Message Activity</string>
<string name="return_home"><u>Return Home</u></string>
+ <string name="save_uxr_config_header"><u>Save UX Restrictions For Next Boot</u></string>
+ <string name="save_uxr_config">Save UX Restrictions</string>
+ <string name="set_uxr_config_dialog_title">Select restrictions for IDLING/MOVING</string>
+ <string name="set_uxr_config_dialog_negative_button">Cancel</string>
+ <string name="set_uxr_config_dialog_positive_button">Save UXR Config</string>
</resources>
diff --git a/tests/UxRestrictionsSample/res/values/styles.xml b/tests/UxRestrictionsSample/res/values/styles.xml
new file mode 100644
index 0000000..05d0d03
--- /dev/null
+++ b/tests/UxRestrictionsSample/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<resources>
+
+ <style name="AppTheme" parent="@style/Theme.Car.NoActionBar">
+ <item name="android:windowBackground">@android:color/black</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
index 104e145..cd9c015 100644
--- a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
+++ b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
@@ -15,13 +15,19 @@
*/
package com.google.android.car.uxr.sample;
+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 android.app.Activity;
+import android.app.AlertDialog;
import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.content.pm.CarPackageManager;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarDrivingStateManager;
import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.ComponentName;
import android.content.Intent;
@@ -39,6 +45,21 @@
*/
public class MainActivity extends Activity {
public static final String TAG = "drivingstate";
+
+ // Order of elements is based on number of bits shifted in value of the constants.
+ private static final CharSequence[] UX_RESTRICTION_NAMES = new CharSequence[] {
+ "BASELINE",
+ "NO_DIALPAD",
+ "NO_FILTERING",
+ "LIMIT_STRING_LENGTH",
+ "NO_KEYBOARD",
+ "NO_VIDEO",
+ "LIMIT_CONTENT",
+ "NO_SETUP",
+ "NO_TEXT_MESSAGE",
+ "NO_VOICE_TRANSCRIPTION",
+ };
+
private Car mCar;
private CarDrivingStateManager mCarDrivingStateManager;
private CarUxRestrictionsManager mCarUxRestrictionsManager;
@@ -48,6 +69,7 @@
private TextView mUxrStatus;
private Button mToggleButton;
private Button mSampleMsgButton;
+ private Button mSaveUxrConfigButton;
private boolean mEnableUxR;
@@ -74,7 +96,6 @@
mCarUxRestrictionsManager.registerListener(mUxRChangeListener);
updateUxRText(mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
}
-
} catch (CarNotConnectedException e) {
Log.e(TAG, "Failed to get a connection", e);
}
@@ -96,7 +117,9 @@
: "No Distraction Optimization required");
mUxrStatus.setText("Active Restrictions : 0x"
- + Integer.toHexString(restrictions.getActiveRestrictions()));
+ + Integer.toHexString(restrictions.getActiveRestrictions())
+ + " - "
+ + Integer.toBinaryString(restrictions.getActiveRestrictions()));
mDistractionOptStatus.requestLayout();
mUxrStatus.requestLayout();
@@ -123,13 +146,13 @@
}
String displayText;
switch (state.eventValue) {
- case CarDrivingStateEvent.DRIVING_STATE_PARKED:
+ case DRIVING_STATE_PARKED:
displayText = "Parked";
break;
- case CarDrivingStateEvent.DRIVING_STATE_IDLING:
+ case DRIVING_STATE_IDLING:
displayText = "Idling";
break;
- case CarDrivingStateEvent.DRIVING_STATE_MOVING:
+ case DRIVING_STATE_MOVING:
displayText = "Moving";
break;
default:
@@ -147,8 +170,9 @@
this::updateDrivingStateText;
@Override
- public void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
setContentView(R.layout.main_activity);
mDrvStatus = findViewById(R.id.driving_state);
@@ -156,9 +180,10 @@
mUxrStatus = findViewById(R.id.uxr_status);
mToggleButton = findViewById(R.id.toggle_status);
- mToggleButton.setOnClickListener(v -> {
- updateToggleUxREnable();
- });
+ mSaveUxrConfigButton = findViewById(R.id.save_uxr_config);
+ mSaveUxrConfigButton.setOnClickListener(v -> saveUxrConfig());
+
+ mToggleButton.setOnClickListener(v -> updateToggleUxREnable());
mSampleMsgButton = findViewById(R.id.launch_message);
mSampleMsgButton.setOnClickListener(this::launchSampleMsgActivity);
@@ -168,14 +193,49 @@
mCar.connect();
}
+ private void saveUxrConfig() {
+ // Pop up a dialog to build the IDLING restrictions.
+ boolean[] selected = new boolean[UX_RESTRICTION_NAMES.length];
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.set_uxr_config_dialog_title)
+ .setMultiChoiceItems(UX_RESTRICTION_NAMES, null,
+ (dialog, which, isChecked) -> selected[which] = isChecked)
+ .setPositiveButton(R.string.set_uxr_config_dialog_positive_button,
+ (dialog, id) -> setUxRestrictionsConfig(selected))
+ .setNegativeButton(R.string.set_uxr_config_dialog_negative_button, null)
+ .show();
+ }
+
+ private void setUxRestrictionsConfig(boolean[] selected) {
+ int selectedRestrictions = 0;
+ // Iteration starts at 1 because 0 is BASELINE (no restrictions).
+ for (int i = 1; i < selected.length; i++) {
+ if (selected[i]) {
+ selectedRestrictions += 1 << (i - 1);
+ }
+ }
+ boolean reqOpt = selectedRestrictions != 0;
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, false, 0)
+ .setUxRestrictions(DRIVING_STATE_IDLING, reqOpt, selectedRestrictions)
+ .setUxRestrictions(DRIVING_STATE_MOVING, reqOpt, selectedRestrictions)
+ .build();
+
+ try {
+ mCarUxRestrictionsManager.saveUxRestrictionsConfigurationForNextBoot(config);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car not connected", e);
+ }
+ }
+
private void launchSampleMsgActivity(View view) {
Intent msgIntent = new Intent(this, SampleMessageActivity.class);
startActivity(msgIntent);
}
-
@Override
protected void onDestroy() {
+ super.onDestroy();
try {
if (mCarUxRestrictionsManager != null) {
mCarUxRestrictionsManager.unregisterListener();
diff --git a/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java b/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
index 41de024..ca9915d 100644
--- a/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
+++ b/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
@@ -88,6 +88,14 @@
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "Disconnect from Car Service");
+ if (mVmsSubscriberManager != null) {
+ try {
+ mVmsSubscriberManager.clearVmsSubscriberClientCallback();
+ mVmsSubscriberManager.unsubscribe(TEST_LAYER);
+ } catch (android.car.CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
+ }
+ }
}
private VmsSubscriberManager getVmsSubscriberManager() {
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
index 5b3953f..ecc2e47 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
@@ -25,6 +25,7 @@
import android.os.IBinder;
import android.support.test.filters.RequiresDevice;
import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -103,6 +104,8 @@
mManager.unregisterProjectionRunner(intent);
}
+ //TODO(b/120081013): move this test to CTS
+ @Suppress
@RequiresDevice
public void testAccessPoint() throws Exception {
CountDownLatch startedLatch = new CountDownLatch(1);
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
new file mode 100644
index 0000000..c55683a
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2018 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.apitest;
+
+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.CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+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 android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.support.test.filters.SmallTest;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+/**
+ * Unit test for UXR config and its subclasses.
+ */
+@SmallTest
+public class CarUxRestrictionsConfigurationTest extends TestCase {
+
+ // This test verifies the expected way to build config would succeed.
+ public void testConstruction() {
+ new CarUxRestrictionsConfiguration.Builder().build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setMaxStringLength(1)
+ .build();
+
+ new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+ .build();
+
+ new CarUxRestrictionsConfiguration.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)
+ .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)
+ .build();
+ }
+
+ public void testUnspecifiedDrivingStateUsesDefaultRestriction() {
+ CarUxRestrictionsConfiguration config =
+ new CarUxRestrictionsConfiguration.Builder().build();
+
+ CarUxRestrictions parkedRestrictions = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+ assertTrue(parkedRestrictions.isRequiresDistractionOptimization());
+ assertEquals(parkedRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ CarUxRestrictions movingRestrictions = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+ assertTrue(movingRestrictions.isRequiresDistractionOptimization());
+ 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);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_NonZeroStart() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, MAX_SPEED),
+ true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_Overlap() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(4), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_SpeedRange_Gap() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ builder.setUxRestrictions(DRIVING_STATE_MOVING,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(8), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+ try {
+ builder.build();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testBuilderValidation_NonMovingStateCannotUseSpeedRange() {
+ CarUxRestrictionsConfiguration.Builder builder =
+ new CarUxRestrictionsConfiguration.Builder();
+ try {
+ builder.setUxRestrictions(DRIVING_STATE_PARKED,
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+ UX_RESTRICTIONS_FULLY_RESTRICTED);
+ } catch (IllegalArgumentException 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);
+ }
+
+ public void testSpeedRange_NegativeMax() {
+ try {
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f, -1f);
+ } catch (IllegalArgumentException e) {
+ // Expected exception.
+ }
+ }
+
+ public void testSpeedRange_MinGreaterThanMax() {
+ try {
+ new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, 2f);
+ } catch (IllegalArgumentException 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);
+ 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);
+ 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);
+ 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);
+ assertTrue(s1.compareTo(s2) < 0);
+ assertTrue(s2.compareTo(s1) > 0);
+ }
+
+ public void testSpeedRangeEquals() {
+ CarUxRestrictionsConfiguration.Builder.SpeedRange s1, s2;
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ assertTrue(s1.equals(s1));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ s2 = new CarUxRestrictionsConfiguration.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);
+ assertTrue(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+ assertTrue(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+ s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+ assertFalse(s1.equals(s2));
+
+ s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+ s2 = new CarUxRestrictionsConfiguration.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();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_RestrictionParameters() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setMaxStringLength(1)
+ .setMaxCumulativeContentItems(1)
+ .setMaxContentDepth(1)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_NonMovingStateRestrictions() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testJsonSerialization_MovingStateNoSpeedRange() {
+ CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+ .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ 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)
+ .build();
+
+ verifyConfigThroughJsonSerialization(config);
+ }
+
+ public void testDump() {
+ CarUxRestrictionsConfiguration[] configs = new CarUxRestrictionsConfiguration[] {
+ // Driving state with no speed range
+ new CarUxRestrictionsConfiguration.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()
+ .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)
+ .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)
+ .build(),
+ };
+
+ for (CarUxRestrictionsConfiguration config : configs) {
+ config.dump(new PrintWriter(new ByteArrayOutputStream()));
+ }
+ }
+
+ 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)
+ .build();
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try (PrintWriter writer = new PrintWriter(output)) {
+ config.dump(writer);
+ }
+
+ String dump = new String(output.toByteArray());
+ assertTrue(dump.contains("Max String length"));
+ assertTrue(dump.contains("Max Cumulative Content Items"));
+ assertTrue(dump.contains("Max Content depth"));
+ assertTrue(dump.contains("State:moving"));
+ assertTrue(dump.contains("Speed Range"));
+ assertTrue(dump.contains("Requires DO?"));
+ assertTrue(dump.contains("Restrictions"));
+ }
+
+ private void verifyConfigThroughJsonSerialization(CarUxRestrictionsConfiguration config) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out))) {
+ config.writeJson(writer);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ try (JsonReader reader = new JsonReader(new InputStreamReader(in))) {
+ CarUxRestrictionsConfiguration deserialized = CarUxRestrictionsConfiguration.readJson(
+ reader);
+ assertTrue(config.equals(deserialized));
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+}
diff --git a/tests/carservice_test/Android.mk b/tests/carservice_test/Android.mk
index 0270748..7c17e7e 100644
--- a/tests/carservice_test/Android.mk
+++ b/tests/carservice_test/Android.mk
@@ -20,7 +20,8 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_RESOURCE_DIR += packages/services/Car/service/res
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res \
+ packages/services/Car/service/res
LOCAL_AAPT_FLAGS += --extra-packages com.android.car --auto-add-overlay
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml
new file mode 100644
index 0000000..9dd3678
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+ <RestrictionMapping>
+ <DrivingState car:state="moving" car:minSpeed="0" car:maxSpeed="5.0">
+ <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+ </DrivingState>
+ <!-- Restrictions for speed >=5 -->
+ <DrivingState car:state="moving" car:minSpeed="5.0">
+ <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_keyboard"/>
+ </DrivingState>
+ </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml
new file mode 100644
index 0000000..405619e
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+ <RestrictionMapping>
+ <DrivingState car:state="moving">
+ <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+ </DrivingState>
+ </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml
new file mode 100644
index 0000000..4cd11b0
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+ <RestrictionMapping>
+ <DrivingState car:state="moving" car:minSpeed="0">
+ <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+ </DrivingState>
+ </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml b/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml
new file mode 100644
index 0000000..cc49260
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+ <RestrictionMapping>
+ <DrivingState car:state="parked">
+ <Restrictions car:requiresDistractionOptimization="false" car:uxr="baseline"/>
+ </DrivingState>
+ <DrivingState car:state="idling">
+ <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+ </DrivingState>
+ </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml b/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml
new file mode 100644
index 0000000..4c30c16
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+ <!-- restriction parameters -->
+ <RestrictionParameters>
+ <StringRestrictions car:maxLength="1"/>
+ <ContentRestrictions car:maxCumulativeItems="1" car:maxDepth="1"/>
+ </RestrictionParameters>
+</UxRestrictions>
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java
new file mode 100644
index 0000000..02000a3
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 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 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarUxRestrictionsConfigurationXmlParserTest {
+ private Context getContext() {
+ return InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void testParsingDefaultConfiguration() throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfigurationXmlParser.parse(getContext(), R.xml.car_ux_restrictions_map);
+ }
+
+ @Test
+ public void testParsingParameters() throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+ getContext(), R.xml.ux_restrictions_only_parameters);
+
+ CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+ assertEquals(1, r.getMaxContentDepth());
+ assertEquals(1, r.getMaxCumulativeContentItems());
+ assertEquals(1, r.getMaxRestrictedStringLength());
+ }
+
+ @Test
+ public void testParsingNonMovingState() throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+ getContext(), R.xml.ux_restrictions_non_moving_state);
+
+ CarUxRestrictions parked = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+ assertFalse(parked.isRequiresDistractionOptimization());
+
+ CarUxRestrictions idling = config.getUxRestrictions(DRIVING_STATE_IDLING, 0f);
+ assertTrue(idling.isRequiresDistractionOptimization());
+ assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, idling.getActiveRestrictions());
+ }
+
+ @Test
+ public void testParsingMovingState_NoSpeedRange() throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+ getContext(), R.xml.ux_restrictions_moving_state_no_speed_range);
+
+ CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+ assertTrue(r.isRequiresDistractionOptimization());
+ assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, r.getActiveRestrictions());
+ }
+
+ @Test
+ public void testParsingMovingState_SingleSpeedRange()
+ throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+ getContext(), R.xml.ux_restrictions_moving_state_single_speed_range);
+
+ CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+ assertTrue(r.isRequiresDistractionOptimization());
+ assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, r.getActiveRestrictions());
+ }
+
+ @Test
+ public void testParsingMovingState_MultiSpeedRange()
+ throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+ getContext(), R.xml.ux_restrictions_moving_state_single_speed_range);
+
+ CarUxRestrictions slow = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+ assertTrue(slow.isRequiresDistractionOptimization());
+ assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, slow.getActiveRestrictions());
+
+ CarUxRestrictions fast = config.getUxRestrictions(DRIVING_STATE_MOVING, 6f);
+ assertTrue(fast.isRequiresDistractionOptimization());
+ assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, fast.getActiveRestrictions());
+ }
+}
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
new file mode 100644
index 0000000..e87badf
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2018 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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.car.hardware.CarPropertyValue;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarUxRestrictionsManagerServiceTest {
+ private CarUxRestrictionsManagerService mService;
+
+ @Mock
+ private CarDrivingStateService mMockDrivingStateService;
+ @Mock
+ private CarPropertyService mMockCarPropertyService;
+ @Mock
+ private CarUserManagerHelper mMockCarUserManagerHelper;
+
+ private Context mSpyContext;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mSpyContext = spy(InstrumentationRegistry.getTargetContext());
+
+ setUpMockParkedState();
+
+ mService = new CarUxRestrictionsManagerService(mSpyContext,
+ mMockDrivingStateService, mMockCarPropertyService, mMockCarUserManagerHelper);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mService = null;
+ }
+
+ @Test
+ public void testSaveConfig_WriteStagedFile() throws Exception {
+ // Reset spy context because service tries to access production config
+ // during construction (due to calling loadConfig()).
+ reset(mSpyContext);
+
+ File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED, null);
+ CarUxRestrictionsConfiguration config = createEmptyConfig();
+
+ assertTrue(mService.saveUxRestrictionsConfigurationForNextBoot(config));
+ assertTrue(readFile(staged).equals(config));
+ // Verify prod config file was not touched.
+ verify(mSpyContext, never()).getFileStreamPath(
+ CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION);
+ }
+
+ @Test
+ public void testSaveConfig_ReturnFalseOnException() throws Exception {
+ File tempFile = File.createTempFile("uxr_test", null);
+ doReturn(tempFile).when(mSpyContext).getFileStreamPath(anyString());
+
+ CarUxRestrictionsConfiguration spyConfig = spy(createEmptyConfig());
+ doThrow(new IOException()).when(spyConfig).writeJson(any(JsonWriter.class));
+
+ assertFalse(mService.saveUxRestrictionsConfigurationForNextBoot(spyConfig));
+ }
+
+ @Test
+ public void testSaveConfig_DoesNotAffectCurrentConfig() throws Exception {
+ File tempFile = File.createTempFile("uxr_test", null);
+ doReturn(tempFile).when(mSpyContext).getFileStreamPath(anyString());
+ CarUxRestrictionsConfiguration spyConfig = spy(createEmptyConfig());
+
+ CarUxRestrictionsConfiguration currentConfig = mService.getConfig();
+ assertTrue(mService.saveUxRestrictionsConfigurationForNextBoot(spyConfig));
+
+ verify(spyConfig).writeJson(any(JsonWriter.class));
+ // Verify current config is untouched by address comparison.
+ assertTrue(mService.getConfig() == currentConfig);
+ }
+
+ @Test
+ public void testLoadConfig_UseDefaultConfigWhenNoSavedConfigFileNoXml() {
+ // Prevent R.xml.car_ux_restrictions_map being returned.
+ Resources spyResources = spy(mSpyContext.getResources());
+ doReturn(spyResources).when(mSpyContext).getResources();
+ doReturn(null).when(spyResources).getXml(anyInt());
+
+ assertTrue(mService.loadConfig().equals(mService.createDefaultConfig()));
+ }
+
+ @Test
+ public void testLoadConfig_UseXml() throws IOException, XmlPullParserException {
+ CarUxRestrictionsConfiguration expected = CarUxRestrictionsConfigurationXmlParser.parse(
+ mSpyContext, R.xml.car_ux_restrictions_map);
+
+ CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+ assertTrue(actual.equals(expected));
+ }
+
+ @Test
+ public void testLoadConfig_UseProdConfig() throws IOException {
+ CarUxRestrictionsConfiguration expected = createEmptyConfig();
+ setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, expected);
+
+ CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+ assertTrue(actual.equals(expected));
+ }
+
+ @Test
+ public void testLoadConfig_PromoteStagedFileWhenParked() throws Exception {
+ CarUxRestrictionsConfiguration expected = createEmptyConfig();
+ // Staged file contains actual config. Ignore prod since it should be overwritten by staged.
+ File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED,
+ expected);
+ // Set up temp file for prod to avoid polluting other tests.
+ setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, null);
+
+ CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+ // Staged file should be moved as production.
+ assertFalse(staged.exists());
+ assertTrue(actual.equals(expected));
+ }
+
+ @Test
+ public void testLoadConfig_NoPromoteStagedFileWhenMoving() throws Exception {
+ CarUxRestrictionsConfiguration expected = createEmptyConfig();
+ File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED, null);
+ // Prod file contains actual config. Ignore staged since it should not be promoted.
+ setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, expected);
+
+ setUpMockDrivingState();
+ CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+ // Staged file should be untouched.
+ assertTrue(staged.exists());
+ assertTrue(actual.equals(expected));
+ }
+
+ private CarUxRestrictionsConfiguration createEmptyConfig() {
+ return new CarUxRestrictionsConfiguration.Builder().build();
+ }
+
+ private void setUpMockParkedState() {
+ when(mMockDrivingStateService.getCurrentDrivingState()).thenReturn(
+ new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_PARKED, 0));
+
+ CarPropertyValue<Float> speed = new CarPropertyValue<>(VehicleProperty.PERF_VEHICLE_SPEED,
+ 0, 0f);
+ when(mMockCarPropertyService.getProperty(VehicleProperty.PERF_VEHICLE_SPEED, 0))
+ .thenReturn(speed);
+ }
+
+ private void setUpMockDrivingState() {
+ when(mMockDrivingStateService.getCurrentDrivingState()).thenReturn(
+ new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_MOVING, 0));
+
+ CarPropertyValue<Float> speed = new CarPropertyValue<>(VehicleProperty.PERF_VEHICLE_SPEED,
+ 0, 30f);
+ when(mMockCarPropertyService.getProperty(VehicleProperty.PERF_VEHICLE_SPEED, 0))
+ .thenReturn(speed);
+ }
+
+ private File setupMockFile(String filename, CarUxRestrictionsConfiguration config)
+ throws IOException {
+ File f = File.createTempFile("uxr_test", null);
+ doReturn(f).when(mSpyContext).getFileStreamPath(filename);
+
+ if (config != null) {
+ try (JsonWriter writer = new JsonWriter(
+ new OutputStreamWriter(new FileOutputStream(f), "UTF-8"))) {
+ config.writeJson(writer);
+ }
+ }
+ return f;
+ }
+
+ private CarUxRestrictionsConfiguration readFile(File file) throws Exception {
+ try (JsonReader reader = new JsonReader(
+ new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
+ return CarUxRestrictionsConfiguration.readJson(reader);
+ }
+ }
+}
diff --git a/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java b/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
index f8a4f0a..4455113 100644
--- a/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
@@ -16,7 +16,6 @@
package com.android.car;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
@@ -37,11 +36,13 @@
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SystemActivityMonitoringServiceTest {
private static final long ACTIVITY_TIME_OUT = 5000;
+ private static final long DEFAULT_TIMEOUT_SECONDS = 2;
private SystemActivityMonitoringService mService;
private Semaphore mActivityLaunchSemaphore = new Semaphore(0);
@@ -100,17 +101,30 @@
ComponentName activityThatFinishesImmediately =
toComponentName(getTestContext(), ActivityThatFinishesImmediately.class);
startActivity(getContext(), activityThatFinishesImmediately);
- assertTrue(mActivityLaunchSemaphore.tryAcquire(2, TimeUnit.SECONDS));
+ waitUntil(() -> topTasksHasComponent(activityThatFinishesImmediately));
+ waitUntil(() -> !topTasksHasComponent(activityThatFinishesImmediately));
+ }
- // We won't know if the stack changes, unless we launch another activity.
- startActivity(getContext(), toComponentName(getTestContext(), ActivityA.class));
- assertTrue(mActivityLaunchSemaphore.tryAcquire(2, TimeUnit.SECONDS));
- for (TopTaskInfoContainer topTaskInfoContainer: mService.getTopTasks()) {
- assertNotEquals(topTaskInfoContainer.topActivity, activityThatFinishesImmediately);
+ private void waitUntil(BooleanSupplier condition) throws Exception {
+ while (!condition.getAsBoolean()) {
+ boolean didAquire =
+ mActivityLaunchSemaphore.tryAcquire(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!didAquire && !condition.getAsBoolean()) {
+ throw new RuntimeException("failed while waiting for condition to become true");
+ }
}
}
+ private boolean topTasksHasComponent(ComponentName component) {
+ for (TopTaskInfoContainer topTaskInfoContainer: mService.getTopTasks()) {
+ if (topTaskInfoContainer.topActivity.equals(component)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Activity that closes itself after some timeout to clean up the screen. */
public static class TempActivity extends Activity {
@Override
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
index 1ef35f7..1f0237f 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
@@ -28,6 +28,7 @@
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
+import android.support.test.filters.FlakyTest;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
@@ -47,6 +48,7 @@
@RunWith(AndroidJUnit4.class)
@MediumTest
+@FlakyTest // TODO(b/116333782): Remove the flag when issue fixed
public class VmsPublisherSubscriberTest extends MockedCarTestBase {
private static final int LAYER_ID = 88;
private static final int LAYER_VERSION = 19;
diff --git a/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java b/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
index a180886..8a27909 100644
--- a/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
@@ -64,7 +64,10 @@
* and connection results can be injected (imitating results from the stack)
* 2. {@link CarCabinService} & {@link CarSensorService} - Fake vehicle events are injected to the
* policy's Broadcast Receiver.
+ *
+ * TODO(b/77825248): Fix the flakiness to enable the test class again
*/
+@Suppress
public class BluetoothAutoConnectPolicyTest extends AndroidTestCase {
private BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicyTest;
private BluetoothAdapter mBluetoothAdapter;
diff --git a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
index f731bec..4757015 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
@@ -43,6 +43,7 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.sysprop.CarProperties;
import org.junit.Before;
import org.junit.Test;
@@ -109,7 +110,7 @@
// Restore the non-headless state before every test. Individual tests can set the property
// to true to test the headless system user scenario.
- SystemProperties.set("android.car.systemuser.headless", "false");
+ CarProperties.headless_system_user(false);
}
@Test
@@ -126,7 +127,7 @@
// System user will not be returned when calling get all users.
@Test
public void testHeadlessUser0GetAllUsers_NotReturnSystemUser() {
- SystemProperties.set("android.car.systemuser.headless", "true");
+ CarProperties.headless_system_user(true);
UserInfo otherUser1 = createUserInfoForId(10);
UserInfo otherUser2 = createUserInfoForId(11);
UserInfo otherUser3 = createUserInfoForId(12);
@@ -274,7 +275,7 @@
assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(11);
// In headless user 0 model, we want to exclude the system user.
- SystemProperties.set("android.car.systemuser.headless", "true");
+ CarProperties.headless_system_user(true);
assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(10);
}
@@ -318,7 +319,7 @@
@Test
public void testHeadlessSystemUser_IsUserLimitReached() {
- SystemProperties.set("android.car.systemuser.headless", "true");
+ CarProperties.headless_system_user(true);
UserInfo user1 = createUserInfoForId(10);
UserInfo user2 =
new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE);
@@ -772,7 +773,7 @@
@Test
public void testGetInitialUserWithValidLastActiveUser() {
- SystemProperties.set("android.car.systemuser.headless", "true");
+ CarProperties.headless_system_user(true);
int lastActiveUserId = 12;
UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2);
@@ -787,7 +788,7 @@
@Test
public void testGetInitialUserWithNonExistLastActiveUser() {
- SystemProperties.set("android.car.systemuser.headless", "true");
+ CarProperties.headless_system_user(true);
int lastActiveUserId = 12;
UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2);
diff --git a/tools/keventreader/server/Android.mk b/tools/keventreader/server/Android.mk
index 726e2fc..3dd6bba 100644
--- a/tools/keventreader/server/Android.mk
+++ b/tools/keventreader/server/Android.mk
@@ -42,6 +42,5 @@
LOCAL_MODULE_TAGS := optional
LOCAL_CFLAGS += -Wall -Werror
-LOCAL_CPPFLAGS += -std=c++17
include $(BUILD_EXECUTABLE)
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index 227b2fb..649c89d 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -29,10 +29,10 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
+import android.sysprop.CarProperties;
import android.util.Log;
import com.android.internal.util.UserIcons;
@@ -56,7 +56,6 @@
*/
public class CarUserManagerHelper {
private static final String TAG = "CarUserManagerHelper";
- private static final String HEADLESS_SYSTEM_USER = "android.car.systemuser.headless";
// Place holder for user name of the first user created.
public static final String DEFAULT_FIRST_ADMIN_NAME = "Driver";
@@ -259,7 +258,7 @@
* @return {@boolean true} if headless system user.
*/
public boolean isHeadlessSystemUser() {
- return SystemProperties.getBoolean(HEADLESS_SYSTEM_USER, false);
+ return CarProperties.headless_system_user().orElse(false);
}
/**