chromium/chromecast/media/cma/backend/android/volume_control_android.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/media/cma/backend/android/volume_control_android.h"

#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/android/build_info.h"
#include "base/android/jni_android.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_pump_type.h"
#include "base/no_destructor.h"
#include "chromecast/base/init_command_line_shlib.h"
#include "chromecast/chromecast_buildflags.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chromecast/media/cma/backend/android/audio_track_jni_headers/VolumeControl_jni.h"
#include "chromecast/media/cma/backend/android/audio_track_jni_headers/VolumeMap_jni.h"

namespace chromecast {
namespace media {

namespace {

bool IsSingleVolumeDevice() {
  JNIEnv* env = base::android::AttachCurrentThread();
  return Java_VolumeControl_isSingleVolumeDevice(env);
}

}  // namespace

VolumeControlAndroid& GetVolumeControl() {
  static base::NoDestructor<VolumeControlAndroid> volume_control;
  return *volume_control;
}

VolumeControlAndroid::VolumeControlAndroid()
    : is_single_volume_(IsSingleVolumeDevice()),
      thread_("VolumeControl"),
      initialize_complete_event_(
          base::WaitableEvent::ResetPolicy::MANUAL,
          base::WaitableEvent::InitialState::NOT_SIGNALED) {
  DCHECK(j_volume_control_.is_null());
  j_volume_control_.Reset(Java_VolumeControl_createVolumeControl(
      base::android::AttachCurrentThread(), reinterpret_cast<intptr_t>(this)));

  base::Thread::Options options;
  options.message_pump_type = base::MessagePumpType::IO;
  thread_.StartWithOptions(std::move(options));

  thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&VolumeControlAndroid::InitializeOnThread,
                                base::Unretained(this)));
  initialize_complete_event_.Wait();
}

VolumeControlAndroid::~VolumeControlAndroid() {}

void VolumeControlAndroid::AddVolumeObserver(VolumeObserver* observer) {
  base::AutoLock lock(observer_lock_);
  volume_observers_.push_back(observer);
}

void VolumeControlAndroid::RemoveVolumeObserver(VolumeObserver* observer) {
  base::AutoLock lock(observer_lock_);
  volume_observers_.erase(
      std::remove(volume_observers_.begin(), volume_observers_.end(), observer),
      volume_observers_.end());
}

float VolumeControlAndroid::GetVolume(AudioContentType type) {
  base::AutoLock lock(volume_lock_);
  // The return level needs to be in the kMedia (MUSIC) volume table domain.
  return MapIntoDifferentVolumeTableDomain(type, AudioContentType::kMedia,
                                           volumes_[type]);
}

void VolumeControlAndroid::SetVolume(VolumeChangeSource source,
                                     AudioContentType type,
                                     float level) {
  if (type == AudioContentType::kOther) {
    NOTREACHED_IN_MIGRATION() << "Can't set volume for content type kOther";
    return;
  }

  level = std::clamp(level, 0.0f, 1.0f);
  // The input level value is in the kMedia (MUSIC) volume table domain.
  float mapped_level =
      MapIntoDifferentVolumeTableDomain(AudioContentType::kMedia, type, level);
  thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&VolumeControlAndroid::SetVolumeOnThread,
                                base::Unretained(this), source, type,
                                mapped_level, false /* from_android */));
}

bool VolumeControlAndroid::IsMuted(AudioContentType type) {
  base::AutoLock lock(volume_lock_);
  return muted_[type];
}

void VolumeControlAndroid::SetMuted(VolumeChangeSource source,
                                    AudioContentType type,
                                    bool muted) {
  if (type == AudioContentType::kOther) {
    NOTREACHED_IN_MIGRATION() << "Can't set mute state for content type kOther";
    return;
  }

  thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&VolumeControlAndroid::SetMutedOnThread,
                                base::Unretained(this), source, type, muted,
                                false /* from_android */));
}

void VolumeControlAndroid::SetOutputLimit(AudioContentType type, float limit) {
  if (type == AudioContentType::kOther) {
    NOTREACHED_IN_MIGRATION()
        << "Can't set output limit for content type kOther";
    return;
  }

  // The input limit is in the kMedia (MUSIC) volume table domain.
  limit = std::clamp(limit, 0.0f, 1.0f);
  float limit_db = VolumeToDbFSCached(AudioContentType::kMedia, limit);
  AudioSinkManager::Get()->SetOutputLimitDb(type, limit_db);
}

void VolumeControlAndroid::OnVolumeChange(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    jint type,
    jfloat level) {
  thread_.task_runner()->PostTask(
      FROM_HERE,
      base::BindOnce(&VolumeControlAndroid::ReportVolumeChangeOnThread,
                     base::Unretained(this),
                     static_cast<AudioContentType>(type), level));
}

void VolumeControlAndroid::OnMuteChange(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    jint type,
    jboolean muted) {
  thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&VolumeControlAndroid::ReportMuteChangeOnThread,
                                base::Unretained(this),
                                static_cast<AudioContentType>(type), muted));
}

int VolumeControlAndroid::GetMaxVolumeIndex(AudioContentType type) {
  if (base::android::BuildInfo::GetInstance()->sdk_int() <
      base::android::SDK_VERSION_NOUGAT) {
    return 1;
  }
  return Java_VolumeMap_getMaxVolumeIndex(base::android::AttachCurrentThread(),
                                          static_cast<int>(type));
}

float VolumeControlAndroid::VolumeToDbFS(AudioContentType type, float volume) {
  if (base::android::BuildInfo::GetInstance()->sdk_int() <
      base::android::SDK_VERSION_NOUGAT) {
    return 1.0f;
  }
  return Java_VolumeMap_volumeToDbFs(base::android::AttachCurrentThread(),
                                     static_cast<int>(type), volume);
}

void VolumeControlAndroid::InitializeOnThread() {
  DCHECK(thread_.task_runner()->BelongsToCurrentThread());

  for (auto type :
       {AudioContentType::kMedia, AudioContentType::kAlarm,
        AudioContentType::kCommunication, AudioContentType::kOther}) {
    std::unique_ptr<VolumeCache> vc(new VolumeCache(type, this));
    volume_cache_.emplace(type, std::move(vc));

    volumes_[type] =
        Java_VolumeControl_getVolume(base::android::AttachCurrentThread(),
                                     j_volume_control_, static_cast<int>(type));
    float volume_db = VolumeToDbFSCached(type, volumes_[type]);
    AudioSinkManager::Get()->SetTypeVolumeDb(type, volume_db);
    muted_[type] =
        Java_VolumeControl_isMuted(base::android::AttachCurrentThread(),
                                   j_volume_control_, static_cast<int>(type));
    LOG(INFO) << __func__ << ": Initial values for"
              << " type=" << static_cast<int>(type) << ": "
              << " volume=" << volumes_[type] << " (" << volume_db << ")"
              << " mute=" << muted_[type];
  }

  // kOther is not used on Android, so we don't attempt to set it to 100% here.

  initialize_complete_event_.Signal();
}

void VolumeControlAndroid::SetVolumeOnThread(VolumeChangeSource source,
                                             AudioContentType type,
                                             float level,
                                             bool from_android) {
  // Note: |level| is in the |type| volume table domain.
  DCHECK(thread_.task_runner()->BelongsToCurrentThread());
  {
    base::AutoLock lock(volume_lock_);
    if (level == volumes_[type]) {
      return;
    }
    volumes_[type] = level;
  }

  float level_db = VolumeToDbFSCached(type, level);
  LOG(INFO) << __func__ << ": level=" << level << " (" << level_db << ")";
  // Provide the type volume to the sink manager so it can properly calculate
  // the limiter multiplier. The volume is *not* applied by the sink though.
  AudioSinkManager::Get()->SetTypeVolumeDb(type, level_db);

  // Set proper volume in Android OS.
  if (!from_android) {
    Java_VolumeControl_setVolume(base::android::AttachCurrentThread(),
                                 j_volume_control_, static_cast<int>(type),
                                 level);
  }

  // Report new volume level to observers. Note that the reported value needs
  // to be in the kMedia (MUSIC) volume table domain.
  float media_level =
      MapIntoDifferentVolumeTableDomain(type, AudioContentType::kMedia, level);
  {
    base::AutoLock lock(observer_lock_);
    for (VolumeObserver* observer : volume_observers_) {
      observer->OnVolumeChange(source, type, media_level);
    }
  }
}

void VolumeControlAndroid::SetMutedOnThread(VolumeChangeSource source,
                                            AudioContentType type,
                                            bool muted,
                                            bool from_android) {
  DCHECK(thread_.task_runner()->BelongsToCurrentThread());
  {
    base::AutoLock lock(volume_lock_);
    if (muted == muted_[type]) {
      return;
    }
    muted_[type] = muted;
  }

  if (!from_android) {
    Java_VolumeControl_setMuted(base::android::AttachCurrentThread(),
                                j_volume_control_, static_cast<int>(type),
                                muted);
  }

  {
    base::AutoLock lock(observer_lock_);
    for (VolumeObserver* observer : volume_observers_) {
      observer->OnMuteChange(source, type, muted);
    }
  }
}

void VolumeControlAndroid::ReportVolumeChangeOnThread(AudioContentType type,
                                                      float level) {
  DCHECK(thread_.task_runner()->BelongsToCurrentThread());
  SetVolumeOnThread(VolumeChangeSource::kUser, type, level,
                    true /* from android */);
}

void VolumeControlAndroid::ReportMuteChangeOnThread(AudioContentType type,
                                                    bool muted) {
  DCHECK(thread_.task_runner()->BelongsToCurrentThread());
  SetMutedOnThread(VolumeChangeSource::kUser, type, muted,
                   true /* from_android */);
}

float VolumeControlAndroid::MapIntoDifferentVolumeTableDomain(
    AudioContentType from_type,
    AudioContentType to_type,
    float level) {
  if (from_type == to_type) {
    return level;
  }
  float from_db = VolumeToDbFSCached(from_type, level);
  return DbFSToVolumeCached(to_type, from_db);
}

float VolumeControlAndroid::VolumeToDbFSCached(AudioContentType type,
                                               float vol_level) {
  return volume_cache_[type]->VolumeToDbFS(vol_level);
}

float VolumeControlAndroid::DbFSToVolumeCached(AudioContentType type,
                                               float db) {
  return volume_cache_[type]->DbFSToVolume(db);
}

//
// Implementation of VolumeControl as defined in public/volume_control.h
//

// static
void VolumeControl::Initialize(const std::vector<std::string>& argv) {
  GetVolumeControl();
}

// static
void VolumeControl::Finalize() {
  // Nothing to do.
}

// static
void VolumeControl::AddVolumeObserver(VolumeObserver* observer) {
  GetVolumeControl().AddVolumeObserver(observer);
}

// static
void VolumeControl::RemoveVolumeObserver(VolumeObserver* observer) {
  GetVolumeControl().RemoveVolumeObserver(observer);
}

// static
float VolumeControl::GetVolume(AudioContentType type) {
  return GetVolumeControl().GetVolume(type);
}

// static
void VolumeControl::SetVolume(VolumeChangeSource source,
                              AudioContentType type,
                              float level) {
  GetVolumeControl().SetVolume(source, type, level);
}

// static
bool VolumeControl::IsMuted(AudioContentType type) {
  return GetVolumeControl().IsMuted(type);
}

// static
void VolumeControl::SetMuted(VolumeChangeSource source,
                             AudioContentType type,
                             bool muted) {
  GetVolumeControl().SetMuted(source, type, muted);
}

// static
void VolumeControl::SetOutputLimit(AudioContentType type, float limit) {
  GetVolumeControl().SetOutputLimit(type, limit);
}

// static
float VolumeControl::VolumeToDbFS(float volume) {
  // The volume value is the kMedia (MUSIC) volume table domain.
  return GetVolumeControl().VolumeToDbFSCached(AudioContentType::kMedia,
                                               volume);
}

// static
float VolumeControl::DbFSToVolume(float db) {
  // The db value is the kMedia (MUSIC) volume table domain.
  return GetVolumeControl().DbFSToVolumeCached(AudioContentType::kMedia, db);
}

}  // namespace media
}  // namespace chromecast