audiopolicy: engine: use apm XML config file for volume curves
This CL removes the hard coded volume table in the engine and
use as a fallback for compatibility reason the apm config file
to load the volume tables.
Bug: 124767636
Test: dumpsys media.audio_policy & diff with previous version
Change-Id: I0392aad730c67ee79f898093fd1b4f64e26ab3fd
Signed-off-by: Francois Gaffie <francois.gaffie@renault.com>
diff --git a/services/audiopolicy/engine/common/include/VolumeCurve.h b/services/audiopolicy/engine/common/include/VolumeCurve.h
index c2a1da7..0ec63e1 100644
--- a/services/audiopolicy/engine/common/include/VolumeCurve.h
+++ b/services/audiopolicy/engine/common/include/VolumeCurve.h
@@ -182,8 +182,8 @@
private:
KeyedVector<device_category, sp<VolumeCurve> > mOriginVolumeCurves;
std::map<audio_devices_t, int> mIndexCur; /**< current volume index per device. */
- /*const*/ int mIndexMin; /**< min volume index. */
- /*const*/ int mIndexMax; /**< max volume index. */
+ int mIndexMin; /**< min volume index. */
+ int mIndexMax; /**< max volume index. */
const bool mCanBeMuted = true; /**< true is the stream can be muted. */
const audio_stream_type_t mStream; /**< Keep it for legacy. */
diff --git a/services/audiopolicy/engine/common/src/EngineBase.cpp b/services/audiopolicy/engine/common/src/EngineBase.cpp
index 2449b61..6e2ab4c 100644
--- a/services/audiopolicy/engine/common/src/EngineBase.cpp
+++ b/services/audiopolicy/engine/common/src/EngineBase.cpp
@@ -139,7 +139,10 @@
auto result = engineConfig::parse();
if (result.parsedConfig == nullptr) {
ALOGW("%s: No configuration found, using default matching phone experience.", __FUNCTION__);
- result = {std::make_unique<engineConfig::Config>(gDefaultEngineConfig), 0};
+ engineConfig::Config config = gDefaultEngineConfig;
+ android::status_t ret = engineConfig::parseLegacyVolumes(config.volumeGroups);
+ result = {std::make_unique<engineConfig::Config>(config),
+ static_cast<size_t>(ret == NO_ERROR ? 0 : 1)};
}
ALOGE_IF(result.nbSkippedElement != 0, "skipped %zu elements", result.nbSkippedElement);
loadProductStrategies(result.parsedConfig->productStrategies, mProductStrategies);
diff --git a/services/audiopolicy/engine/common/src/EngineDefaultConfig.h b/services/audiopolicy/engine/common/src/EngineDefaultConfig.h
index 86a59b0..f1642c5 100644
--- a/services/audiopolicy/engine/common/src/EngineDefaultConfig.h
+++ b/services/audiopolicy/engine/common/src/EngineDefaultConfig.h
@@ -131,131 +131,11 @@
}
};
-const engineConfig::VolumeGroups gVolumeGroups = {
- {"voice_call", "AUDIO_STREAM_VOICE_CALL", 1, 10,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -4200}, {33, -2800}, {66, -1400}, {100, 0} } },
- {"DEVICE_CATEGORY_SPEAKER", { {0, -2400}, {33, -1600}, {66, -800}, {100, 0} } },
- {"DEVICE_CATEGORY_EARPIECE", { {0, -2700}, {33, -1800}, {66, -900}, {100, 0} } },
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } },
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } },
- },
- },
- {"system", "AUDIO_STREAM_SYSTEM", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -3000}, {33, -2600}, {66, -2200}, {100, -1800} } },
- {"DEVICE_CATEGORY_SPEAKER", { {1, -5100}, {57, -2800}, {71, -2500}, {85, -2300}, {100, -2100} } },
- {"DEVICE_CATEGORY_EARPIECE", { {1, -2400}, {33, -1800}, {66, -1200}, {100, -600} } },
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"ring", "AUDIO_STREAM_RING", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_HEADSET_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } },
- {"DEVICE_CATEGORY_EARPIECE", { {1, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_EARPIECE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"music", "AUDIO_STREAM_MUSIC", 0, 40,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_SPEAKER_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"alarm", "AUDIO_STREAM_ALARM", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_HEADSET_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {0, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_SPEAKER_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {0, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_EARPIECE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {0, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_NON_MUTABLE_EXT_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {0, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_NON_MUTABLE_HEARING_AID_VOLUME_CURVE
- },
- },
- {"notification", "AUDIO_STREAM_NOTIFICATION", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_HEADSET_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {1, -4680}, {42, -2070}, {85, -540}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_SPEAKER_SYSTEM_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {1, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_EARPIECE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -4950}, {33, -3350}, {66, -1700}, {100, 0} } }, // DEFAULT_DEVICE_CATEGORY_HEADSET_VOLUME_CURVE
- },
- },
- {"bluetooth_sco", "AUDIO_STREAM_BLUETOOTH_SCO", 1, 10,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -4200}, {33, -2800}, {66, -1400}, {100, 0} } },
- {"DEVICE_CATEGORY_SPEAKER", { {0, -2400}, {33, -1600}, {66, -800}, {100, 0} } },
- {"DEVICE_CATEGORY_EARPIECE", { {0, -4200}, {33, -2800}, {66, -1400}, {100, 0} } },
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"enforced_audible", "AUDIO_STREAM_ENFORCED_AUDIBLE", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -3000}, {33, -2600}, {66, -2200}, {100, -1800} } },
- {"DEVICE_CATEGORY_SPEAKER", { {1, -3400}, {71, -2400}, {100, -2000} } },
- {"DEVICE_CATEGORY_EARPIECE", { {1, -2400}, {33, -1800}, {66, -1200}, {100, -600} } }, // DEFAULT_SYSTEM_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"dtmf", "AUDIO_STREAM_DTMF", 0, 100,
- {
- {"DEVICE_CATEGORY_HEADSET", { {1, -3000}, {33, -2600}, {66, -2200}, {100, -1800} } },
- {"DEVICE_CATEGORY_SPEAKER", { {1, -4000}, {71, -2400}, {100, -1400} } }, // DEFAULT_SYSTEM_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {1, -2400}, {33, -1800}, {66, -1200}, {100, -600} } }, // DEFAULT_SYSTEM_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {1, -5800}, {20, -4000}, {60, -2100}, {100, -1000} } }, // DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {1, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_HEARING_AID_VOLUME_CURVE
- },
- },
- {"tts", "AUDIO_STREAM_TTS", 0, 16,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -9600}, {100, -9600} } }, // SILENT_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {0, -9600}, {100, -9600} } }, // SILENT_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {0, -9600}, {100, -9600} } }, // SILENT_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {0, -9600}, {100, -9600} } }, // SILENT_VOLUME_CURVE
- },
- },
- {"accessibility", "AUDIO_STREAM_ACCESSIBILITY", 1, 40,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {0, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_SPEAKER_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {0, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {0, -5800}, {20, -4000}, {60, -1700}, {100, 0} } }, // DEFAULT_NON_MUTABLE_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {0, -12700}, {20, -8000}, {60, -4000}, {100, 0} } }, // DEFAULT_NON_MUTABLE_HEARING_AID_VOLUME_CURVE
- },
- },
- {"rerouting", "AUDIO_STREAM_REROUTING", 0, 1,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- },
- },
- {"patch", "AUDIO_STREAM_PATCH", 0, 1,
- {
- {"DEVICE_CATEGORY_HEADSET", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_SPEAKER", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EARPIECE", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_EXT_MEDIA", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- {"DEVICE_CATEGORY_HEARING_AID", { {0, -0}, {100, 0} } }, // FULL_SCALE_VOLUME_CURVE
- },
- },
-};
-
const engineConfig::Config gDefaultEngineConfig = {
1.0,
gOrderedStrategies,
{},
{},
- gVolumeGroups
+ {}
};
} // namespace android
diff --git a/services/audiopolicy/engine/config/Android.mk b/services/audiopolicy/engine/config/Android.mk
index fe7d961..0b292a5 100644
--- a/services/audiopolicy/engine/config/Android.mk
+++ b/services/audiopolicy/engine/config/Android.mk
@@ -23,7 +23,8 @@
libandroidicu \
libxml2 \
libutils \
- liblog
+ liblog \
+ libcutils
LOCAL_STATIC_LIBRARIES := \
libaudiopolicycomponents
diff --git a/services/audiopolicy/engine/config/include/EngineConfig.h b/services/audiopolicy/engine/config/include/EngineConfig.h
index f7caad2..a188115 100644
--- a/services/audiopolicy/engine/config/include/EngineConfig.h
+++ b/services/audiopolicy/engine/config/include/EngineConfig.h
@@ -113,6 +113,7 @@
* @return audio policy usage @see Config
*/
ParsingResult parse(const char* path = DEFAULT_PATH);
+android::status_t parseLegacyVolumes(VolumeGroups &volumeGroups);
} // namespace engineConfig
} // namespace android
diff --git a/services/audiopolicy/engine/config/src/EngineConfig.cpp b/services/audiopolicy/engine/config/src/EngineConfig.cpp
index 227ebd8..00fbac4 100644
--- a/services/audiopolicy/engine/config/src/EngineConfig.cpp
+++ b/services/audiopolicy/engine/config/src/EngineConfig.cpp
@@ -19,6 +19,7 @@
#include "EngineConfig.h"
#include <policy.h>
+#include <cutils/properties.h>
#include <media/TypeConverter.h>
#include <media/convert.h>
#include <utils/Log.h>
@@ -26,6 +27,7 @@
#include <libxml/xinclude.h>
#include <string>
#include <vector>
+#include <map>
#include <sstream>
#include <istream>
@@ -506,6 +508,84 @@
return NO_ERROR;
}
+static constexpr const char *legacyVolumecollectionTag = "volumes";
+static constexpr const char *legacyVolumeTag = "volume";
+
+status_t deserializeLegacyVolume(_xmlDoc *doc, const _xmlNode *cur,
+ std::map<std::string, VolumeCurves> &legacyVolumes)
+{
+ std::string streamTypeLiteral = getXmlAttribute(cur, "stream");
+ if (streamTypeLiteral.empty()) {
+ ALOGE("%s: No attribute stream found", __func__);
+ return BAD_VALUE;
+ }
+ std::string deviceCategoryLiteral = getXmlAttribute(cur, "deviceCategory");
+ if (deviceCategoryLiteral.empty()) {
+ ALOGE("%s: No attribute deviceCategory found", __func__);
+ return BAD_VALUE;
+ }
+ std::string referenceName = getXmlAttribute(cur, "ref");
+ const xmlNode *ref = NULL;
+ if (!referenceName.empty()) {
+ getReference(xmlDocGetRootElement(doc), ref, referenceName, legacyVolumecollectionTag);
+ if (ref == NULL) {
+ ALOGE("%s: No reference Ptr found for %s", __func__, referenceName.c_str());
+ return BAD_VALUE;
+ }
+ ALOGV("%s: reference found for %s", __func__, referenceName.c_str());
+ }
+ CurvePoints curvePoints;
+ for (const xmlNode *child = referenceName.empty() ?
+ cur->xmlChildrenNode : ref->xmlChildrenNode; child != NULL; child = child->next) {
+ if (!xmlStrcmp(child->name, (const xmlChar *)VolumeTraits::volumePointTag)) {
+ xmlCharUnique pointXml(xmlNodeListGetString(doc, child->xmlChildrenNode, 1), xmlFree);
+ if (pointXml == NULL) {
+ return BAD_VALUE;
+ }
+ ALOGV("%s: %s=%s", __func__, legacyVolumeTag,
+ reinterpret_cast<const char*>(pointXml.get()));
+ std::vector<int> point;
+ collectionFromString<DefaultTraits<int>>(
+ reinterpret_cast<const char*>(pointXml.get()), point, ",");
+ if (point.size() != 2) {
+ ALOGE("%s: Invalid %s: %s", __func__, VolumeTraits::volumePointTag,
+ reinterpret_cast<const char*>(pointXml.get()));
+ return BAD_VALUE;
+ }
+ curvePoints.push_back({point[0], point[1]});
+ }
+ }
+ legacyVolumes[streamTypeLiteral].push_back({ deviceCategoryLiteral, curvePoints });
+ return NO_ERROR;
+}
+
+static status_t deserializeLegacyVolumeCollection(_xmlDoc *doc, const _xmlNode *cur,
+ VolumeGroups &volumeGroups,
+ size_t &nbSkippedElement)
+{
+ std::map<std::string, VolumeCurves> legacyVolumeMap;
+ for (cur = cur->xmlChildrenNode; cur != NULL; cur = cur->next) {
+ if (xmlStrcmp(cur->name, (const xmlChar *)legacyVolumecollectionTag)) {
+ continue;
+ }
+ const xmlNode *child = cur->xmlChildrenNode;
+ for (; child != NULL; child = child->next) {
+ if (!xmlStrcmp(child->name, (const xmlChar *)legacyVolumeTag)) {
+
+ status_t status = deserializeLegacyVolume(doc, child, legacyVolumeMap);
+ if (status != NO_ERROR) {
+ nbSkippedElement += 1;
+ }
+ }
+ }
+ }
+ for (const auto &volumeMapIter : legacyVolumeMap) {
+ volumeGroups.push_back({ volumeMapIter.first, volumeMapIter.first, 0, 100,
+ volumeMapIter.second });
+ }
+ return NO_ERROR;
+}
+
ParsingResult parse(const char* path) {
xmlDocPtr doc;
doc = xmlParseFile(path);
@@ -543,5 +623,60 @@
return {std::move(config), nbSkippedElements};
}
+android::status_t parseLegacyVolumeFile(const char* path, VolumeGroups &volumeGroups) {
+ xmlDocPtr doc;
+ doc = xmlParseFile(path);
+ if (doc == NULL) {
+ ALOGE("%s: Could not parse document %s", __FUNCTION__, path);
+ return BAD_VALUE;
+ }
+ xmlNodePtr cur = xmlDocGetRootElement(doc);
+ if (cur == NULL) {
+ ALOGE("%s: Could not parse: empty document %s", __FUNCTION__, path);
+ xmlFreeDoc(doc);
+ return BAD_VALUE;
+ }
+ if (xmlXIncludeProcess(doc) < 0) {
+ ALOGE("%s: libxml failed to resolve XIncludes on document %s", __FUNCTION__, path);
+ return BAD_VALUE;
+ }
+ size_t nbSkippedElements = 0;
+ return deserializeLegacyVolumeCollection(doc, cur, volumeGroups, nbSkippedElements);
+}
+
+static const char *kConfigLocationList[] = {"/odm/etc", "/vendor/etc", "/system/etc"};
+static const int kConfigLocationListSize =
+ (sizeof(kConfigLocationList) / sizeof(kConfigLocationList[0]));
+static const int gApmXmlConfigFilePathMaxLength = 128;
+
+static constexpr const char *apmXmlConfigFileName = "audio_policy_configuration.xml";
+static constexpr const char *apmA2dpOffloadDisabledXmlConfigFileName =
+ "audio_policy_configuration_a2dp_offload_disabled.xml";
+
+android::status_t parseLegacyVolumes(VolumeGroups &volumeGroups) {
+ char audioPolicyXmlConfigFile[gApmXmlConfigFilePathMaxLength];
+ std::vector<const char *> fileNames;
+ status_t ret;
+
+ if (property_get_bool("ro.bluetooth.a2dp_offload.supported", false) &&
+ property_get_bool("persist.bluetooth.a2dp_offload.disabled", false)) {
+ // A2DP offload supported but disabled: try to use special XML file
+ fileNames.push_back(apmA2dpOffloadDisabledXmlConfigFileName);
+ }
+ fileNames.push_back(apmXmlConfigFileName);
+
+ for (const char* fileName : fileNames) {
+ for (int i = 0; i < kConfigLocationListSize; i++) {
+ snprintf(audioPolicyXmlConfigFile, sizeof(audioPolicyXmlConfigFile),
+ "%s/%s", kConfigLocationList[i], fileName);
+ ret = parseLegacyVolumeFile(audioPolicyXmlConfigFile, volumeGroups);
+ if (ret == NO_ERROR) {
+ return ret;
+ }
+ }
+ }
+ return BAD_VALUE;
+}
+
} // namespace engineConfig
} // namespace android