blob: 48b441079a4a2be71d916ad5137f0a9fb94ec71b [file] [log] [blame]
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +01001/*
Biswarup Pal6152a302023-12-19 12:44:09 +00002 * Copyright 2023 The Android Open Source Project
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +01003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// #define LOG_NDEBUG 0
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010018#define LOG_TAG "VirtualCameraService"
19#include "VirtualCameraService.h"
20
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010021#include <algorithm>
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +020022#include <array>
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010023#include <cinttypes>
24#include <cstdint>
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010025#include <memory>
26#include <mutex>
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010027#include <optional>
28#include <regex>
29#include <variant>
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010030
31#include "VirtualCameraDevice.h"
32#include "VirtualCameraProvider.h"
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010033#include "aidl/android/companion/virtualcamera/Format.h"
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010034#include "aidl/android/companion/virtualcamera/LensFacing.h"
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010035#include "aidl/android/companion/virtualcamera/VirtualCameraConfiguration.h"
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010036#include "android/binder_auto_utils.h"
37#include "android/binder_libbinder.h"
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010038#include "android/binder_status.h"
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010039#include "binder/Status.h"
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +020040#include "fmt/format.h"
41#include "util/EglDisplayContext.h"
42#include "util/EglUtil.h"
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +010043#include "util/Permissions.h"
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010044#include "util/Util.h"
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010045
46using ::android::binder::Status;
47
48namespace android {
49namespace companion {
50namespace virtualcamera {
51
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010052using ::aidl::android::companion::virtualcamera::Format;
Biswarup Pal112458f2023-12-28 19:50:17 +000053using ::aidl::android::companion::virtualcamera::LensFacing;
Biswarup Pal6152a302023-12-19 12:44:09 +000054using ::aidl::android::companion::virtualcamera::SensorOrientation;
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010055using ::aidl::android::companion::virtualcamera::SupportedStreamConfiguration;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010056using ::aidl::android::companion::virtualcamera::VirtualCameraConfiguration;
57
Marvin Ramina8196132024-03-15 15:55:22 +000058// TODO(b/301023410) Make camera id range configurable / dynamic
59// based on already registered devices.
60std::atomic_int VirtualCameraService::sNextId{1000};
61
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010062namespace {
63
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010064constexpr int kVgaWidth = 640;
65constexpr int kVgaHeight = 480;
Biswarup Pal6152a302023-12-19 12:44:09 +000066constexpr int kMaxFps = 60;
Biswarup Pal37a75182024-01-16 15:53:35 +000067constexpr int kDefaultDeviceId = 0;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010068constexpr char kEnableTestCameraCmd[] = "enable_test_camera";
69constexpr char kDisableTestCameraCmd[] = "disable_test_camera";
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010070constexpr char kHelp[] = "help";
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010071constexpr char kShellCmdHelp[] = R"(
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010072Usage:
73 cmd virtual_camera command [--option=value]
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010074Available commands:
75 * enable_test_camera
Jan Sebechlebsky773c0142024-03-25 12:17:05 +010076 Options:
77 --camera_id=(ID) - override numerical ID for test camera instance
78 --lens_facing=(front|back|external) - specifies lens facing for test camera instance
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010079 * disable_test_camera
80)";
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +010081constexpr char kCreateVirtualDevicePermission[] =
82 "android.permission.CREATE_VIRTUAL_DEVICE";
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +010083
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +020084constexpr std::array<const char*, 3> kRequiredEglExtensions = {
85 "GL_OES_EGL_image_external",
86 "GL_OES_EGL_image_external_essl3",
87 "GL_EXT_YUV_target",
88};
89
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +010090ndk::ScopedAStatus validateConfiguration(
91 const VirtualCameraConfiguration& configuration) {
92 if (configuration.supportedStreamConfigs.empty()) {
93 ALOGE("%s: No supported input configuration specified", __func__);
94 return ndk::ScopedAStatus::fromServiceSpecificError(
95 Status::EX_ILLEGAL_ARGUMENT);
96 }
97
98 for (const SupportedStreamConfiguration& config :
99 configuration.supportedStreamConfigs) {
100 if (!isFormatSupportedForInput(config.width, config.height,
Biswarup Pal6152a302023-12-19 12:44:09 +0000101 config.pixelFormat, config.maxFps)) {
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +0100102 ALOGE("%s: Requested unsupported input format: %d x %d (%d)", __func__,
103 config.width, config.height, static_cast<int>(config.pixelFormat));
104 return ndk::ScopedAStatus::fromServiceSpecificError(
105 Status::EX_ILLEGAL_ARGUMENT);
106 }
107 }
Biswarup Pal6152a302023-12-19 12:44:09 +0000108
109 if (configuration.sensorOrientation != SensorOrientation::ORIENTATION_0 &&
110 configuration.sensorOrientation != SensorOrientation::ORIENTATION_90 &&
111 configuration.sensorOrientation != SensorOrientation::ORIENTATION_180 &&
112 configuration.sensorOrientation != SensorOrientation::ORIENTATION_270) {
113 return ndk::ScopedAStatus::fromServiceSpecificError(
114 Status::EX_ILLEGAL_ARGUMENT);
115 }
116
Biswarup Pal112458f2023-12-28 19:50:17 +0000117 if (configuration.lensFacing != LensFacing::FRONT &&
118 configuration.lensFacing != LensFacing::BACK &&
119 configuration.lensFacing != LensFacing::EXTERNAL) {
120 return ndk::ScopedAStatus::fromServiceSpecificError(
121 Status::EX_ILLEGAL_ARGUMENT);
122 }
123
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +0100124 return ndk::ScopedAStatus::ok();
125}
126
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100127enum class Command {
128 ENABLE_TEST_CAMERA,
129 DISABLE_TEST_CAMERA,
130 HELP,
131};
132
133struct CommandWithOptions {
134 Command command;
135 std::map<std::string, std::string> optionToValueMap;
136};
137
138std::optional<int> parseInt(const std::string& s) {
139 if (!std::all_of(s.begin(), s.end(), [](char c) { return std::isdigit(c); })) {
140 return std::nullopt;
141 }
142 int ret = atoi(s.c_str());
143 return ret > 0 ? std::optional(ret) : std::nullopt;
144}
145
146std::optional<LensFacing> parseLensFacing(const std::string& s) {
147 static const std::map<std::string, LensFacing> strToLensFacing{
148 {"front", LensFacing::FRONT},
149 {"back", LensFacing::BACK},
150 {"external", LensFacing::EXTERNAL}};
151 auto it = strToLensFacing.find(s);
152 return it == strToLensFacing.end() ? std::nullopt : std::optional(it->second);
153}
154
155std::variant<CommandWithOptions, std::string> parseCommand(
156 const char** args, const uint32_t numArgs) {
157 static const std::regex optionRegex("^--(\\w+)(?:=(.+))?$");
158 static const std::map<std::string, Command> strToCommand{
159 {kHelp, Command::HELP},
160 {kEnableTestCameraCmd, Command::ENABLE_TEST_CAMERA},
161 {kDisableTestCameraCmd, Command::DISABLE_TEST_CAMERA}};
162
163 if (numArgs < 1) {
164 return CommandWithOptions{.command = Command::HELP};
165 }
166
167 // We interpret the first argument as command;
168 auto it = strToCommand.find(args[0]);
169 if (it == strToCommand.end()) {
170 return "Unknown command: " + std::string(args[0]);
171 }
172
173 CommandWithOptions cmd{.command = it->second};
174
175 for (int i = 1; i < numArgs; i++) {
176 std::cmatch cm;
177 if (!std::regex_match(args[i], cm, optionRegex)) {
178 return "Not an option: " + std::string(args[i]);
179 }
180
181 cmd.optionToValueMap[cm[1]] = cm[2];
182 }
183
184 return cmd;
185};
186
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +0200187ndk::ScopedAStatus verifyRequiredEglExtensions() {
188 EglDisplayContext context;
189 for (const char* eglExtension : kRequiredEglExtensions) {
190 if (!isGlExtensionSupported(eglExtension)) {
191 ALOGE("%s not supported", eglExtension);
192 return ndk::ScopedAStatus::fromExceptionCodeWithMessage(
193 EX_UNSUPPORTED_OPERATION,
194 fmt::format(
195 "Cannot create virtual camera, because required EGL extension {} "
196 "is not supported on this system",
197 eglExtension)
198 .c_str());
199 }
200 }
201 return ndk::ScopedAStatus::ok();
202}
203
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100204} // namespace
205
206VirtualCameraService::VirtualCameraService(
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +0100207 std::shared_ptr<VirtualCameraProvider> virtualCameraProvider,
208 const PermissionsProxy& permissionProxy)
209 : mVirtualCameraProvider(virtualCameraProvider),
210 mPermissionProxy(permissionProxy) {
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100211}
212
213ndk::ScopedAStatus VirtualCameraService::registerCamera(
214 const ::ndk::SpAIBinder& token,
Biswarup Pal37a75182024-01-16 15:53:35 +0000215 const VirtualCameraConfiguration& configuration, const int32_t deviceId,
216 bool* _aidl_return) {
217 return registerCamera(token, configuration, sNextId++, deviceId, _aidl_return);
Marvin Ramina8196132024-03-15 15:55:22 +0000218}
219
220ndk::ScopedAStatus VirtualCameraService::registerCamera(
221 const ::ndk::SpAIBinder& token,
222 const VirtualCameraConfiguration& configuration, const int cameraId,
Biswarup Pal37a75182024-01-16 15:53:35 +0000223 const int32_t deviceId, bool* _aidl_return) {
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +0100224 if (!mPermissionProxy.checkCallingPermission(kCreateVirtualDevicePermission)) {
225 ALOGE("%s: caller (pid %d, uid %d) doesn't hold %s permission", __func__,
226 getpid(), getuid(), kCreateVirtualDevicePermission);
227 return ndk::ScopedAStatus::fromExceptionCode(EX_SECURITY);
228 }
229
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100230 if (_aidl_return == nullptr) {
231 return ndk::ScopedAStatus::fromServiceSpecificError(
232 Status::EX_ILLEGAL_ARGUMENT);
233 }
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +0100234
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +0200235 if (mVerifyEglExtensions) {
236 auto status = verifyRequiredEglExtensions();
237 if (!status.isOk()) {
238 *_aidl_return = false;
239 return status;
240 }
241 }
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100242
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +0100243 auto status = validateConfiguration(configuration);
244 if (!status.isOk()) {
245 *_aidl_return = false;
246 return status;
247 }
248
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100249 std::lock_guard lock(mLock);
250 if (mTokenToCameraName.find(token) != mTokenToCameraName.end()) {
251 ALOGE(
252 "Attempt to register camera corresponding to already registered binder "
253 "token: "
254 "0x%" PRIxPTR,
255 reinterpret_cast<uintptr_t>(token.get()));
256 *_aidl_return = false;
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +0100257 return ndk::ScopedAStatus::ok();
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100258 }
259
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100260 std::shared_ptr<VirtualCameraDevice> camera =
Biswarup Pal37a75182024-01-16 15:53:35 +0000261 mVirtualCameraProvider->createCamera(configuration, cameraId, deviceId);
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100262 if (camera == nullptr) {
263 ALOGE("Failed to create camera for binder token 0x%" PRIxPTR,
264 reinterpret_cast<uintptr_t>(token.get()));
265 *_aidl_return = false;
266 return ndk::ScopedAStatus::fromServiceSpecificError(
267 Status::EX_SERVICE_SPECIFIC);
268 }
269
270 mTokenToCameraName[token] = camera->getCameraName();
Jan Sebechlebskyb36090e2024-04-11 09:19:33 +0200271 *_aidl_return = true;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100272 return ndk::ScopedAStatus::ok();
273}
274
275ndk::ScopedAStatus VirtualCameraService::unregisterCamera(
276 const ::ndk::SpAIBinder& token) {
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +0100277 if (!mPermissionProxy.checkCallingPermission(kCreateVirtualDevicePermission)) {
278 ALOGE("%s: caller (pid %d, uid %d) doesn't hold %s permission", __func__,
279 getpid(), getuid(), kCreateVirtualDevicePermission);
280 return ndk::ScopedAStatus::fromExceptionCode(EX_SECURITY);
281 }
282
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100283 std::lock_guard lock(mLock);
284
285 auto it = mTokenToCameraName.find(token);
286 if (it == mTokenToCameraName.end()) {
287 ALOGE(
288 "Attempt to unregister camera corresponding to unknown binder token: "
289 "0x%" PRIxPTR,
290 reinterpret_cast<uintptr_t>(token.get()));
291 return ndk::ScopedAStatus::ok();
292 }
293
294 mVirtualCameraProvider->removeCamera(it->second);
295
Tony Guo6cbe11b2024-03-17 02:34:23 +0000296 mTokenToCameraName.erase(it);
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100297 return ndk::ScopedAStatus::ok();
298}
299
Biswarup Pal68137fc2023-11-24 18:06:54 +0000300ndk::ScopedAStatus VirtualCameraService::getCameraId(
Marvin Ramina8196132024-03-15 15:55:22 +0000301 const ::ndk::SpAIBinder& token, int32_t* _aidl_return) {
Jan Sebechlebskyde6f16f2023-11-29 09:27:36 +0100302 if (!mPermissionProxy.checkCallingPermission(kCreateVirtualDevicePermission)) {
303 ALOGE("%s: caller (pid %d, uid %d) doesn't hold %s permission", __func__,
304 getpid(), getuid(), kCreateVirtualDevicePermission);
305 return ndk::ScopedAStatus::fromExceptionCode(EX_SECURITY);
306 }
307
Biswarup Pal68137fc2023-11-24 18:06:54 +0000308 if (_aidl_return == nullptr) {
309 return ndk::ScopedAStatus::fromServiceSpecificError(
Marvin Ramina8196132024-03-15 15:55:22 +0000310 Status::EX_ILLEGAL_ARGUMENT);
Biswarup Pal68137fc2023-11-24 18:06:54 +0000311 }
312
313 auto camera = getCamera(token);
314 if (camera == nullptr) {
315 ALOGE(
316 "Attempt to get camera id corresponding to unknown binder token: "
317 "0x%" PRIxPTR,
318 reinterpret_cast<uintptr_t>(token.get()));
319 return ndk::ScopedAStatus::ok();
320 }
321
322 *_aidl_return = camera->getCameraId();
323
324 return ndk::ScopedAStatus::ok();
325}
326
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100327std::shared_ptr<VirtualCameraDevice> VirtualCameraService::getCamera(
328 const ::ndk::SpAIBinder& token) {
329 if (token == nullptr) {
330 return nullptr;
331 }
332
333 std::lock_guard lock(mLock);
334 auto it = mTokenToCameraName.find(token);
335 if (it == mTokenToCameraName.end()) {
336 return nullptr;
337 }
338
339 return mVirtualCameraProvider->getCamera(it->second);
340}
341
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100342binder_status_t VirtualCameraService::handleShellCommand(int, int out, int err,
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100343 const char** args,
344 uint32_t numArgs) {
345 if (numArgs <= 0) {
346 dprintf(out, kShellCmdHelp);
Jan Sebechlebsky76d7e212023-11-28 14:10:25 +0100347 fsync(out);
348 return STATUS_OK;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100349 }
350
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100351 auto isNullptr = [](const char* ptr) { return ptr == nullptr; };
352 if (args == nullptr || std::any_of(args, args + numArgs, isNullptr)) {
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100353 return STATUS_BAD_VALUE;
354 }
Marvin Ramina8196132024-03-15 15:55:22 +0000355
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100356 std::variant<CommandWithOptions, std::string> cmdOrErrorMessage =
357 parseCommand(args, numArgs);
358 if (std::holds_alternative<std::string>(cmdOrErrorMessage)) {
359 dprintf(err, "Error: %s\n",
360 std::get<std::string>(cmdOrErrorMessage).c_str());
361 return STATUS_BAD_VALUE;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100362 }
363
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100364 const CommandWithOptions& cmd =
365 std::get<CommandWithOptions>(cmdOrErrorMessage);
366 binder_status_t status = STATUS_OK;
367 switch (cmd.command) {
368 case Command::HELP:
369 dprintf(out, kShellCmdHelp);
370 break;
371 case Command::ENABLE_TEST_CAMERA:
372 status = enableTestCameraCmd(out, err, cmd.optionToValueMap);
373 break;
374 case Command::DISABLE_TEST_CAMERA:
375 disableTestCameraCmd(out);
376 break;
377 }
378
379 fsync(err);
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100380 fsync(out);
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100381 return status;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100382}
383
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100384binder_status_t VirtualCameraService::enableTestCameraCmd(
385 const int out, const int err,
386 const std::map<std::string, std::string>& options) {
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100387 if (mTestCameraToken != nullptr) {
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100388 dprintf(out, "Test camera is already enabled (%s).\n",
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100389 getCamera(mTestCameraToken)->getCameraName().c_str());
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100390 return STATUS_OK;
391 }
392
393 std::optional<int> cameraId;
394 auto it = options.find("camera_id");
395 if (it != options.end()) {
396 cameraId = parseInt(it->second);
397 if (!cameraId.has_value()) {
398 dprintf(err, "Invalid camera_id: %s\n, must be number > 0",
399 it->second.c_str());
400 return STATUS_BAD_VALUE;
401 }
402 }
403
404 std::optional<LensFacing> lensFacing;
405 it = options.find("lens_facing");
406 if (it != options.end()) {
407 lensFacing = parseLensFacing(it->second);
408 if (!lensFacing.has_value()) {
409 dprintf(err, "Invalid lens_facing: %s\n, must be front|back|external",
410 it->second.c_str());
411 return STATUS_BAD_VALUE;
412 }
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100413 }
414
415 sp<BBinder> token = sp<BBinder>::make();
416 mTestCameraToken.set(AIBinder_fromPlatformBinder(token));
417
418 bool ret;
Jan Sebechlebsky3b478c42023-11-23 13:15:56 +0100419 VirtualCameraConfiguration configuration;
Biswarup Pal6152a302023-12-19 12:44:09 +0000420 configuration.supportedStreamConfigs.push_back({.width = kVgaWidth,
421 .height = kVgaHeight,
422 Format::YUV_420_888,
423 .maxFps = kMaxFps});
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100424 configuration.lensFacing = lensFacing.value_or(LensFacing::EXTERNAL);
425 registerCamera(mTestCameraToken, configuration, cameraId.value_or(sNextId++),
Biswarup Pal37a75182024-01-16 15:53:35 +0000426 kDefaultDeviceId, &ret);
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100427 if (ret) {
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100428 dprintf(out, "Successfully registered test camera %s\n",
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100429 getCamera(mTestCameraToken)->getCameraName().c_str());
430 } else {
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100431 dprintf(err, "Failed to create test camera\n");
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100432 }
Jan Sebechlebsky773c0142024-03-25 12:17:05 +0100433 return STATUS_OK;
Jan Sebechlebsky5cb39962023-11-22 17:33:07 +0100434}
435
436void VirtualCameraService::disableTestCameraCmd(const int out) {
437 if (mTestCameraToken == nullptr) {
438 dprintf(out, "Test camera is not registered.");
439 }
440 unregisterCamera(mTestCameraToken);
441 mTestCameraToken.set(nullptr);
442}
443
444} // namespace virtualcamera
445} // namespace companion
446} // namespace android