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&amp;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;