Bug 1196724 - Refactoring of AudioManager r=alwu
authorSotaro Ikeda <sotaro.ikeda.g@gmail.com>
Wed, 28 Oct 2015 07:11:05 -0700
changeset 269950 29c9e01ef7a297e35d42ea127e76f6b79377af59
parent 269949 f23234d57557fabc79e1635472de6ae8040fe4d8
child 269951 b6cf4c726f704d1e923e63ae93f9dc281db215bd
push id67218
push usersikeda@mozilla.com
push dateWed, 28 Oct 2015 14:11:15 +0000
treeherdermozilla-inbound@29c9e01ef7a2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersalwu
bugs1196724
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1196724 - Refactoring of AudioManager r=alwu
dom/system/gonk/AudioManager.cpp
dom/system/gonk/AudioManager.h
dom/system/gonk/android_audio/AudioSystem.h
--- a/dom/system/gonk/AudioManager.cpp
+++ b/dom/system/gonk/AudioManager.cpp
@@ -41,16 +41,17 @@
 #include "nsJSUtils.h"
 #include "nsThreadUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsComponentManagerUtils.h"
 #include "nsXULAppAPI.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/SettingChangeNotificationBinding.h"
 
+using namespace mozilla::dom;
 using namespace mozilla::dom::gonk;
 using namespace android;
 using namespace mozilla;
 using namespace mozilla::dom::bluetooth;
 
 #undef LOG
 #define LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "AudioManager" , ## args)
 
@@ -76,23 +77,57 @@ static const uint32_t sMaxStreamVolumeTb
   15,  // enforced audible
   15,  // DTMF
   15,  // TTS
 #if ANDROID_VERSION < 19
   15,  // FM
 #endif
 };
 
-// Use a half value of each volume category as the default volume.
-static const uint32_t sDefaultVolumeCategoriesTbl[VOLUME_TOTAL_NUMBER] = {
-  8, // VOLUME_MEDIA
-  8, // VOLUME_NOTIFICATION
-  8, // VOLUME_ALARM
-  3, // VOLUME_TELEPHONY
-  8, // VOLUME_BLUETOOTH_SCO
+static const uint32_t sDefaultStreamVolumeTbl[AUDIO_STREAM_CNT] = {
+  3,  // voice call
+  8,  // system
+  8,  // ring
+  8,  // music
+  8,  // alarm
+  8,  // notification
+  8,  // BT SCO
+  15, // enforced audible  // XXX Handle as fixed maximum audio setting
+  8,  // DTMF
+  8,  // TTS
+#if ANDROID_VERSION < 19
+  8,  // FM
+#endif
+};
+
+static const int32_t sStreamVolumeAliasTbl[AUDIO_STREAM_CNT] = {
+  AUDIO_STREAM_VOICE_CALL,      // voice call
+  AUDIO_STREAM_NOTIFICATION,    // system
+  AUDIO_STREAM_NOTIFICATION,    // ring
+  AUDIO_STREAM_MUSIC,           // music
+  AUDIO_STREAM_ALARM,           // alarm
+  AUDIO_STREAM_NOTIFICATION,    // notification
+  AUDIO_STREAM_BLUETOOTH_SCO,   // BT SCO
+  AUDIO_STREAM_ENFORCED_AUDIBLE,// enforced audible
+  AUDIO_STREAM_DTMF,            // DTMF
+  AUDIO_STREAM_TTS,             // TTS
+#if ANDROID_VERSION < 19
+  AUDIO_STREAM_MUSIC,           // FM
+#endif
+};
+
+static const uint32_t sChannelStreamTbl[NUMBER_OF_AUDIO_CHANNELS] = {
+  AUDIO_STREAM_MUSIC,           // AudioChannel::Normal
+  AUDIO_STREAM_MUSIC,           // AudioChannel::Content
+  AUDIO_STREAM_NOTIFICATION,    // AudioChannel::Notification
+  AUDIO_STREAM_ALARM,           // AudioChannel::Alarm
+  AUDIO_STREAM_VOICE_CALL,      // AudioChannel::Telephony
+  AUDIO_STREAM_RING,            // AudioChannel::Ringer
+  AUDIO_STREAM_ENFORCED_AUDIBLE,// AudioChannel::Publicnotification
+  AUDIO_STREAM_SYSTEM,          // AudioChannel::System
 };
 
 // Mappings AudioOutputProfiles to strings.
 static const nsAttrValue::EnumTable kAudioOutputProfilesTable[] = {
   { "primary",   DEVICE_PRIMARY },
   { "headset",   DEVICE_HEADSET },
   { "bluetooth", DEVICE_BLUETOOTH },
   { nullptr }
@@ -101,22 +136,36 @@ static const nsAttrValue::EnumTable kAud
 static const int kBtSampleRate = 8000;
 
 typedef MozPromise<bool, const char*, true> VolumeInitPromise;
 
 namespace mozilla {
 namespace dom {
 namespace gonk {
 
-static const VolumeData gVolumeData[VOLUME_TOTAL_NUMBER] = {
-  {"audio.volume.content",      VOLUME_MEDIA},
-  {"audio.volume.notification", VOLUME_NOTIFICATION},
-  {"audio.volume.alarm",        VOLUME_ALARM},
-  {"audio.volume.telephony",    VOLUME_TELEPHONY},
-  {"audio.volume.bt_sco",       VOLUME_BLUETOOTH_SCO}
+/**
+ * We have five sound volume settings from UX spec,
+ * You can see more informations in Bug1068219.
+ * (1) Media : music, video, FM ...
+ * (2) Notification : ringer, notification ...
+ * (3) Alarm : alarm
+ * (4) Telephony : GSM call, WebRTC call
+ * (5) Bluetooth SCO : SCO call
+ **/
+struct VolumeData {
+  const char* mChannelName;
+  int32_t mStreamType;
+};
+
+static const VolumeData gVolumeData[] = {
+  {"audio.volume.content",      AUDIO_STREAM_MUSIC},
+  {"audio.volume.notification", AUDIO_STREAM_NOTIFICATION},
+  {"audio.volume.alarm",        AUDIO_STREAM_ALARM},
+  {"audio.volume.telephony",    AUDIO_STREAM_VOICE_CALL},
+  {"audio.volume.bt_sco",       AUDIO_STREAM_BLUETOOTH_SCO}
 };
 
 class RunnableCallTask : public Task
 {
 public:
   explicit RunnableCallTask(nsIRunnable* aRunnable)
     : mRunnable(aRunnable) {}
 
@@ -140,90 +189,84 @@ GetSettingServiceLock()
   nsCOMPtr<nsISettingsServiceLock> lock;
   rv = service->CreateLock(nullptr, getter_AddRefs(lock));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return nullptr;
   }
   return lock.forget();
 }
 
-class AudioProfileData final
+#if ANDROID_VERSION >= 21
+class GonkAudioPortCallback : public AudioSystem::AudioPortCallback
 {
 public:
-  explicit AudioProfileData(AudioOutputProfiles aProfile)
-    : mProfile(aProfile)
-    , mActive(false)
+  virtual void onAudioPortListUpdate()
   {
-    for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
-      mVolumeTable.AppendElement(0);
-    }
-  };
-
-  AudioOutputProfiles GetProfile() const
-  {
-    return mProfile;
+    nsCOMPtr<nsIRunnable> runnable =
+      NS_NewRunnableFunction([]() {
+        MOZ_ASSERT(NS_IsMainThread());
+        RefPtr<AudioManager> audioManager = AudioManager::GetInstance();
+        NS_ENSURE_TRUE(audioManager.get(), );
+        audioManager->UpdateCachedActiveDevicesForStreams();
+        audioManager->MaybeUpdateVolumeSettingToDatabase();
+      });
+    NS_DispatchToMainThread(runnable);
   }
-
-  void SetActive(bool aActive)
-  {
-    mActive = aActive;
-  }
-
-  bool GetActive() const
-  {
-    return mActive;
-  }
-
-  nsTArray<uint32_t> mVolumeTable;
-private:
-  const AudioOutputProfiles mProfile;
-  bool mActive;
+  virtual void onAudioPatchListUpdate() { }
+  virtual void onServiceDied() { }
 };
+#endif
 
 void
 AudioManager::HandleAudioFlingerDied()
 {
+  //Disable volume change notification
+  mIsVolumeInited = false;
+
   uint32_t attempt;
   for (attempt = 0; attempt < 50; attempt++) {
     if (defaultServiceManager()->checkService(String16(AUDIO_POLICY_SERVICE_NAME)) != 0) {
       break;
     }
-
     LOG("AudioPolicyService is dead! attempt=%d", attempt);
     usleep(1000 * 200);
   }
 
   MOZ_RELEASE_ASSERT(attempt < 50);
 
-  for (uint32_t loop = 0; loop < AUDIO_STREAM_CNT; ++loop) {
-    AudioSystem::initStreamVolume(static_cast<audio_stream_type_t>(loop),
-                                  0,
-                                  sMaxStreamVolumeTbl[loop]);
-    uint32_t index;
-    GetStreamVolumeIndex(loop, &index);
-    SetStreamVolumeIndex(loop, index);
+  // Indicate to audio HAL that we start the reconfiguration phase after a media
+  // server crash
+  AudioSystem::setParameters(0, String8("restarting=true"));
+
+  // Restore device connection states
+  SetAllDeviceConnectionStates();
+
+  // Restore call state
+#if ANDROID_VERSION < 17
+  AudioSystem::setPhoneState(mPhoneState);
+#else
+  AudioSystem::setPhoneState(static_cast<audio_mode_t>(mPhoneState));
+#endif
+
+  // Restore master volume
+  AudioSystem::setMasterVolume(1.0);
+
+  // Restore stream volumes
+  for (uint32_t streamType = 0; streamType < AUDIO_STREAM_CNT; ++streamType) {
+    mStreamStates[streamType]->InitStreamVolume();
+    mStreamStates[streamType]->RestoreVolumeIndexToAllDevices();
   }
 
-  if (mHeadsetState & AUDIO_DEVICE_OUT_WIRED_HEADSET) {
-    UpdateHeadsetConnectionState(hal::SWITCH_STATE_HEADSET);
-  } else if (mHeadsetState & AUDIO_DEVICE_OUT_WIRED_HEADPHONE) {
-    UpdateHeadsetConnectionState(hal::SWITCH_STATE_HEADPHONE);
-  } else {
-    UpdateHeadsetConnectionState(hal::SWITCH_STATE_OFF);
-  }
+  // Indicate the end of reconfiguration phase to audio HAL
+  AudioSystem::setParameters(0, String8("restarting=true"));
 
-  int32_t phoneState = nsIAudioManager::PHONE_STATE_INVALID;
-  GetPhoneState(&phoneState);
-#if ANDROID_VERSION < 17
-  AudioSystem::setPhoneState(phoneState);
-#else
-  AudioSystem::setPhoneState(static_cast<audio_mode_t>(phoneState));
-#endif
-
-  AudioSystem::get_audio_flinger();
+  // Enable volume change notification
+  mIsVolumeInited = true;
+  mAudioOutProfileUpdated = 0;
+  MaybeUpdateVolumeSettingToDatabase(true);
 }
 
 class VolumeInitCallback final : public nsISettingsServiceCallback
 {
 public:
   NS_DECL_ISUPPORTS
 
   VolumeInitCallback()
@@ -236,33 +279,32 @@ public:
   {
     return mPromise;
   }
 
   NS_IMETHOD Handle(const nsAString& aName, JS::Handle<JS::Value> aResult)
   {
     RefPtr<AudioManager> audioManager = AudioManager::GetInstance();
     MOZ_ASSERT(audioManager);
-    for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
+    for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
       NS_ConvertASCIItoUTF16 volumeType(gVolumeData[idx].mChannelName);
       if (StringBeginsWith(aName, volumeType)) {
         AudioOutputProfiles profile = GetProfileFromSettingName(aName);
         MOZ_ASSERT(profile != DEVICE_ERROR);
-
-        uint32_t category = gVolumeData[idx].mCategory;
+        int32_t stream = gVolumeData[idx].mStreamType;
         uint32_t volIndex = aResult.isInt32() ?
-                  aResult.toInt32() : sDefaultVolumeCategoriesTbl[category];
-        nsresult rv = audioManager->ValidateVolumeIndex(category, volIndex);
+                  aResult.toInt32() : sDefaultStreamVolumeTbl[stream];
+        nsresult rv = audioManager->ValidateVolumeIndex(stream, volIndex);
         if (NS_WARN_IF(NS_FAILED(rv))) {
           mPromiseHolder.Reject("Error : invalid volume index.", __func__);
           return rv;
         }
 
-        audioManager->InitProfileVolume(profile, category, volIndex);
-        if (++mInitCounter == DEVICE_TOTAL_NUMBER * VOLUME_TOTAL_NUMBER) {
+        audioManager->InitVolumeForProfile(profile, stream, volIndex);
+        if (++mInitCounter == DEVICE_TOTAL_NUMBER * MOZ_ARRAY_LENGTH(gVolumeData)) {
           mPromiseHolder.Resolve(true, __func__);
         }
         return NS_OK;
       }
     }
     mPromiseHolder.Reject("Error : unexpected audio init event.", __func__);
     return NS_OK;
   }
@@ -307,137 +349,179 @@ BinderDeadCallback(status_t aErr)
       RefPtr<AudioManager> audioManager = AudioManager::GetInstance();
       NS_ENSURE_TRUE(audioManager.get(), );
       audioManager->HandleAudioFlingerDied();
     });
 
   NS_DispatchToMainThread(runnable);
 }
 
-static bool
-IsDeviceOn(audio_devices_t device)
+bool
+AudioManager::IsFmOutConnected()
 {
-#if ANDROID_VERSION >= 15
-  return AudioSystem::getDeviceConnectionState(device, "") ==
-           AUDIO_POLICY_DEVICE_STATE_AVAILABLE;
-#else
-  return false;
-#endif
+  return mConnectedDevices.Get(AUDIO_DEVICE_OUT_FM, nullptr);
 }
 
 NS_IMPL_ISUPPORTS(AudioManager, nsIAudioManager, nsIObserver)
 
 void
+AudioManager::AudioOutProfileUpdated(AudioOutputProfiles aProfile)
+{
+  mAudioOutProfileUpdated |= (1 << aProfile);
+}
+
+void
 AudioManager::UpdateHeadsetConnectionState(hal::SwitchState aState)
 {
+  bool headphoneConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADPHONE,
+                                                  nullptr);
+  bool headsetConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADSET,
+                                                nullptr);
+  if (aState == hal::SWITCH_STATE_HEADSET) {
+    UpdateDeviceConnectionState(true,
+                                AUDIO_DEVICE_OUT_WIRED_HEADSET,
+                                NS_LITERAL_CSTRING(""));
+  } else if (aState == hal::SWITCH_STATE_HEADPHONE) {
+    UpdateDeviceConnectionState(true,
+                                AUDIO_DEVICE_OUT_WIRED_HEADPHONE,
+                                NS_LITERAL_CSTRING(""));
+  } else if (aState == hal::SWITCH_STATE_OFF) {
+    if (headsetConnected) {
+      UpdateDeviceConnectionState(false,
+                                  AUDIO_DEVICE_OUT_WIRED_HEADSET,
+                                  NS_LITERAL_CSTRING(""));
+    }
+    if (headphoneConnected) {
+      UpdateDeviceConnectionState(false,
+                                  AUDIO_DEVICE_OUT_WIRED_HEADPHONE,
+                                  NS_LITERAL_CSTRING(""));
+    }
+  }
+}
+
+void
+AudioManager::UpdateDeviceConnectionState(bool aIsConnected, uint32_t aDevice, const nsCString& aDeviceName)
+{
 #if ANDROID_VERSION >= 15
-  if (aState == hal::SWITCH_STATE_HEADSET) {
-    AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_WIRED_HEADSET,
-                                          AUDIO_POLICY_DEVICE_STATE_AVAILABLE, "");
-    mHeadsetState |= AUDIO_DEVICE_OUT_WIRED_HEADSET;
-  } else if (aState == hal::SWITCH_STATE_HEADPHONE) {
-    AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_WIRED_HEADPHONE,
-                                          AUDIO_POLICY_DEVICE_STATE_AVAILABLE, "");
-    mHeadsetState |= AUDIO_DEVICE_OUT_WIRED_HEADPHONE;
-  } else if (aState == hal::SWITCH_STATE_OFF) {
-    if (mHeadsetState & AUDIO_DEVICE_OUT_WIRED_HEADSET) {
-      AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_WIRED_HEADSET,
-                                            AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, "");
-    }
-    if (mHeadsetState & AUDIO_DEVICE_OUT_WIRED_HEADPHONE) {
-      AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_WIRED_HEADPHONE,
-                                            AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, "");
-    }
-    mHeadsetState = 0;
+  bool isConnected = mConnectedDevices.Get(aDevice, nullptr);
+  if (isConnected && !aIsConnected) {
+    AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice),
+                                          AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE,
+                                          aDeviceName.get());
+    mConnectedDevices.Remove(aDevice);
+  } else if(!isConnected && aIsConnected) {
+    AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice),
+                                          AUDIO_POLICY_DEVICE_STATE_AVAILABLE,
+                                          aDeviceName.get());
+    mConnectedDevices.Put(aDevice, aDeviceName);
   }
-
+#if ANDROID_VERSION < 21
+  // Manually call it, since AudioPortCallback is not supported.
+  // Current volumes might be changed by updating active devices in android
+  // AudioPolicyManager.
+  MaybeUpdateVolumeSettingToDatabase();
+#endif
 #else
   NS_NOTREACHED("Doesn't support audio routing on GB version");
 #endif
 }
 
 void
+AudioManager::SetAllDeviceConnectionStates()
+{
+  for (auto iter = mConnectedDevices.Iter(); !iter.Done(); iter.Next()) {
+    const uint32_t& device = iter.Key();
+    nsCString& deviceAddress = iter.Data();
+    AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(device),
+                                          AUDIO_POLICY_DEVICE_STATE_AVAILABLE,
+                                          deviceAddress.get());
+  }
+#if ANDROID_VERSION < 21
+  // Manually call it, since AudioPortCallback is not supported.
+  // Current volumes might be changed by updating active devices in android
+  // AudioPolicyManager.
+  MaybeUpdateVolumeSettingToDatabase(true);
+#endif
+}
+
+void
 AudioManager::HandleBluetoothStatusChanged(nsISupports* aSubject,
                                            const char* aTopic,
                                            const nsCString aAddress)
 {
 #ifdef MOZ_B2G_BT
-  bool status;
+  bool isConnected = false;
   if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) {
     BluetoothHfpManagerBase* hfp =
       static_cast<BluetoothHfpManagerBase*>(aSubject);
-    status = hfp->IsScoConnected();
+    isConnected = hfp->IsScoConnected();
   } else {
     BluetoothProfileManagerBase* profile =
       static_cast<BluetoothProfileManagerBase*>(aSubject);
-    status = profile->IsConnected();
+    isConnected = profile->IsConnected();
   }
 
-  audio_policy_dev_state_t audioState = status ?
-    AUDIO_POLICY_DEVICE_STATE_AVAILABLE :
-    AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE;
-
   if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) {
-    if (audioState == AUDIO_POLICY_DEVICE_STATE_AVAILABLE) {
+    if (isConnected) {
       String8 cmd;
       cmd.appendFormat("bt_samplerate=%d", kBtSampleRate);
       AudioSystem::setParameters(0, cmd);
       SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_BT_SCO);
-      SwitchProfileData(DEVICE_BLUETOOTH, true);
     } else {
       int32_t force;
       GetForceForUse(nsIAudioManager::USE_COMMUNICATION, &force);
       if (force == nsIAudioManager::FORCE_BT_SCO) {
         SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_NONE);
       }
-      SwitchProfileData(DEVICE_BLUETOOTH, false);
     }
   } else if (!strcmp(aTopic, BLUETOOTH_A2DP_STATUS_CHANGED_ID)) {
-    if (audioState == AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE && mA2dpSwitchDone) {
+    if (!isConnected && mA2dpSwitchDone) {
       RefPtr<AudioManager> self = this;
       nsCOMPtr<nsIRunnable> runnable =
-        NS_NewRunnableFunction([self, audioState, aAddress]() {
+        NS_NewRunnableFunction([self, isConnected, aAddress]() {
           if (self->mA2dpSwitchDone) {
             return;
           }
-          AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_BLUETOOTH_A2DP,
-                                                audioState,
-                                                aAddress.get());
+          self->UpdateDeviceConnectionState(isConnected,
+                                            AUDIO_DEVICE_OUT_BLUETOOTH_A2DP,
+                                            aAddress);
+
           String8 cmd("bluetooth_enabled=false");
           AudioSystem::setParameters(0, cmd);
           cmd.setTo("A2dpSuspended=true");
           AudioSystem::setParameters(0, cmd);
           self->mA2dpSwitchDone = true;
         });
       MessageLoop::current()->PostDelayedTask(
         FROM_HERE, new RunnableCallTask(runnable), 1000);
 
       mA2dpSwitchDone = false;
-      SwitchProfileData(DEVICE_BLUETOOTH, false);
     } else {
-      AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_BLUETOOTH_A2DP,
-                                            audioState, aAddress.get());
+      UpdateDeviceConnectionState(isConnected,
+                                  AUDIO_DEVICE_OUT_BLUETOOTH_A2DP,
+                                  aAddress);
       String8 cmd("bluetooth_enabled=true");
       AudioSystem::setParameters(0, cmd);
       cmd.setTo("A2dpSuspended=false");
       AudioSystem::setParameters(0, cmd);
       mA2dpSwitchDone = true;
-      SwitchProfileData(DEVICE_BLUETOOTH, true);
 #if ANDROID_VERSION >= 17
       if (AudioSystem::getForceUse(AUDIO_POLICY_FORCE_FOR_MEDIA) == AUDIO_POLICY_FORCE_NO_BT_A2DP) {
         SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE);
       }
 #endif
     }
-    mBluetoothA2dpEnabled = audioState == AUDIO_POLICY_DEVICE_STATE_AVAILABLE;
+    mBluetoothA2dpEnabled = isConnected;
   } else if (!strcmp(aTopic, BLUETOOTH_HFP_STATUS_CHANGED_ID)) {
-    AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET,
-                                          audioState, aAddress.get());
-    AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET,
-                                          audioState, aAddress.get());
+    UpdateDeviceConnectionState(isConnected,
+                                AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET,
+                                aAddress);
+    UpdateDeviceConnectionState(isConnected,
+                                AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET,
+                                aAddress);
   } else if (!strcmp(aTopic, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID)) {
       String8 cmd;
       BluetoothHfpManagerBase* hfp =
           static_cast<BluetoothHfpManagerBase*>(aSubject);
       if (hfp->IsNrecEnabled()) {
           cmd.setTo("bt_headset_name=<unknown>;bt_headset_nrec=on");
           AudioSystem::setParameters(0, cmd);
       } else {
@@ -508,25 +592,19 @@ AudioManager::Observe(nsISupports* aSubj
     if (!StringBeginsWith(setting.mKey, NS_LITERAL_STRING("audio.volume."))) {
       return NS_OK;
     }
     if (!setting.mValue.isNumber()) {
       return NS_OK;
     }
 
     uint32_t volIndex = setting.mValue.toNumber();
-    for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
+    for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
       if (setting.mKey.EqualsASCII(gVolumeData[idx].mChannelName)) {
-        SetVolumeByCategory(gVolumeData[idx].mCategory, volIndex);
-        nsCOMPtr<nsISettingsServiceLock> lock = GetSettingServiceLock();
-        UpdateVolumeSettingToDatabase(lock.get(),
-                                      AppendProfileToVolumeSetting(
-                                        gVolumeData[idx].mChannelName,
-                                        mPresentProfile).get(),
-                                      volIndex);
+        SetStreamVolumeIndex(gVolumeData[idx].mStreamType, volIndex);
         return NS_OK;
       }
     }
   }
 
   NS_WARNING("Unexpected topic in AudioManager");
   return NS_ERROR_FAILURE;
 }
@@ -570,73 +648,74 @@ AudioManager::HandleHeadphoneSwitchEvent
       NS_NewRunnableFunction([self]() {
         if (self->mSwitchDone) {
           return;
         }
         self->UpdateHeadsetConnectionState(hal::SWITCH_STATE_OFF);
         self->mSwitchDone = true;
     });
     MessageLoop::current()->PostDelayedTask(FROM_HERE, new RunnableCallTask(runnable), 1000);
-
-    SwitchProfileData(DEVICE_HEADSET, false);
     mSwitchDone = false;
   } else if (aEvent.status() != hal::SWITCH_STATE_OFF) {
     UpdateHeadsetConnectionState(aEvent.status());
-    SwitchProfileData(DEVICE_HEADSET, true);
     mSwitchDone = true;
   }
   // Handle the coexistence of a2dp / headset device, latest one wins.
 #if ANDROID_VERSION >= 17
   int32_t forceUse = 0;
   GetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, &forceUse);
   if (aEvent.status() != hal::SWITCH_STATE_OFF && mBluetoothA2dpEnabled) {
     SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NO_BT_A2DP);
   } else if (forceUse == AUDIO_POLICY_FORCE_NO_BT_A2DP) {
     SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE);
   }
 #endif
 }
 
 AudioManager::AudioManager()
   : mPhoneState(PHONE_STATE_CURRENT)
-  , mHeadsetState(0)
+  , mIsVolumeInited(false)
+  , mAudioOutProfileUpdated(0)
   , mSwitchDone(true)
 #if defined(MOZ_B2G_BT) || ANDROID_VERSION >= 17
   , mBluetoothA2dpEnabled(false)
 #endif
 #ifdef MOZ_B2G_BT
   , mA2dpSwitchDone(true)
 #endif
   , mObserver(new HeadphoneSwitchObserver())
 #ifdef MOZ_B2G_RIL
   , mMuteCallToRIL(false)
 #endif
 {
-  hal::RegisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver);
+  AudioSystem::setErrorCallback(BinderDeadCallback);
+#if ANDROID_VERSION >= 21
+  android::sp<GonkAudioPortCallback> callback = new GonkAudioPortCallback();
+  AudioSystem::setAudioPortCallback(callback);
+#endif
 
+  // Create VolumeStreamStates
+  for (uint32_t loop = 0; loop < AUDIO_STREAM_CNT; ++loop) {
+    VolumeStreamState* streamState =
+      new VolumeStreamState(*this, static_cast<audio_stream_type_t>(loop));
+    mStreamStates.AppendElement(streamState);
+  }
+  UpdateCachedActiveDevicesForStreams();
+
+  RegisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver);
+  // Initialize headhone/heaset status
   UpdateHeadsetConnectionState(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES));
   NotifyHeadphonesStatus(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES));
 
-  for (uint32_t loop = 0; loop < AUDIO_STREAM_CNT; ++loop) {
-    AudioSystem::initStreamVolume(static_cast<audio_stream_type_t>(loop), 0,
-                                  sMaxStreamVolumeTbl[loop]);
-    mCurrentStreamVolumeTbl[loop] = sMaxStreamVolumeTbl[loop];
-  }
-  // Force publicnotification to output at maximal volume
-  SetStreamVolumeIndex(AUDIO_STREAM_ENFORCED_AUDIBLE,
-                       sMaxStreamVolumeTbl[AUDIO_STREAM_ENFORCED_AUDIBLE]);
-  CreateAudioProfilesData();
-
   // Get the initial volume index from settings DB during boot up.
   InitVolumeFromDatabase();
 
   // Gecko only control stream volume not master so set to default value
   // directly.
   AudioSystem::setMasterVolume(1.0);
-  AudioSystem::setErrorCallback(BinderDeadCallback);
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   NS_ENSURE_TRUE_VOID(obs);
   if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID, false))) {
     NS_WARNING("Failed to add bluetooth sco status changed observer!");
   }
   if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID, false))) {
     NS_WARNING("Failed to add bluetooth a2dp status changed observer!");
@@ -659,16 +738,20 @@ AudioManager::AudioManager()
   property_get("ro.moz.mute.call.to_ril", value, "false");
   if (!strcmp(value, "true")) {
     mMuteCallToRIL = true;
   }
 #endif
 }
 
 AudioManager::~AudioManager() {
+  AudioSystem::setErrorCallback(nullptr);
+#if ANDROID_VERSION >= 21
+  AudioSystem::setAudioPortCallback(nullptr);
+#endif
   hal::UnregisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver);
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   NS_ENSURE_TRUE_VOID(obs);
   if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID))) {
     NS_WARNING("Failed to remove bluetooth sco status changed observer!");
   }
   if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID))) {
@@ -767,27 +850,39 @@ AudioManager::SetPhoneState(int32_t aSta
 #if ANDROID_VERSION < 17
   if (AudioSystem::setPhoneState(aState)) {
 #else
   if (AudioSystem::setPhoneState(static_cast<audio_mode_t>(aState))) {
 #endif
     return NS_ERROR_FAILURE;
   }
 
+#if ANDROID_VERSION < 21
+  // Manually call it, since AudioPortCallback is not supported.
+  // Current volumes might be changed by updating active devices in android
+  // AudioPolicyManager.
+  MaybeUpdateVolumeSettingToDatabase();
+#endif
   mPhoneState = aState;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AudioManager::SetForceForUse(int32_t aUsage, int32_t aForce)
 {
 #if ANDROID_VERSION >= 15
   status_t status = AudioSystem::setForceUse(
                       (audio_policy_force_use_t)aUsage,
                       (audio_policy_forced_cfg_t)aForce);
+#if ANDROID_VERSION < 21
+  // Manually call it, since AudioPortCallback is not supported.
+  // Current volumes might be changed by updating active devices in android
+  // AudioPolicyManager.
+  MaybeUpdateVolumeSettingToDatabase();
+#endif
   return status ? NS_ERROR_FAILURE : NS_OK;
 #else
   NS_NOTREACHED("Doesn't support force routing on GB version");
   return NS_ERROR_UNEXPECTED;
 #endif
 }
 
 NS_IMETHODIMP
@@ -799,318 +894,172 @@ AudioManager::GetForceForUse(int32_t aUs
   NS_NOTREACHED("Doesn't support force routing on GB version");
   return NS_ERROR_UNEXPECTED;
 #endif
 }
 
 NS_IMETHODIMP
 AudioManager::GetFmRadioAudioEnabled(bool *aFmRadioAudioEnabled)
 {
-  *aFmRadioAudioEnabled = IsDeviceOn(AUDIO_DEVICE_OUT_FM);
+  *aFmRadioAudioEnabled = IsFmOutConnected();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AudioManager::SetFmRadioAudioEnabled(bool aFmRadioAudioEnabled)
 {
-  AudioSystem::setDeviceConnectionState(AUDIO_DEVICE_OUT_FM,
-    aFmRadioAudioEnabled ? AUDIO_POLICY_DEVICE_STATE_AVAILABLE :
-    AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, "");
-  UpdateHeadsetConnectionState(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES));
+  UpdateDeviceConnectionState(aFmRadioAudioEnabled,
+                              AUDIO_DEVICE_OUT_FM,
+                              NS_LITERAL_CSTRING(""));
   // AUDIO_STREAM_FM is not used on recent gonk.
   // AUDIO_STREAM_MUSIC is used for FM radio volume control.
 #if ANDROID_VERSION < 19
   // sync volume with music after powering on fm radio
   if (aFmRadioAudioEnabled) {
-    uint32_t volIndex = mCurrentStreamVolumeTbl[AUDIO_STREAM_MUSIC];
-    SetStreamVolumeIndex(AUDIO_STREAM_FM, volIndex);
-    mCurrentStreamVolumeTbl[AUDIO_STREAM_FM] = volIndex;
+    uint32_t volIndex = mStreamStates[AUDIO_STREAM_MUSIC]->GetVolumeIndex();
+    nsresult rv = mStreamStates[AUDIO_STREAM_FM]->
+      SetVolumeIndex(volIndex, AUDIO_DEVICE_OUT_FM);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
   }
 #endif
   return NS_OK;
 }
 
+NS_IMETHODIMP
+AudioManager::SetAudioChannelVolume(uint32_t aChannel, uint32_t aIndex)
+{
+  if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  return SetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex);
+}
+
+NS_IMETHODIMP
+AudioManager::GetAudioChannelVolume(uint32_t aChannel, uint32_t* aIndex)
+{
+  if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  if (!aIndex) {
+    return NS_ERROR_NULL_POINTER;
+  }
+
+  return GetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex);
+}
+
+NS_IMETHODIMP
+AudioManager::GetMaxAudioChannelVolume(uint32_t aChannel, uint32_t* aMaxIndex)
+{
+  if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  if (!aMaxIndex) {
+    return NS_ERROR_NULL_POINTER;
+  }
+
+  *aMaxIndex = mStreamStates[sChannelStreamTbl[aChannel]]->GetMaxIndex();
+   return NS_OK;
+}
+
 nsresult
-AudioManager::ValidateVolumeIndex(uint32_t aCategory, uint32_t aIndex) const
+AudioManager::ValidateVolumeIndex(int32_t aStream, uint32_t aIndex) const
 {
-  uint32_t maxIndex = GetMaxVolumeByCategory(aCategory);
+  if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  uint32_t maxIndex = mStreamStates[aStream]->GetMaxIndex();
   if (aIndex > maxIndex) {
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 nsresult
-AudioManager::SetVolumeByCategory(uint32_t aCategory, uint32_t aIndex)
+AudioManager::SetStreamVolumeForProfile(AudioOutputProfiles aProfile,
+                                        int32_t aStream,
+                                        uint32_t aIndex)
 {
-  nsresult status;
-  switch (static_cast<AudioVolumeCategories>(aCategory)) {
-    case VOLUME_MEDIA:
-      // AUDIO_STREAM_FM is not used on recent gonk.
-      // AUDIO_STREAM_MUSIC is used for FM radio volume control.
-#if ANDROID_VERSION < 19
-      // sync FMRadio's volume with content channel.
-      if (IsDeviceOn(AUDIO_DEVICE_OUT_FM)) {
-        status = SetStreamVolumeIndex(AUDIO_STREAM_FM, aIndex);
-        if (NS_WARN_IF(NS_FAILED(status))) {
-          return status;
-        }
-      }
-#endif
-      status = SetStreamVolumeIndex(AUDIO_STREAM_MUSIC, aIndex);
+  if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  int32_t streamAlias = sStreamVolumeAliasTbl[aStream];
+  VolumeStreamState* state = mStreamStates[streamAlias].get();
+  // Rescaling of index is not necessary.
+  switch (aProfile) {
+    case DEVICE_PRIMARY:
+      state->SetVolumeIndexToAliasStreams(aIndex, AUDIO_DEVICE_OUT_SPEAKER);
       break;
-    case VOLUME_NOTIFICATION:
-      status = SetStreamVolumeIndex(AUDIO_STREAM_NOTIFICATION, aIndex);
-      if (NS_WARN_IF(NS_FAILED(status))) {
-        return status;
-      }
-      status = SetStreamVolumeIndex(AUDIO_STREAM_RING, aIndex);
-      if (NS_WARN_IF(NS_FAILED(status))) {
-        return status;
-      }
-      status = SetStreamVolumeIndex(AUDIO_STREAM_SYSTEM, aIndex);
+    case DEVICE_HEADSET:
+      state->SetVolumeIndexToAliasStreams(aIndex, AUDIO_DEVICE_OUT_WIRED_HEADSET);
       break;
-    case VOLUME_ALARM:
-      status = SetStreamVolumeIndex(AUDIO_STREAM_ALARM, aIndex);
-      break;
-    case VOLUME_TELEPHONY:
-      status = SetStreamVolumeIndex(AUDIO_STREAM_VOICE_CALL, aIndex);
-    case VOLUME_BLUETOOTH_SCO:
-      status = SetStreamVolumeIndex(AUDIO_STREAM_BLUETOOTH_SCO, aIndex);
+    case DEVICE_BLUETOOTH:
+      state->SetVolumeIndexToAliasStreams(aIndex, AUDIO_DEVICE_OUT_BLUETOOTH_A2DP);
       break;
     default:
-      return NS_ERROR_INVALID_ARG;
-  }
-  return status;
-}
-
-uint32_t
-AudioManager::GetVolumeByCategory(uint32_t aCategory) const
-{
-  switch (static_cast<AudioVolumeCategories>(aCategory)) {
-    case VOLUME_MEDIA:
-      return mCurrentStreamVolumeTbl[AUDIO_STREAM_MUSIC];
-    case VOLUME_NOTIFICATION:
-      MOZ_ASSERT(mCurrentStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION] ==
-                 mCurrentStreamVolumeTbl[AUDIO_STREAM_RING]);
-      MOZ_ASSERT(mCurrentStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION] ==
-                 mCurrentStreamVolumeTbl[AUDIO_STREAM_SYSTEM]);
-      return mCurrentStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION];
-    case VOLUME_ALARM:
-      return mCurrentStreamVolumeTbl[AUDIO_STREAM_ALARM];
-    case VOLUME_TELEPHONY:
-      return mCurrentStreamVolumeTbl[AUDIO_STREAM_VOICE_CALL];
-    case VOLUME_BLUETOOTH_SCO:
-      return mCurrentStreamVolumeTbl[AUDIO_STREAM_BLUETOOTH_SCO];
-    default:
-      NS_WARNING("Can't get volume from error volume category.");
-      return 0;
-  }
-}
-
-uint32_t
-AudioManager::GetMaxVolumeByCategory(uint32_t aCategory) const
-{
-  switch (static_cast<AudioVolumeCategories>(aCategory)) {
-    case VOLUME_MEDIA:
-      return sMaxStreamVolumeTbl[AUDIO_STREAM_MUSIC];
-    case VOLUME_NOTIFICATION:
-      MOZ_ASSERT(sMaxStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION] ==
-                 sMaxStreamVolumeTbl[AUDIO_STREAM_RING]);
-      MOZ_ASSERT(sMaxStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION] ==
-                 sMaxStreamVolumeTbl[AUDIO_STREAM_SYSTEM]);
-      return sMaxStreamVolumeTbl[AUDIO_STREAM_NOTIFICATION];
-    case VOLUME_ALARM:
-      return sMaxStreamVolumeTbl[AUDIO_STREAM_ALARM];
-    case VOLUME_TELEPHONY:
-      return sMaxStreamVolumeTbl[AUDIO_STREAM_VOICE_CALL];
-    case VOLUME_BLUETOOTH_SCO:
-      return sMaxStreamVolumeTbl[AUDIO_STREAM_BLUETOOTH_SCO];
-    default:
-      NS_WARNING("Can't get max volume from error volume category.");
-      return 0;
-  }
-}
-
-NS_IMETHODIMP
-AudioManager::SetAudioChannelVolume(uint32_t aChannel, uint32_t aIndex)
-{
-  nsresult status;
-  AudioVolumeCategories category = (mPresentProfile == DEVICE_BLUETOOTH) ?
-                                    VOLUME_BLUETOOTH_SCO : VOLUME_TELEPHONY;
-  switch (static_cast<AudioChannel>(aChannel)) {
-    case AudioChannel::Normal:
-    case AudioChannel::Content:
-      status = SetVolumeByCategory(VOLUME_MEDIA, aIndex);
       break;
-    case AudioChannel::Notification:
-    case AudioChannel::Ringer:
-    case AudioChannel::Publicnotification:
-    case AudioChannel::System:
-      status = SetVolumeByCategory(VOLUME_NOTIFICATION, aIndex);
-      break;
-    case AudioChannel::Alarm:
-      status = SetVolumeByCategory(VOLUME_ALARM, aIndex);
-      break;
-    case AudioChannel::Telephony:
-      status = SetVolumeByCategory(category, aIndex);
-      break;
-    default:
-      return NS_ERROR_INVALID_ARG;
-  }
-  return status;
-}
-
-NS_IMETHODIMP
-AudioManager::GetAudioChannelVolume(uint32_t aChannel, uint32_t* aIndex)
-{
-  if (!aIndex) {
-    return NS_ERROR_NULL_POINTER;
-  }
-  AudioVolumeCategories category = (mPresentProfile == DEVICE_BLUETOOTH) ?
-                                    VOLUME_BLUETOOTH_SCO : VOLUME_TELEPHONY;
-  switch (static_cast<AudioChannel>(aChannel)) {
-    case AudioChannel::Normal:
-    case AudioChannel::Content:
-      *aIndex = GetVolumeByCategory(VOLUME_MEDIA);
-      break;
-    case AudioChannel::Notification:
-    case AudioChannel::Ringer:
-    case AudioChannel::Publicnotification:
-    case AudioChannel::System:
-      *aIndex = GetVolumeByCategory(VOLUME_NOTIFICATION);
-      break;
-    case AudioChannel::Alarm:
-      *aIndex = GetVolumeByCategory(VOLUME_ALARM);
-      break;
-    case AudioChannel::Telephony:
-      *aIndex = GetVolumeByCategory(category);
-      break;
-    default:
-      return NS_ERROR_INVALID_ARG;
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AudioManager::GetMaxAudioChannelVolume(uint32_t aChannel, uint32_t* aMaxIndex)
-{
-  if (!aMaxIndex) {
-    return NS_ERROR_NULL_POINTER;
-  }
-  AudioVolumeCategories category = (mPresentProfile == DEVICE_BLUETOOTH) ?
-                                    VOLUME_BLUETOOTH_SCO : VOLUME_TELEPHONY;
-  switch (static_cast<AudioChannel>(aChannel)) {
-    case AudioChannel::Normal:
-    case AudioChannel::Content:
-      *aMaxIndex = GetMaxVolumeByCategory(VOLUME_MEDIA);
-      break;
-    case AudioChannel::Notification:
-    case AudioChannel::Ringer:
-    case AudioChannel::Publicnotification:
-    case AudioChannel::System:
-      *aMaxIndex = GetMaxVolumeByCategory(VOLUME_NOTIFICATION);
-      break;
-    case AudioChannel::Alarm:
-      *aMaxIndex = GetMaxVolumeByCategory(VOLUME_ALARM);
-      break;
-    case AudioChannel::Telephony:
-      *aMaxIndex = GetMaxVolumeByCategory(category);
-      break;
-    default:
-      return NS_ERROR_INVALID_ARG;
   }
   return NS_OK;
 }
 
 nsresult
-AudioManager::SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex) {
-  if (aIndex > sMaxStreamVolumeTbl[aStream]) {
+AudioManager::SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex)
+{
+  if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) {
     return NS_ERROR_INVALID_ARG;
   }
-  mCurrentStreamVolumeTbl[aStream] = aIndex;
-  status_t status;
-#if ANDROID_VERSION < 17
-   status = AudioSystem::setStreamVolumeIndex(
-              static_cast<audio_stream_type_t>(aStream),
-              aIndex);
-   return status ? NS_ERROR_FAILURE : NS_OK;
-#else
+
+  int32_t streamAlias = sStreamVolumeAliasTbl[aStream];
+
+  nsresult rv;
+  for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) {
+    if (streamAlias == sStreamVolumeAliasTbl[streamType]) {
+      rv = mStreamStates[streamType]->SetVolumeIndexToActiveDevices(aIndex);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+  }
+
+  // AUDIO_STREAM_FM is not used on recent gonk.
+  // AUDIO_STREAM_MUSIC is used for FM radio volume control.
 #if ANDROID_VERSION < 19
-  if (aStream == AUDIO_STREAM_FM) {
-    status = AudioSystem::setStreamVolumeIndex(
-               static_cast<audio_stream_type_t>(aStream),
-               aIndex,
-               AUDIO_DEVICE_OUT_FM);
-    return status ? NS_ERROR_FAILURE : NS_OK;
+  if (streamAlias == AUDIO_STREAM_MUSIC && IsFmOutConnected()) {
+    rv = mStreamStates[AUDIO_STREAM_FM]->
+      SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_FM);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
   }
 #endif
-  if (mPresentProfile == DEVICE_PRIMARY) {
-    status = AudioSystem::setStreamVolumeIndex(
-            static_cast<audio_stream_type_t>(aStream),
-            aIndex,
-            AUDIO_DEVICE_OUT_SPEAKER);
-    status += AudioSystem::setStreamVolumeIndex(
-            static_cast<audio_stream_type_t>(aStream),
-            aIndex,
-            AUDIO_DEVICE_OUT_EARPIECE);
-  } else if (mPresentProfile == DEVICE_HEADSET) {
-    status = AudioSystem::setStreamVolumeIndex(
-              static_cast<audio_stream_type_t>(aStream),
-              aIndex,
-              AUDIO_DEVICE_OUT_WIRED_HEADSET);
-    status += AudioSystem::setStreamVolumeIndex(
-              static_cast<audio_stream_type_t>(aStream),
-              aIndex,
-              AUDIO_DEVICE_OUT_WIRED_HEADPHONE);
-  } else if (mPresentProfile == DEVICE_BLUETOOTH) {
-    status = AudioSystem::setStreamVolumeIndex(
-             static_cast<audio_stream_type_t>(aStream),
-             aIndex,
-             AUDIO_DEVICE_OUT_BLUETOOTH_A2DP);
-    status += AudioSystem::setStreamVolumeIndex(
-              static_cast<audio_stream_type_t>(aStream),
-              aIndex,
-              AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET);
-  } else {
-    NS_WARNING("Can't set stream volume on error profile!");
-  }
-  return status ? NS_ERROR_FAILURE : NS_OK;
-#endif
+
+  return NS_OK;
 }
 
 nsresult
-AudioManager::GetStreamVolumeIndex(int32_t aStream, uint32_t *aIndex) {
+AudioManager::GetStreamVolumeIndex(int32_t aStream, uint32_t *aIndex)
+{
   if (!aIndex) {
     return NS_ERROR_INVALID_ARG;
   }
 
   if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) {
     return NS_ERROR_INVALID_ARG;
   }
 
-  *aIndex = mCurrentStreamVolumeTbl[aStream];
-
+  *aIndex = mStreamStates[aStream]->GetVolumeIndex();
   return NS_OK;
 }
 
-AudioProfileData*
-AudioManager::FindAudioProfileData(AudioOutputProfiles aProfile)
-{
-  uint32_t profilesNum = mAudioProfiles.Length();
-  MOZ_ASSERT(profilesNum == DEVICE_TOTAL_NUMBER, "Error profile numbers!");
-  for (uint32_t idx = 0; idx < profilesNum; ++idx) {
-    if (mAudioProfiles[idx]->GetProfile() == aProfile) {
-      return mAudioProfiles[idx];
-    }
-  }
-  NS_WARNING("Can't find audio profile data");
-  return nullptr;
-}
-
 nsAutoCString
 AudioManager::AppendProfileToVolumeSetting(const char* aName, AudioOutputProfiles aProfile)
 {
   nsAutoCString topic;
   topic.Assign(aName);
   for (uint32_t idx = 0; kAudioOutputProfilesTable[idx].tag; ++idx) {
     if (kAudioOutputProfilesTable[idx].value == aProfile) {
       topic.Append(".");
@@ -1137,158 +1086,435 @@ AudioManager::InitVolumeFromDatabase()
   }
 
   RefPtr<VolumeInitCallback> callback = new VolumeInitCallback();
   MOZ_ASSERT(callback);
   callback->GetPromise()->Then(AbstractThread::MainThread(), __func__, this,
                                &AudioManager::InitProfileVolumeSucceeded,
                                &AudioManager::InitProfileVolumeFailed);
 
-  for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
-    for (uint32_t pIdx = 0; pIdx < DEVICE_TOTAL_NUMBER; ++pIdx) {
+  for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
+    for (uint32_t profile = 0; profile < DEVICE_TOTAL_NUMBER; ++profile) {
       lock->Get(AppendProfileToVolumeSetting(gVolumeData[idx].mChannelName,
-                  static_cast<AudioOutputProfiles>(pIdx)).get(), callback);
+                  static_cast<AudioOutputProfiles>(profile)).get(), callback);
     }
   }
 }
 
 void
 AudioManager::InitProfileVolumeSucceeded()
 {
-  SendVolumeChangeNotification(FindAudioProfileData(mPresentProfile));
+  mIsVolumeInited = true;
+  MaybeUpdateVolumeSettingToDatabase(true);
 }
 
 void
 AudioManager::InitProfileVolumeFailed(const char* aError)
 {
+  // Initialize stream volumes with default values
+  for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
+    for (int32_t profile = 0; profile < DEVICE_TOTAL_NUMBER; ++profile) {
+      int32_t stream = gVolumeData[idx].mStreamType;
+      uint32_t volIndex = sDefaultStreamVolumeTbl[stream];
+      InitVolumeForProfile(static_cast<AudioOutputProfiles>(profile),
+                           stream,
+                           volIndex);
+    }
+  }
+  mIsVolumeInited = true;
+  MaybeUpdateVolumeSettingToDatabase(true);
   NS_WARNING(aError);
 }
 
 void
-AudioManager::UpdateVolumeSettingToDatabase(nsISettingsServiceLock* aLock,
-                                            const char* aTopic,
-                                            uint32_t aVolIndex)
+AudioManager::MaybeUpdateVolumeSettingToDatabase(bool aForce)
 {
-  MOZ_ASSERT(aLock);
+  if (!mIsVolumeInited) {
+    return;
+  }
 
+  nsCOMPtr<nsISettingsServiceLock> lock = GetSettingServiceLock();
+  if (NS_WARN_IF(!lock)) {
+    return;
+  }
+
+  // Send events to update the Gaia volumes
   mozilla::AutoSafeJSContext cx;
   JS::Rooted<JS::Value> value(cx);
-  value.setInt32(aVolIndex);
-  aLock->Set(aTopic, value, nullptr, nullptr);
-}
+  uint32_t volume = 0;
+  for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
+    int32_t streamType = gVolumeData[idx].mStreamType;
+    VolumeStreamState* streamState = mStreamStates[streamType].get();
+    if(!aForce && !streamState->IsDevicesChanged()) {
+      continue;
+    }
+    // Get volume index of active device.
+    volume = streamState->GetVolumeIndex();
+    value.setInt32(volume);
+    lock->Set(gVolumeData[idx].mChannelName, value, nullptr, nullptr);
+  }
 
-void
-AudioManager::SendVolumeChangeNotification(AudioProfileData* aProfileData)
-{
-  MOZ_ASSERT(aProfileData);
+  // Clear device changed flags
+  for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
+    int32_t  streamType = gVolumeData[idx].mStreamType;
+    mStreamStates[streamType]->ClearDevicesChanged();
+  }
+
+  if (!mAudioOutProfileUpdated) {
+    return;
+  }
+
+  // For reducing the code dependency, Gaia doesn't need to know the
+  // profile volume, it only need to care about different volume categories.
+  // However, we need to send the setting volume to the permanent database,
+  // so that we can store the volume setting even if the phone reboots.
 
-  // Change the value of the current volume setting, so that the Gaia can get
-  // correct volume values and update the volume UI. In addition, for reducing
-  // the code dependency, Gaia doesn't need to know the current profile, it
-  // only need to care about different volume categories.
-  nsCOMPtr<nsISettingsServiceLock> lock = GetSettingServiceLock();
-  for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
-    uint32_t volSetting = gVolumeData[idx].mCategory;
-    UpdateVolumeSettingToDatabase(lock.get(),
-                                  gVolumeData[idx].mChannelName,
-                                  aProfileData->mVolumeTable[volSetting]);
+  for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) {
+    int32_t  streamType = gVolumeData[idx].mStreamType;
+    VolumeStreamState* streamState = mStreamStates[streamType].get();
+
+    if (mAudioOutProfileUpdated & (1 << DEVICE_PRIMARY)) {
+      volume = streamState->GetVolumeIndex(AUDIO_DEVICE_OUT_SPEAKER);
+      value.setInt32(volume);
+      lock->Set(AppendProfileToVolumeSetting(
+                  gVolumeData[idx].mChannelName,
+                  DEVICE_PRIMARY).get(),
+                  value, nullptr, nullptr);
+    }
+    if (mAudioOutProfileUpdated & (1 << DEVICE_HEADSET)) {
+      volume = streamState->GetVolumeIndex(AUDIO_DEVICE_OUT_WIRED_HEADSET);
+      value.setInt32(volume);
+      lock->Set(AppendProfileToVolumeSetting(
+                  gVolumeData[idx].mChannelName,
+                  DEVICE_HEADSET).get(),
+                  value, nullptr, nullptr);
+    }
+    if (mAudioOutProfileUpdated & (1 << DEVICE_BLUETOOTH)) {
+      volume = streamState->GetVolumeIndex(AUDIO_DEVICE_OUT_BLUETOOTH_A2DP);
+      value.setInt32(volume);
+      lock->Set(AppendProfileToVolumeSetting(
+                  gVolumeData[idx].mChannelName,
+                  DEVICE_BLUETOOTH).get(),
+                  value, nullptr, nullptr);
+    }
   }
-}
 
-void
-AudioManager::CreateAudioProfilesData()
-{
-  MOZ_ASSERT(mAudioProfiles.IsEmpty(), "mAudioProfiles should be empty!");
-  for (uint32_t idx = 0; idx < DEVICE_TOTAL_NUMBER; ++idx) {
-    AudioProfileData* profile = new AudioProfileData(static_cast<AudioOutputProfiles>(idx));
-    mAudioProfiles.AppendElement(profile);
-  }
-  UpdateProfileState(DEVICE_PRIMARY, true);
+  // Clear mAudioOutProfileUpdated
+  mAudioOutProfileUpdated = 0;
 }
 
 void
-AudioManager::InitProfileVolume(AudioOutputProfiles aProfile,
-                                 uint32_t aCategory,
-                                 uint32_t aIndex)
-{
-  AudioProfileData* profileData = FindAudioProfileData(aProfile);
-  MOZ_ASSERT(profileData);
-  profileData->mVolumeTable[aCategory] = aIndex;
-  SetVolumeByCategory(aCategory, aIndex);
-}
-
-void
-AudioManager::SwitchProfileData(AudioOutputProfiles aProfile,
-                                bool aActive)
+AudioManager::InitVolumeForProfile(AudioOutputProfiles aProfile,
+                                   int32_t aStreamType,
+                                   uint32_t aIndex)
 {
-  MOZ_ASSERT(DEVICE_PRIMARY <= aProfile &&
-             aProfile < DEVICE_TOTAL_NUMBER, "Error profile type!");
-
-  // Save the present profile volume data.
-  AudioOutputProfiles oldProfile = mPresentProfile;
-  AudioProfileData* profileData = FindAudioProfileData(oldProfile);
-  MOZ_ASSERT(profileData);
-  UpdateVolumeToProfile(profileData);
-  UpdateProfileState(aProfile, aActive);
-
-  AudioOutputProfiles newProfile = mPresentProfile;
-  if (oldProfile == newProfile) {
-    return;
-  }
-
-  // Update new profile volume data and send the changing event.
-  profileData = FindAudioProfileData(newProfile);
-  MOZ_ASSERT(profileData);
-  UpdateVolumeFromProfile(profileData);
-  SendVolumeChangeNotification(profileData);
-}
-
-void
-AudioManager::UpdateProfileState(AudioOutputProfiles aProfile, bool aActive)
-{
-  MOZ_ASSERT(DEVICE_PRIMARY <= aProfile && aProfile < DEVICE_TOTAL_NUMBER,
-             "Error profile type!");
-  if (aProfile == DEVICE_PRIMARY && !aActive) {
-    NS_WARNING("Can't turn off the primary profile!");
-    return;
-  }
-
-  mAudioProfiles[aProfile]->SetActive(aActive);
-  if (aActive) {
-    mPresentProfile = aProfile;
-    return;
-  }
-
-  // The primary profile has the lowest priority. We will check whether there
-  // are other profiles. The bluetooth and headset have the same priotity.
-  uint32_t profilesNum = mAudioProfiles.Length();
-  MOZ_ASSERT(profilesNum == DEVICE_TOTAL_NUMBER, "Error profile numbers!");
-  for (int32_t idx = profilesNum - 1; idx >= 0; --idx) {
-    if (mAudioProfiles[idx]->GetActive()) {
-      mPresentProfile = static_cast<AudioOutputProfiles>(idx);
-      break;
+  // Set volume to streams of aStreamType and devices of Profile.
+  for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) {
+    if (aStreamType == sStreamVolumeAliasTbl[streamType]) {
+      SetStreamVolumeForProfile(aProfile, streamType, aIndex);
     }
   }
 }
 
 void
-AudioManager::UpdateVolumeToProfile(AudioProfileData* aProfileData)
+AudioManager::UpdateCachedActiveDevicesForStreams()
+{
+  // This function updates cached active devices for streams.
+  // It is used for optimization of GetDevicesForStream() since L.
+  // AudioManager could know when active devices
+  // are changed in AudioPolicyManager by onAudioPortListUpdate().
+  // Except it, AudioManager normally do not need to ask AuidoPolicyManager
+  // about current active devices of streams and could use cached values.
+  // Before L, onAudioPortListUpdate() does not exist and GetDevicesForStream()
+  // does not use the cache. Therefore this function do nothing.
+#if ANDROID_VERSION >= 21
+  for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) {
+    // Update cached active devices of stream
+    mStreamStates[streamType]->IsDevicesChanged(false /* aFromCache */);
+  }
+#endif
+}
+
+uint32_t
+AudioManager::GetDevicesForStream(int32_t aStream, bool aFromCache)
+{
+#if ANDROID_VERSION >= 21
+  // Since Lollipop, devices update could be notified by AudioPortCallback.
+  // Cached values can be used if there is no update.
+  if (aFromCache) {
+    return mStreamStates[aStream]->GetLastDevices();
+  }
+#endif
+
+#if ANDROID_VERSION >= 17
+  audio_devices_t devices =
+    AudioSystem::getDevicesForStream(static_cast<audio_stream_type_t>(aStream));
+
+  return static_cast<uint32_t>(devices);
+#else
+  return AUDIO_DEVICE_OUT_DEFAULT;
+#endif
+}
+
+uint32_t
+AudioManager::GetDeviceForStream(int32_t aStream)
 {
-  MOZ_ASSERT(aProfileData);
-  for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
-    uint32_t volume = GetVolumeByCategory(gVolumeData[idx].mCategory);
-    aProfileData->mVolumeTable[gVolumeData[idx].mCategory] = volume;
+  uint32_t devices =
+    GetDevicesForStream(static_cast<audio_stream_type_t>(aStream));
+  uint32_t device = SelectDeviceFromDevices(devices);
+  return device;
+}
+
+/* static */ uint32_t
+AudioManager::SelectDeviceFromDevices(uint32_t aOutDevices)
+{
+  uint32_t device = aOutDevices;
+
+  // See android AudioService.getDeviceForStream().
+  // AudioPolicyManager expects it.
+  // See also android AudioPolicyManager::getDeviceForVolume().
+  if ((device & (device - 1)) != 0) {
+    // Multiple device selection.
+    if ((device & AUDIO_DEVICE_OUT_SPEAKER) != 0) {
+      device = AUDIO_DEVICE_OUT_SPEAKER;
+#if ANDROID_VERSION >= 21
+    } else if ((device & AUDIO_DEVICE_OUT_HDMI_ARC) != 0) {
+      device = AUDIO_DEVICE_OUT_HDMI_ARC;
+    } else if ((device & AUDIO_DEVICE_OUT_SPDIF) != 0) {
+      device = AUDIO_DEVICE_OUT_SPDIF;
+    } else if ((device & AUDIO_DEVICE_OUT_AUX_LINE) != 0) {
+       device = AUDIO_DEVICE_OUT_AUX_LINE;
+#endif
+    } else {
+       device &= AUDIO_DEVICE_OUT_ALL_A2DP;
+    }
   }
+  return device;
+}
+AudioManager::VolumeStreamState::VolumeStreamState(AudioManager& aManager,
+                                                   int32_t aStreamType)
+  : mManager(aManager)
+  , mStreamType(aStreamType)
+  , mLastDevices(0)
+  , mIsDevicesChanged(true)
+{
+  InitStreamVolume();
+}
+
+bool
+AudioManager::VolumeStreamState::IsDevicesChanged(bool aFromCache)
+{
+  uint32_t devices = mManager.GetDevicesForStream(mStreamType, aFromCache);
+  if (devices != mLastDevices) {
+    mLastDevices = devices;
+    mIsDevicesChanged = true;
+  }
+  return mIsDevicesChanged;
+}
+
+void
+AudioManager::VolumeStreamState::ClearDevicesChanged()
+{
+  mIsDevicesChanged = false;
 }
 
 void
-AudioManager::UpdateVolumeFromProfile(AudioProfileData* aProfileData)
+AudioManager::VolumeStreamState::InitStreamVolume()
+{
+  AudioSystem::initStreamVolume(static_cast<audio_stream_type_t>(mStreamType),
+                                0,
+                                GetMaxIndex());
+}
+
+uint32_t
+AudioManager::VolumeStreamState::GetMaxIndex()
+{
+  return sMaxStreamVolumeTbl[mStreamType];
+}
+
+uint32_t
+AudioManager::VolumeStreamState::GetDefaultIndex()
+{
+  return sDefaultStreamVolumeTbl[mStreamType];
+}
+
+uint32_t
+AudioManager::VolumeStreamState::GetVolumeIndex()
+{
+  uint32_t device = mManager.GetDeviceForStream(mStreamType);
+  return GetVolumeIndex(device);
+}
+
+uint32_t
+AudioManager::VolumeStreamState::GetVolumeIndex(uint32_t aDevice)
+{
+  uint32_t index = 0;
+  bool ret = mVolumeIndexes.Get(aDevice, &index);
+  if (!ret) {
+    index = mVolumeIndexes.Get(AUDIO_DEVICE_OUT_DEFAULT);
+  }
+  return index;
+}
+
+nsresult
+AudioManager::VolumeStreamState::SetVolumeIndexToActiveDevices(uint32_t aIndex)
+{
+  uint32_t device = mManager.GetDeviceForStream(mStreamType);
+
+  // Update volume index for device
+  uint32_t oldVolumeIndex = 0;
+  bool exist = mVolumeIndexes.Get(device, &oldVolumeIndex);
+  if (exist && aIndex == oldVolumeIndex) {
+    // No update
+    return NS_OK;
+  }
+
+  // AudioPolicyManager::setStreamVolumeIndex() set volumes of all active
+  // devices for stream.
+  nsresult rv = SetVolumeIndexToAliasDevices(aIndex, device);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Workaround to make audio volume control consisitent.
+  // Active devices of AUDIO_STREAM_NOTIFICATION are affected by
+  // AUDIO_STREAM_MUSIC's activity. It makes audio volume control inconsistent.
+  // See Bug 1196724
+  if (device != AUDIO_DEVICE_OUT_SPEAKER &&
+      mStreamType == AUDIO_STREAM_NOTIFICATION) {
+      // Rescaling of index is not necessary.
+      rv = SetVolumeIndexToAliasDevices(aIndex, AUDIO_DEVICE_OUT_SPEAKER);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+AudioManager::VolumeStreamState::SetVolumeIndexToAliasStreams(uint32_t aIndex,
+                                                              uint32_t aDevice)
 {
-  MOZ_ASSERT(aProfileData);
-  for (uint32_t idx = 0; idx < VOLUME_TOTAL_NUMBER; ++idx) {
-    SetVolumeByCategory(gVolumeData[idx].mCategory,
-                        aProfileData->mVolumeTable[gVolumeData[idx].mCategory]);
+  uint32_t oldVolumeIndex = 0;
+  bool exist = mVolumeIndexes.Get(aDevice, &oldVolumeIndex);
+  if (exist && aIndex == oldVolumeIndex) {
+    // No update
+    return NS_OK;
+  }
+  nsresult rv = SetVolumeIndexToAliasDevices(aIndex, aDevice);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) {
+    if ((streamType != mStreamType) &&
+         sStreamVolumeAliasTbl[streamType] == mStreamType) {
+      // Rescaling of index is not necessary.
+      rv = mManager.mStreamStates[streamType]->
+        SetVolumeIndexToAliasStreams(aIndex, aDevice);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+  }
+  return NS_OK;
+}
+
+nsresult
+AudioManager::VolumeStreamState::SetVolumeIndexToAliasDevices(uint32_t aIndex,
+                                                              uint32_t aDevice)
+{
+#if ANDROID_VERSION >= 17
+  nsresult rv = NS_ERROR_FAILURE;
+  switch (aDevice) {
+    case AUDIO_DEVICE_OUT_EARPIECE:
+    case AUDIO_DEVICE_OUT_SPEAKER:
+      // Apply volume index of DEVICE_PRIMARY devices
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_EARPIECE);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_SPEAKER);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      mManager.AudioOutProfileUpdated(DEVICE_PRIMARY);
+      break;
+    case AUDIO_DEVICE_OUT_WIRED_HEADSET:
+    case AUDIO_DEVICE_OUT_WIRED_HEADPHONE:
+      // Apply volume index of DEVICE_HEADSET devices
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_WIRED_HEADSET);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_WIRED_HEADPHONE);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      mManager.AudioOutProfileUpdated(DEVICE_HEADSET);
+      break;
+    case AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET:
+    case AUDIO_DEVICE_OUT_BLUETOOTH_A2DP:
+      // Apply volume index of DEVICE_BLUETOOTH devices
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_BLUETOOTH_A2DP);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      mManager.AudioOutProfileUpdated(DEVICE_BLUETOOTH);
+      break;
+    default:
+      rv = SetVolumeIndex(aIndex, aDevice);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      break;
+  }
+#else
+  SetVolumeIndex(aIndex, aDevice);
+#endif
+  return NS_OK;
+}
+
+nsresult
+AudioManager::VolumeStreamState::SetVolumeIndex(uint32_t aIndex,
+                                                uint32_t aDevice,
+                                                bool aUpdateCache)
+{
+  status_t rv;
+#if ANDROID_VERSION >= 17
+  if (aUpdateCache) {
+    mVolumeIndexes.Put(aDevice, aIndex);
+  }
+
+  rv = AudioSystem::setStreamVolumeIndex(
+         static_cast<audio_stream_type_t>(mStreamType),
+         aIndex,
+         aDevice);
+  return rv ? NS_ERROR_FAILURE : NS_OK;
+#else
+  if (aUpdateCache) {
+    mVolumeIndexes.Put(AUDIO_DEVICE_OUT_DEFAULT, aIndex);
+  }
+  rv = AudioSystem::setStreamVolumeIndex(
+         static_cast<audio_stream_type_t>(mStreamType),
+         aIndex);
+  return rv ? NS_ERROR_FAILURE : NS_OK;
+#endif
+}
+
+void
+AudioManager::VolumeStreamState::RestoreVolumeIndexToAllDevices()
+{
+  for (auto iter = mVolumeIndexes.Iter(); !iter.Done(); iter.Next()) {
+    const uint32_t& key = iter.Key();
+    uint32_t& index = iter.Data();
+    SetVolumeIndex(key, index, /* aUpdateCache */ false);
   }
 }
 
 } /* namespace gonk */
 } /* namespace dom */
 } /* namespace mozilla */
--- a/dom/system/gonk/AudioManager.h
+++ b/dom/system/gonk/AudioManager.h
@@ -13,17 +13,19 @@
  * limitations under the License.
  */
 
 #ifndef mozilla_dom_system_b2g_audiomanager_h__
 #define mozilla_dom_system_b2g_audiomanager_h__
 
 #include "mozilla/HalTypes.h"
 #include "mozilla/Observer.h"
+#include "mozilla/UniquePtr.h"
 #include "nsAutoPtr.h"
+#include "nsDataHashtable.h"
 #include "nsIAudioManager.h"
 #include "nsIObserver.h"
 #include "android_audio/AudioSystem.h"
 
 // {b2b51423-502d-4d77-89b3-7786b562b084}
 #define NS_AUDIOMANAGER_CID {0x94f6fd70, 0x7615, 0x4af9, \
       {0x89, 0x10, 0xf9, 0x3c, 0x55, 0xe6, 0x62, 0xec}}
 #define NS_AUDIOMANAGER_CONTRACTID "@mozilla.org/telephony/audiomanager;1"
@@ -48,145 +50,152 @@ namespace gonk {
 enum AudioOutputProfiles {
   DEVICE_ERROR        = -1,
   DEVICE_PRIMARY      = 0,
   DEVICE_HEADSET      = 1,
   DEVICE_BLUETOOTH    = 2,
   DEVICE_TOTAL_NUMBER = 3,
 };
 
-/**
- * We have five sound volume settings from UX spec,
- * You can see more informations in Bug1068219.
- * (1) Media : music, video, FM ...
- * (2) Notification : ringer, notification ...
- * (3) Alarm : alarm
- * (4) Telephony : GSM call, WebRTC call
- * (5) Bluetooth SCO : SCO call
- **/
-enum AudioVolumeCategories {
-  VOLUME_MEDIA         = 0,
-  VOLUME_NOTIFICATION  = 1,
-  VOLUME_ALARM         = 2,
-  VOLUME_TELEPHONY     = 3,
-  VOLUME_BLUETOOTH_SCO = 4,
-  VOLUME_TOTAL_NUMBER  = 5,
-};
-
-struct VolumeData {
-  const char* mChannelName;
-  uint32_t mCategory;
-};
-
-class RecoverTask;
 class VolumeInitCallback;
-class AudioProfileData;
 
 class AudioManager final : public nsIAudioManager
                          , public nsIObserver
 {
 public:
   static already_AddRefed<AudioManager> GetInstance();
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIAUDIOMANAGER
   NS_DECL_NSIOBSERVER
 
-  // When audio backend is dead, recovery task needs to read all volume
-  // settings then set back into audio backend.
-  friend class RecoverTask;
-  friend class VolumeInitCallback;
-
-  // Open or close the specific profile
-  void SwitchProfileData(AudioOutputProfiles aProfile, bool aActive);
-
   // Validate whether the volume index is within the range
-  nsresult ValidateVolumeIndex(uint32_t aCategory, uint32_t aIndex) const;
+  nsresult ValidateVolumeIndex(int32_t aStream, uint32_t aIndex) const;
 
   // Called when android AudioFlinger in mediaserver is died
   void HandleAudioFlingerDied();
 
   void HandleHeadphoneSwitchEvent(const hal::SwitchEvent& aEvent);
 
+  class VolumeStreamState {
+  public:
+    explicit VolumeStreamState(AudioManager& aManager, int32_t aStreamType);
+    int32_t GetStreamType()
+    {
+      return mStreamType;
+    }
+    bool IsDevicesChanged(bool aFromCache = true);
+    void ClearDevicesChanged();
+    uint32_t GetLastDevices()
+    {
+      return mLastDevices;
+    }
+    void InitStreamVolume();
+    uint32_t GetMaxIndex();
+    uint32_t GetDefaultIndex();
+    uint32_t GetVolumeIndex();
+    uint32_t GetVolumeIndex(uint32_t aDevice);
+    void ClearCurrentVolumeUpdated();
+    // Set volume index to all active devices.
+    // Active devices are chosen by android AudioPolicyManager.
+    nsresult SetVolumeIndexToActiveDevices(uint32_t aIndex);
+    // Set volume index to all alias streams. Alias streams have same volume.
+    // It is used to update volume based on audio output profile data.
+    nsresult SetVolumeIndexToAliasStreams(uint32_t aIndex, uint32_t aDevice);
+    // Set volume index to all alias devices in audio output profile.
+    // Alias devices have same volume.
+    nsresult SetVolumeIndexToAliasDevices(uint32_t aIndex, uint32_t aDevice);
+    nsresult SetVolumeIndex(uint32_t aIndex, uint32_t aDevice, bool aUpdateCache = true);
+    // Restore volume index to all devices. Called when AudioFlinger is restarted.
+    void RestoreVolumeIndexToAllDevices();
+  private:
+    AudioManager& mManager;
+    const int32_t mStreamType;
+    uint32_t mLastDevices;
+    bool mIsDevicesChanged;
+    nsDataHashtable<nsUint32HashKey, uint32_t> mVolumeIndexes;
+  };
+
 protected:
   int32_t mPhoneState;
 
-  // A bitwise variable for recording what kind of headset/headphone is attached.
-  int32_t mHeadsetState;
+  bool mIsVolumeInited;
+
+  // A bitwise variable for volume update of audio output profiles
+  uint32_t mAudioOutProfileUpdated;
+
+  // Connected devices that are controlled by setDeviceConnectionState()
+  nsDataHashtable<nsUint32HashKey, nsCString> mConnectedDevices;
 
   bool mSwitchDone;
 
 #if defined(MOZ_B2G_BT) || ANDROID_VERSION >= 17
   bool mBluetoothA2dpEnabled;
 #endif
 #ifdef MOZ_B2G_BT
   bool mA2dpSwitchDone;
 #endif
-  uint32_t mCurrentStreamVolumeTbl[AUDIO_STREAM_CNT];
+  nsTArray<UniquePtr<VolumeStreamState> > mStreamStates;
+  uint32_t mLastChannelVolume[AUDIO_STREAM_CNT];
 
+  bool IsFmOutConnected();
+
+  nsresult SetStreamVolumeForProfile(AudioOutputProfiles aProfile,
+                                     int32_t aStream,
+                                     uint32_t aIndex);
   nsresult SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex);
   nsresult GetStreamVolumeIndex(int32_t aStream, uint32_t *aIndex);
 
+  void UpdateCachedActiveDevicesForStreams();
+  uint32_t GetDevicesForStream(int32_t aStream, bool aFromCache = true);
+  uint32_t GetDeviceForStream(int32_t aStream);
+  // Choose one device as representative of active devices.
+  static uint32_t SelectDeviceFromDevices(uint32_t aOutDevices);
+
 private:
   nsAutoPtr<mozilla::hal::SwitchObserver> mObserver;
 #ifdef MOZ_B2G_RIL
   bool                                    mMuteCallToRIL;
   // mIsMicMuted is only used for toggling mute call to RIL.
   bool                                    mIsMicMuted;
 #endif
-  nsTArray<nsAutoPtr<AudioProfileData>>   mAudioProfiles;
-  AudioOutputProfiles mPresentProfile;
 
   void HandleBluetoothStatusChanged(nsISupports* aSubject,
                                     const char* aTopic,
                                     const nsCString aAddress);
   void HandleAudioChannelProcessChanged();
 
-  void CreateAudioProfilesData();
-
-  // Init the volume setting from the init setting callback
-  void InitProfileVolume(AudioOutputProfiles aProfile,
-                        uint32_t aCatogory, uint32_t aIndex);
-
-  // Update volume data of profiles
-  void UpdateVolumeToProfile(AudioProfileData* aProfileData);
-
-  // Apply the volume data to device
-  void UpdateVolumeFromProfile(AudioProfileData* aProfileData);
+  // Initialize volume index for audio output profile
+  void InitVolumeForProfile(AudioOutputProfiles aProfile,
+                            int32_t aStreamType,
+                            uint32_t aIndex);
 
-  // Send the volume changing event to Gaia
-  void SendVolumeChangeNotification(AudioProfileData* aProfileData);
-
-  // Update the mPresentProfile and profiles active status
-  void UpdateProfileState(AudioOutputProfiles aProfile, bool aActive);
-
-  // Volume control functions
-  nsresult SetVolumeByCategory(uint32_t aCategory, uint32_t aIndex);
-  uint32_t GetVolumeByCategory(uint32_t aCategory) const;
-  uint32_t GetMaxVolumeByCategory(uint32_t aCategory) const;
-
-  AudioProfileData* FindAudioProfileData(AudioOutputProfiles aProfile);
-
-  // Append the profile to the volume setting string.
+  // Append the audio output profile to the volume setting string.
   nsAutoCString AppendProfileToVolumeSetting(const char* aName,
                                              AudioOutputProfiles aProfile);
 
   // We store the volume setting in the database, these are related functions.
   void InitVolumeFromDatabase();
-  void UpdateVolumeSettingToDatabase(nsISettingsServiceLock* aLock,
-                                     const char* aTopic,
-                                     uint32_t aVolIndex);
+  void MaybeUpdateVolumeSettingToDatabase(bool aForce = false);
 
   // Promise functions.
   void InitProfileVolumeSucceeded();
   void InitProfileVolumeFailed(const char* aError);
 
+  void AudioOutProfileUpdated(AudioOutputProfiles aProfile);
+
   void UpdateHeadsetConnectionState(hal::SwitchState aState);
+  void UpdateDeviceConnectionState(bool aIsConnected, uint32_t aDevice, const nsCString& aDeviceName);
+  void SetAllDeviceConnectionStates();
 
   AudioManager();
   ~AudioManager();
+
+  friend class VolumeInitCallback;
+  friend class VolumeStreamState;
+  friend class GonkAudioPortCallback;
 };
 
 } /* namespace gonk */
 } /* namespace dom */
 } /* namespace mozilla */
 
 #endif // mozilla_dom_system_b2g_audiomanager_h__
--- a/dom/system/gonk/android_audio/AudioSystem.h
+++ b/dom/system/gonk/android_audio/AudioSystem.h
@@ -964,16 +964,19 @@ public:
     static status_t getStreamVolumeIndex(audio_stream_type_t stream,
                                          int *index,
                                          audio_devices_t device);
 #endif
     static status_t getStreamVolumeIndex(stream_type stream, int *index);
     static status_t getStreamVolumeIndex(audio_stream_type_t stream, int *index);
 
     static uint32_t getStrategyForStream(stream_type stream);
+#if ANDROID_VERSION >= 17
+    static audio_devices_t getDevicesForStream(audio_stream_type_t stream);
+#endif
 
     static audio_io_handle_t getOutputForEffect(effect_descriptor_t *desc);
     static status_t registerEffect(effect_descriptor_t *desc,
                                     audio_io_handle_t output,
                                     uint32_t strategy,
                                     int session,
                                     int id);
     static status_t unregisterEffect(int id);
@@ -990,16 +993,33 @@ public:
     static bool isSeperatedStream(stream_type stream);
     static bool isLowVisibility(stream_type stream);
     static bool isOutputChannel(uint32_t channel);
     static bool isInputChannel(uint32_t channel);
     static bool isValidFormat(uint32_t format);
     static bool isLinearPCM(uint32_t format);
     static bool isModeInCall();
 
+#if ANDROID_VERSION >= 21
+    class AudioPortCallback : public RefBase
+    {
+    public:
+
+                AudioPortCallback() {}
+        virtual ~AudioPortCallback() {}
+
+        virtual void onAudioPortListUpdate() = 0;
+        virtual void onAudioPatchListUpdate() = 0;
+        virtual void onServiceDied() = 0;
+
+    };
+
+    static void setAudioPortCallback(sp<AudioPortCallback> callBack);
+#endif
+
 private:
 
     class AudioFlingerClient: public IBinder::DeathRecipient, public BnAudioFlingerClient
     {
     public:
         AudioFlingerClient() {
         }