Merge "Adds unified car audio configuration"
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
index 1b251f8..7e8704e 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
@@ -24,6 +24,7 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.hardware.usb.UsbDevice;
import android.util.Log;
+
import java.util.ArrayList;
import java.util.List;
@@ -51,25 +52,23 @@
private Cursor queryFor(SQLiteDatabase db, UsbDevice device) {
String serial = device.getSerialNumber();
String selection;
- String[] selectionArgs;
+ List<String> selectionArgs = new ArrayList<>();
if (AoapInterface.isDeviceInAoapMode(device)) {
selection = COLUMN_SERIAL + " = ? AND " + COLUMN_AOAP + " = 1";
- selectionArgs = new String[] {serial};
+ selectionArgs.add(serial);
} else if (serial == null) {
- selection = COLUMN_SERIAL + " IS NULL AND "
- + COLUMN_VID + " = ? AND " + COLUMN_PID + " = ?";
- selectionArgs = new String[] {
- Integer.toString(device.getVendorId()),
- Integer.toString(device.getProductId())};
+ selection = COLUMN_SERIAL + " IS NULL";
} else {
- selection =
- COLUMN_SERIAL + " = ? AND " + COLUMN_VID + " = ? AND " + COLUMN_PID + " = ?";
- selectionArgs = new String[] {
- device.getSerialNumber(),
- Integer.toString(device.getVendorId()),
- Integer.toString(device.getProductId())};
+ selection = COLUMN_SERIAL + " = ?";
+ selectionArgs.add(serial);
}
- return db.query(TABLE_USB_SETTINGS, null, selection, selectionArgs, null, null, null);
+
+ selection += " AND " + COLUMN_VID + " = ? AND " + COLUMN_PID + " = ?";
+ selectionArgs.add(String.valueOf(device.getVendorId()));
+ selectionArgs.add(String.valueOf(device.getProductId()));
+
+ return db.query(TABLE_USB_SETTINGS, null, selection,
+ selectionArgs.toArray(new String[0]), null, null, null);
}
/**
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/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..bd53973 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -74,6 +74,13 @@
*/
public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
+ // 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 DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
private static final int[] CONTEXT_NUMBERS = new int[] {
@@ -205,6 +212,7 @@
};
private AudioPolicy mAudioPolicy;
+ private CarAudioFocus mFocusHandler;
private CarVolumeGroup[] mCarVolumeGroups;
public CarAudioService(Context context) {
@@ -247,6 +255,8 @@
if (mAudioPolicy != null) {
mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
mAudioPolicy = null;
+ mFocusHandler.setOwningPolicy(null, null);
+ mFocusHandler = null;
}
} else {
mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
@@ -401,13 +411,16 @@
private void setupDynamicRouting() {
final IAudioControl audioControl = getAudioControl();
if (audioControl == null) {
- return;
+ throw new RuntimeException(
+ "Dynamic routing requested but audioControl HAL not available");
}
+
AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl);
int r = mAudioManager.registerAudioPolicy(audioPolicy);
if (r != AudioManager.SUCCESS) {
throw new RuntimeException("registerAudioPolicy failed " + r);
}
+
mAudioPolicy = audioPolicy;
}
@@ -488,7 +501,7 @@
AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
builder.setLooper(Looper.getMainLooper());
- // 1st, enumerate all output bus device ports
+ // 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");
@@ -508,7 +521,7 @@
}
}
- // 2nd, map context to physical bus
+ // Map context to physical bus
try {
for (int contextNumber : CONTEXT_NUMBERS) {
int busNumber = audioControl.getBusForContext(contextNumber);
@@ -522,7 +535,7 @@
Log.e(CarLog.TAG_AUDIO, "Error mapping context to physical bus", e);
}
- // 3rd, enumerate all physical buses and build the routing policy.
+ // 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);
@@ -564,10 +577,36 @@
}
}
- // 4th, attach the {@link AudioPolicyVolumeCallback}
+ // Attach the {@link AudioPolicyVolumeCallback}
builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
- return builder.build();
+ 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);
+ }
+
+ // Instantiate the AudioPolicy
+ final AudioPolicy audioPolicy = builder.build();
+
+ if (sUseCarAudioFocus) {
+ // Connect the AudioPolicy and the focus listener
+ mFocusHandler.setOwningPolicy(this, audioPolicy);
+ }
+
+ // Send the completed AudioPolicy back for use
+ return audioPolicy;
+ }
+
+ // This is public so it can be used by our CarAudioFocus policy implementation, but since
+ // "context" is a HAL level concept, this API should not be visible through the
+ // CarAudioManager interface.
+ // Returns 0 (INVALID) context if an unrecognized audio usage is passed in.
+ public int getContextForUsage(int audioUsage) {
+ return USAGE_TO_CONTEXT.get(audioUsage);
}
private int[] getUsagesForContext(int contextNumber) {
@@ -784,7 +823,7 @@
for (int i = 0; i < mCarVolumeGroups.length; i++) {
int[] contexts = mCarVolumeGroups[i].getContexts();
for (int context : contexts) {
- if (USAGE_TO_CONTEXT.get(usage) == context) {
+ if (getContextForUsage(usage) == context) {
return i;
}
}
@@ -856,7 +895,7 @@
final CarVolumeGroup group = Preconditions.checkNotNull(mCarVolumeGroups[groupId],
"Can not find CarVolumeGroup by usage: "
+ AudioAttributes.usageToString(usage));
- return group.getAudioDevicePortForContext(USAGE_TO_CONTEXT.get(usage));
+ return group.getAudioDevicePortForContext(getContextForUsage(usage));
}
/**
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/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/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index 3b8b435..eb425bd 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -61,6 +61,7 @@
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;
@@ -170,6 +171,7 @@
});
add("activity view", ActivityViewTestFragment.class);
add("connectivity", ConnectivityFragment.class);
+ add("web links", WebLinksTestFragment.class);
add("quit", KitchenSinkActivity.this::finish);
}
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/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/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/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/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;