/* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // #define LOG_NDEBUG 0 #define LOG_TAG "VirtualCameraDevice" #include "VirtualCameraDevice.h" #include #include #include #include #include #include #include #include #include #include "VirtualCameraService.h" #include "VirtualCameraSession.h" #include "aidl/android/companion/virtualcamera/SupportedStreamConfiguration.h" #include "aidl/android/companion/virtualcamera/VirtualCameraConfiguration.h" #include "aidl/android/hardware/camera/common/Status.h" #include "aidl/android/hardware/camera/device/CameraMetadata.h" #include "aidl/android/hardware/camera/device/StreamConfiguration.h" #include "android/binder_auto_utils.h" #include "android/binder_status.h" #include "log/log.h" #include "system/camera_metadata.h" #include "util/MetadataUtil.h" #include "util/Util.h" namespace android { namespace companion { namespace virtualcamera { using ::aidl::android::companion::virtualcamera::Format; using ::aidl::android::companion::virtualcamera::IVirtualCameraCallback; using ::aidl::android::companion::virtualcamera::LensFacing; using ::aidl::android::companion::virtualcamera::SensorOrientation; using ::aidl::android::companion::virtualcamera::SupportedStreamConfiguration; using ::aidl::android::companion::virtualcamera::VirtualCameraConfiguration; using ::aidl::android::hardware::camera::common::CameraResourceCost; using ::aidl::android::hardware::camera::common::Status; using ::aidl::android::hardware::camera::device::CameraMetadata; using ::aidl::android::hardware::camera::device::ICameraDeviceCallback; using ::aidl::android::hardware::camera::device::ICameraDeviceSession; using ::aidl::android::hardware::camera::device::ICameraInjectionSession; using ::aidl::android::hardware::camera::device::Stream; using ::aidl::android::hardware::camera::device::StreamConfiguration; using ::aidl::android::hardware::camera::device::StreamRotation; using ::aidl::android::hardware::camera::device::StreamType; using ::aidl::android::hardware::graphics::common::PixelFormat; namespace { using namespace std::chrono_literals; // Prefix of camera name - "device@1.1/virtual/{camera_id}" const char* kDevicePathPrefix = "device@1.1/virtual/"; constexpr int32_t kMaxJpegSize = 3 * 1024 * 1024 /*3MiB*/; constexpr std::chrono::nanoseconds kMaxFrameDuration = std::chrono::duration_cast( 1e9ns / VirtualCameraDevice::kMinFps); constexpr uint8_t kPipelineMaxDepth = 2; constexpr int k30Fps = 30; constexpr MetadataBuilder::ControlRegion kDefaultEmptyControlRegion{}; const std::array kStandardJpegThumbnailSizes{ Resolution(176, 144), Resolution(240, 144), Resolution(256, 144), Resolution(240, 160), Resolution(240, 180)}; const std::array kOutputFormats{ PixelFormat::IMPLEMENTATION_DEFINED, PixelFormat::YCBCR_420_888, PixelFormat::BLOB}; // The resolutions below will used to extend the set of supported output formats. // All resolutions with lower pixel count and same aspect ratio as some supported // input resolution will be added to the set of supported output resolutions. const std::array kOutputResolutions{ Resolution(320, 240), Resolution(640, 360), Resolution(640, 480), Resolution(720, 480), Resolution(720, 576), Resolution(800, 600), Resolution(1024, 576), Resolution(1280, 720), Resolution(1280, 960), Resolution(1280, 1080), }; std::vector getSupportedJpegThumbnailSizes( const std::vector& configs) { auto isSupportedByAnyInputConfig = [&configs](const Resolution thumbnailResolution) { return std::any_of( configs.begin(), configs.end(), [thumbnailResolution](const SupportedStreamConfiguration& config) { return isApproximatellySameAspectRatio( thumbnailResolution, Resolution(config.width, config.height)); }); }; std::vector supportedThumbnailSizes({Resolution(0, 0)}); std::copy_if(kStandardJpegThumbnailSizes.begin(), kStandardJpegThumbnailSizes.end(), std::back_insert_iterator(supportedThumbnailSizes), isSupportedByAnyInputConfig); return supportedThumbnailSizes; } bool isSupportedOutputFormat(const PixelFormat pixelFormat) { return std::find(kOutputFormats.begin(), kOutputFormats.end(), pixelFormat) != kOutputFormats.end(); } std::vector fpsRangesForInputConfig( const std::vector& configs) { std::set availableRanges; for (const SupportedStreamConfiguration& config : configs) { availableRanges.insert( {.minFps = VirtualCameraDevice::kMinFps, .maxFps = config.maxFps}); availableRanges.insert({.minFps = config.maxFps, .maxFps = config.maxFps}); } if (std::any_of(configs.begin(), configs.end(), [](const SupportedStreamConfiguration& config) { return config.maxFps >= k30Fps; })) { // Extend the set of available ranges with (minFps <= 15, 30) & (30, 30) as // required by CDD. availableRanges.insert( {.minFps = VirtualCameraDevice::kMinFps, .maxFps = k30Fps}); availableRanges.insert({.minFps = k30Fps, .maxFps = k30Fps}); } return std::vector(availableRanges.begin(), availableRanges.end()); } std::optional getMaxResolution( const std::vector& configs) { auto itMax = std::max_element(configs.begin(), configs.end(), [](const SupportedStreamConfiguration& a, const SupportedStreamConfiguration& b) { return a.width * b.height < a.width * b.height; }); if (itMax == configs.end()) { ALOGE( "%s: empty vector of supported configurations, cannot find largest " "resolution.", __func__); return std::nullopt; } return Resolution(itMax->width, itMax->height); } // Returns a map of unique resolution to maximum maxFps for all streams with // that resolution. std::map getResolutionToMaxFpsMap( const std::vector& configs) { std::map resolutionToMaxFpsMap; for (const SupportedStreamConfiguration& config : configs) { Resolution resolution(config.width, config.height); if (resolutionToMaxFpsMap.find(resolution) == resolutionToMaxFpsMap.end()) { resolutionToMaxFpsMap[resolution] = config.maxFps; } else { int currentMaxFps = resolutionToMaxFpsMap[resolution]; resolutionToMaxFpsMap[resolution] = std::max(currentMaxFps, config.maxFps); } } std::map additionalResolutionToMaxFpsMap; // Add additional resolutions we can support by downscaling input streams with // same aspect ratio. for (const Resolution& outputResolution : kOutputResolutions) { for (const auto& [resolution, maxFps] : resolutionToMaxFpsMap) { if (resolutionToMaxFpsMap.find(outputResolution) != resolutionToMaxFpsMap.end()) { // Resolution is already in the map, skip it. continue; } if (outputResolution < resolution && isApproximatellySameAspectRatio(outputResolution, resolution)) { // Lower resolution with same aspect ratio, we can achieve this by // downscaling, let's add it to the map. ALOGD( "Extending set of output resolutions with %dx%d which has same " "aspect ratio as supported input %dx%d.", outputResolution.width, outputResolution.height, resolution.width, resolution.height); additionalResolutionToMaxFpsMap[outputResolution] = maxFps; break; } } } // Add all resolution we can achieve by downscaling to the map. resolutionToMaxFpsMap.insert(additionalResolutionToMaxFpsMap.begin(), additionalResolutionToMaxFpsMap.end()); return resolutionToMaxFpsMap; } // TODO(b/301023410) - Populate camera characteristics according to camera configuration. std::optional initCameraCharacteristics( const std::vector& supportedInputConfig, const SensorOrientation sensorOrientation, const LensFacing lensFacing, const int32_t deviceId) { if (!std::all_of(supportedInputConfig.begin(), supportedInputConfig.end(), [](const SupportedStreamConfiguration& config) { return isFormatSupportedForInput( config.width, config.height, config.pixelFormat, config.maxFps); })) { ALOGE("%s: input configuration contains unsupported format", __func__); return std::nullopt; } MetadataBuilder builder = MetadataBuilder() .setSupportedHardwareLevel( ANDROID_INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) .setDeviceId(deviceId) .setFlashAvailable(false) .setLensFacing( static_cast(lensFacing)) .setAvailableFocalLengths({VirtualCameraDevice::kFocalLength}) .setSensorOrientation(static_cast(sensorOrientation)) .setSensorReadoutTimestamp( ANDROID_SENSOR_READOUT_TIMESTAMP_NOT_SUPPORTED) .setSensorTimestampSource(ANDROID_SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN) .setSensorPhysicalSize(36.0, 24.0) .setAvailableAberrationCorrectionModes( {ANDROID_COLOR_CORRECTION_ABERRATION_MODE_OFF}) .setAvailableNoiseReductionModes({ANDROID_NOISE_REDUCTION_MODE_OFF}) .setAvailableFaceDetectModes({ANDROID_STATISTICS_FACE_DETECT_MODE_OFF}) .setAvailableStreamUseCases( {ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT, ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW, ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE, ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD, ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL, ANDROID_SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL}) .setAvailableTestPatternModes({ANDROID_SENSOR_TEST_PATTERN_MODE_OFF}) .setAvailableMaxDigitalZoom(1.0) .setControlAvailableModes({ANDROID_CONTROL_MODE_AUTO}) .setControlAfAvailableModes({ANDROID_CONTROL_AF_MODE_OFF}) .setControlAvailableSceneModes({ANDROID_CONTROL_SCENE_MODE_DISABLED}) .setControlAvailableEffects({ANDROID_CONTROL_EFFECT_MODE_OFF}) .setControlAvailableVideoStabilizationModes( {ANDROID_CONTROL_VIDEO_STABILIZATION_MODE_OFF}) .setControlAeAvailableModes({ANDROID_CONTROL_AE_MODE_ON}) .setControlAeAvailableAntibandingModes( {ANDROID_CONTROL_AE_ANTIBANDING_MODE_AUTO}) .setControlAeAvailableFpsRanges( fpsRangesForInputConfig(supportedInputConfig)) .setControlMaxRegions(0, 0, 0) .setControlAfRegions({kDefaultEmptyControlRegion}) .setControlAeRegions({kDefaultEmptyControlRegion}) .setControlAwbRegions({kDefaultEmptyControlRegion}) .setControlAeCompensationRange(0, 0) .setControlAeCompensationStep(camera_metadata_rational_t{0, 1}) .setControlAwbLockAvailable(false) .setControlAeLockAvailable(false) .setControlAvailableAwbModes({ANDROID_CONTROL_AWB_MODE_AUTO}) .setControlZoomRatioRange(/*min=*/1.0, /*max=*/1.0) .setCroppingType(ANDROID_SCALER_CROPPING_TYPE_CENTER_ONLY) .setJpegAvailableThumbnailSizes( getSupportedJpegThumbnailSizes(supportedInputConfig)) .setMaxJpegSize(kMaxJpegSize) .setMaxFaceCount(0) .setMaxFrameDuration(kMaxFrameDuration) .setMaxNumberOutputStreams( VirtualCameraDevice::kMaxNumberOfRawStreams, VirtualCameraDevice::kMaxNumberOfProcessedStreams, VirtualCameraDevice::kMaxNumberOfStallStreams) .setRequestPartialResultCount(1) .setPipelineMaxDepth(kPipelineMaxDepth) .setSyncMaxLatency(ANDROID_SYNC_MAX_LATENCY_UNKNOWN) .setAvailableRequestKeys({ANDROID_COLOR_CORRECTION_ABERRATION_MODE, ANDROID_CONTROL_CAPTURE_INTENT, ANDROID_CONTROL_AE_MODE, ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION, ANDROID_CONTROL_AE_TARGET_FPS_RANGE, ANDROID_CONTROL_AE_ANTIBANDING_MODE, ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER, ANDROID_CONTROL_AF_TRIGGER, ANDROID_CONTROL_AF_MODE, ANDROID_CONTROL_AWB_MODE, ANDROID_SCALER_CROP_REGION, ANDROID_CONTROL_EFFECT_MODE, ANDROID_CONTROL_MODE, ANDROID_CONTROL_SCENE_MODE, ANDROID_CONTROL_VIDEO_STABILIZATION_MODE, ANDROID_CONTROL_ZOOM_RATIO, ANDROID_FLASH_MODE, ANDROID_JPEG_AVAILABLE_THUMBNAIL_SIZES, ANDROID_JPEG_ORIENTATION, ANDROID_JPEG_QUALITY, ANDROID_JPEG_THUMBNAIL_QUALITY, ANDROID_JPEG_THUMBNAIL_SIZE, ANDROID_NOISE_REDUCTION_MODE, ANDROID_STATISTICS_FACE_DETECT_MODE}) .setAvailableResultKeys({ ANDROID_COLOR_CORRECTION_ABERRATION_MODE, ANDROID_CONTROL_AE_ANTIBANDING_MODE, ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION, ANDROID_CONTROL_AE_LOCK, ANDROID_CONTROL_AE_MODE, ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER, ANDROID_CONTROL_AE_STATE, ANDROID_CONTROL_AE_TARGET_FPS_RANGE, ANDROID_CONTROL_AF_MODE, ANDROID_CONTROL_AF_STATE, ANDROID_CONTROL_AF_TRIGGER, ANDROID_CONTROL_AWB_LOCK, ANDROID_CONTROL_AWB_MODE, ANDROID_CONTROL_AWB_STATE, ANDROID_CONTROL_CAPTURE_INTENT, ANDROID_CONTROL_EFFECT_MODE, ANDROID_CONTROL_MODE, ANDROID_CONTROL_SCENE_MODE, ANDROID_CONTROL_VIDEO_STABILIZATION_MODE, ANDROID_STATISTICS_FACE_DETECT_MODE, ANDROID_FLASH_MODE, ANDROID_FLASH_STATE, ANDROID_JPEG_AVAILABLE_THUMBNAIL_SIZES, ANDROID_JPEG_QUALITY, ANDROID_JPEG_THUMBNAIL_QUALITY, ANDROID_LENS_FOCAL_LENGTH, ANDROID_LENS_OPTICAL_STABILIZATION_MODE, ANDROID_NOISE_REDUCTION_MODE, ANDROID_REQUEST_PIPELINE_DEPTH, ANDROID_SENSOR_TIMESTAMP, ANDROID_STATISTICS_HOT_PIXEL_MAP_MODE, ANDROID_STATISTICS_LENS_SHADING_MAP_MODE, ANDROID_STATISTICS_SCENE_FLICKER, }) .setAvailableCapabilities( {ANDROID_REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE}); // Active array size must correspond to largest supported input resolution. std::optional maxResolution = getMaxResolution(supportedInputConfig); if (!maxResolution.has_value()) { return std::nullopt; } builder.setSensorActiveArraySize(0, 0, maxResolution->width, maxResolution->height); builder.setSensorPixelArraySize(maxResolution->width, maxResolution->height); std::vector outputConfigurations; // TODO(b/301023410) Add also all "standard" resolutions we can rescale the // streams to (all standard resolutions with same aspect ratio). std::map resolutionToMaxFpsMap = getResolutionToMaxFpsMap(supportedInputConfig); // Add configurations for all unique input resolutions and output formats. for (const PixelFormat format : kOutputFormats) { std::transform( resolutionToMaxFpsMap.begin(), resolutionToMaxFpsMap.end(), std::back_inserter(outputConfigurations), [format](const auto& entry) { Resolution resolution = entry.first; int maxFps = entry.second; return MetadataBuilder::StreamConfiguration{ .width = resolution.width, .height = resolution.height, .format = static_cast(format), .minFrameDuration = std::chrono::nanoseconds(1s) / maxFps, .minStallDuration = 0s}; }); } ALOGV("Adding %zu output configurations", outputConfigurations.size()); builder.setAvailableOutputStreamConfigurations(outputConfigurations); auto metadata = builder.setAvailableCharacteristicKeys().build(); if (metadata == nullptr) { ALOGE("Failed to build metadata!"); return CameraMetadata(); } return std::move(*metadata); } } // namespace VirtualCameraDevice::VirtualCameraDevice( const std::string& cameraId, const VirtualCameraConfiguration& configuration, int32_t deviceId) : mCameraId(cameraId), mVirtualCameraClientCallback(configuration.virtualCameraCallback), mSupportedInputConfigurations(configuration.supportedStreamConfigs) { std::optional metadata = initCameraCharacteristics( mSupportedInputConfigurations, configuration.sensorOrientation, configuration.lensFacing, deviceId); if (metadata.has_value()) { mCameraCharacteristics = *metadata; } else { ALOGE( "%s: Failed to initialize camera characteristic based on provided " "configuration.", __func__); } } ndk::ScopedAStatus VirtualCameraDevice::getCameraCharacteristics( CameraMetadata* _aidl_return) { ALOGV("%s", __func__); if (_aidl_return == nullptr) { return cameraStatus(Status::ILLEGAL_ARGUMENT); } *_aidl_return = mCameraCharacteristics; return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus VirtualCameraDevice::getPhysicalCameraCharacteristics( const std::string& in_physicalCameraId, CameraMetadata* _aidl_return) { ALOGV("%s: physicalCameraId %s", __func__, in_physicalCameraId.c_str()); (void)_aidl_return; // VTS tests expect this call to fail with illegal argument status for // all publicly advertised camera ids. // Because we don't support physical camera ids, we just always // fail with illegal argument (there's no valid argument to provide). return cameraStatus(Status::ILLEGAL_ARGUMENT); } ndk::ScopedAStatus VirtualCameraDevice::getResourceCost( CameraResourceCost* _aidl_return) { ALOGV("%s", __func__); if (_aidl_return == nullptr) { return cameraStatus(Status::ILLEGAL_ARGUMENT); } _aidl_return->resourceCost = 100; // ¯\_(ツ)_/¯ return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus VirtualCameraDevice::isStreamCombinationSupported( const StreamConfiguration& in_streams, bool* _aidl_return) { ALOGV("%s", __func__); if (_aidl_return == nullptr) { return cameraStatus(Status::ILLEGAL_ARGUMENT); } *_aidl_return = isStreamCombinationSupported(in_streams); return ndk::ScopedAStatus::ok(); }; bool VirtualCameraDevice::isStreamCombinationSupported( const StreamConfiguration& streamConfiguration) const { if (streamConfiguration.streams.empty()) { ALOGE("%s: Querying empty configuration", __func__); return false; } const std::vector& streams = streamConfiguration.streams; Resolution firstStreamResolution(streams[0].width, streams[0].height); auto isSameAspectRatioAsFirst = [firstStreamResolution](const Stream& stream) { return isApproximatellySameAspectRatio( firstStreamResolution, Resolution(stream.width, stream.height)); }; if (!std::all_of(streams.begin(), streams.end(), isSameAspectRatioAsFirst)) { ALOGW( "%s: Requested streams do not have same aspect ratio. Different aspect " "ratios are currently " "not supported by virtual camera. Stream configuration: %s", __func__, streamConfiguration.toString().c_str()); return false; } int numberOfProcessedStreams = 0; int numberOfStallStreams = 0; for (const Stream& stream : streamConfiguration.streams) { ALOGV("%s: Configuration queried: %s", __func__, stream.toString().c_str()); if (stream.streamType == StreamType::INPUT) { ALOGW("%s: Input stream type is not supported", __func__); return false; } if (stream.rotation != StreamRotation::ROTATION_0 || !isSupportedOutputFormat(stream.format)) { ALOGV("Unsupported output stream type"); return false; } if (stream.format == PixelFormat::BLOB) { numberOfStallStreams++; } else { numberOfProcessedStreams++; } Resolution requestedResolution(stream.width, stream.height); auto matchesSupportedInputConfig = [requestedResolution](const SupportedStreamConfiguration& config) { Resolution supportedInputResolution(config.width, config.height); return requestedResolution <= supportedInputResolution && isApproximatellySameAspectRatio(requestedResolution, supportedInputResolution); }; if (std::none_of(mSupportedInputConfigurations.begin(), mSupportedInputConfigurations.end(), matchesSupportedInputConfig)) { ALOGV("Requested config doesn't match any supported input config"); return false; } } if (numberOfProcessedStreams > kMaxNumberOfProcessedStreams) { ALOGE("%s: %d processed streams exceeds the supported maximum of %d", __func__, numberOfProcessedStreams, kMaxNumberOfProcessedStreams); return false; } if (numberOfStallStreams > kMaxNumberOfStallStreams) { ALOGE("%s: %d stall streams exceeds the supported maximum of %d", __func__, numberOfStallStreams, kMaxNumberOfStallStreams); return false; } return true; } ndk::ScopedAStatus VirtualCameraDevice::open( const std::shared_ptr& in_callback, std::shared_ptr* _aidl_return) { ALOGV("%s", __func__); *_aidl_return = ndk::SharedRefBase::make( sharedFromThis(), in_callback, mVirtualCameraClientCallback); return ndk::ScopedAStatus::ok(); }; ndk::ScopedAStatus VirtualCameraDevice::openInjectionSession( const std::shared_ptr& in_callback, std::shared_ptr* _aidl_return) { ALOGV("%s", __func__); (void)in_callback; (void)_aidl_return; return cameraStatus(Status::OPERATION_NOT_SUPPORTED); } ndk::ScopedAStatus VirtualCameraDevice::setTorchMode(bool in_on) { ALOGV("%s: on = %s", __func__, in_on ? "on" : "off"); return cameraStatus(Status::OPERATION_NOT_SUPPORTED); } ndk::ScopedAStatus VirtualCameraDevice::turnOnTorchWithStrengthLevel( int32_t in_torchStrength) { ALOGV("%s: torchStrength = %d", __func__, in_torchStrength); return cameraStatus(Status::OPERATION_NOT_SUPPORTED); } ndk::ScopedAStatus VirtualCameraDevice::getTorchStrengthLevel( int32_t* _aidl_return) { (void)_aidl_return; return cameraStatus(Status::OPERATION_NOT_SUPPORTED); } binder_status_t VirtualCameraDevice::dump(int fd, const char**, uint32_t) { ALOGD("Dumping virtual camera %s", mCameraId.c_str()); const char* indent = " "; const char* doubleIndent = " "; dprintf(fd, "%svirtual_camera %s belongs to virtual device %d\n", indent, mCameraId.c_str(), getDeviceId(mCameraCharacteristics) .value_or(VirtualCameraService::kDefaultDeviceId)); dprintf(fd, "%sSupportedStreamConfiguration:\n", indent); for (auto& config : mSupportedInputConfigurations) { dprintf(fd, "%s%s", doubleIndent, config.toString().c_str()); } return STATUS_OK; } std::string VirtualCameraDevice::getCameraName() const { return std::string(kDevicePathPrefix) + mCameraId; } const std::vector& VirtualCameraDevice::getInputConfigs() const { return mSupportedInputConfigurations; } Resolution VirtualCameraDevice::getMaxInputResolution() const { std::optional maxResolution = getMaxResolution(mSupportedInputConfigurations); if (!maxResolution.has_value()) { ALOGE( "%s: Cannot determine sensor size for virtual camera - input " "configurations empty?", __func__); return Resolution(0, 0); } return maxResolution.value(); } int VirtualCameraDevice::allocateInputStreamId() { return mNextInputStreamId++; } std::shared_ptr VirtualCameraDevice::sharedFromThis() { // SharedRefBase which BnCameraDevice inherits from breaks // std::enable_shared_from_this. This is recommended replacement for // shared_from_this() per documentation in binder_interface_utils.h. return ref(); } } // namespace virtualcamera } // namespace companion } // namespace android