1 /*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
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_TAG "Camera3-ZoomRatioMapper"
18 //#define LOG_NDEBUG 0
19
20 #include <algorithm>
21
22 #include "device3/ZoomRatioMapper.h"
23 #include "utils/SessionConfigurationUtilsHost.h"
24
25 namespace android {
26
27 namespace camera3 {
28
initRemappedKeys()29 void ZoomRatioMapper::initRemappedKeys() {
30 mRemappedKeys.insert(
31 kMeteringRegionsToCorrect.begin(),
32 kMeteringRegionsToCorrect.end());
33 mRemappedKeys.insert(
34 kRectsToCorrect.begin(),
35 kRectsToCorrect.end());
36 mRemappedKeys.insert(
37 kResultPointsToCorrectNoClamp.begin(),
38 kResultPointsToCorrectNoClamp.end());
39
40 mRemappedKeys.insert(ANDROID_CONTROL_ZOOM_RATIO);
41 if (flags::concert_mode()) {
42 mRemappedKeys.insert(ANDROID_LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_SENSOR_CROP_REGION);
43 }
44 }
45
initZoomRatioInTemplate(CameraMetadata * request)46 status_t ZoomRatioMapper::initZoomRatioInTemplate(CameraMetadata *request) {
47 camera_metadata_entry_t entry;
48 entry = request->find(ANDROID_CONTROL_ZOOM_RATIO);
49 float defaultZoomRatio = 1.0f;
50 if (entry.count == 0) {
51 return request->update(ANDROID_CONTROL_ZOOM_RATIO, &defaultZoomRatio, 1);
52 }
53 return OK;
54 }
55
overrideZoomRatioTags(CameraMetadata * deviceInfo,bool * supportNativeZoomRatio)56 status_t ZoomRatioMapper::overrideZoomRatioTags(
57 CameraMetadata* deviceInfo, bool* supportNativeZoomRatio) {
58 if (deviceInfo == nullptr || supportNativeZoomRatio == nullptr) {
59 return BAD_VALUE;
60 }
61
62 camera_metadata_entry_t entry;
63 entry = deviceInfo->find(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
64 if (entry.count != 2 && entry.count != 0) return BAD_VALUE;
65
66 // Hal has zoom ratio support
67 if (entry.count == 2) {
68 *supportNativeZoomRatio = true;
69 return OK;
70 }
71
72 // Hal has no zoom ratio support
73 *supportNativeZoomRatio = false;
74
75 entry = deviceInfo->find(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
76 if (entry.count != 1) {
77 ALOGI("%s: Camera device doesn't support SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key!",
78 __FUNCTION__);
79 return OK;
80 }
81
82 float zoomRange[] = {1.0f, entry.data.f[0]};
83 status_t res = deviceInfo->update(ANDROID_CONTROL_ZOOM_RATIO_RANGE, zoomRange, 2);
84 if (res != OK) {
85 ALOGE("%s: Failed to update CONTROL_ZOOM_RATIO_RANGE key: %s (%d)",
86 __FUNCTION__, strerror(-res), res);
87 return res;
88 }
89
90 std::vector<int32_t> requestKeys;
91 entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS);
92 if (entry.count > 0) {
93 requestKeys.insert(requestKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
94 }
95 requestKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO);
96 res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS,
97 requestKeys.data(), requestKeys.size());
98 if (res != OK) {
99 ALOGE("%s: Failed to update REQUEST_AVAILABLE_REQUEST_KEYS: %s (%d)",
100 __FUNCTION__, strerror(-res), res);
101 return res;
102 }
103
104 std::vector<int32_t> resultKeys;
105 entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS);
106 if (entry.count > 0) {
107 resultKeys.insert(resultKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
108 }
109 resultKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO);
110 res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS,
111 resultKeys.data(), resultKeys.size());
112 if (res != OK) {
113 ALOGE("%s: Failed to update REQUEST_AVAILABLE_RESULT_KEYS: %s (%d)",
114 __FUNCTION__, strerror(-res), res);
115 return res;
116 }
117
118 std::vector<int32_t> charKeys;
119 entry = deviceInfo->find(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS);
120 if (entry.count > 0) {
121 charKeys.insert(charKeys.end(), entry.data.i32, entry.data.i32 + entry.count);
122 }
123 charKeys.push_back(ANDROID_CONTROL_ZOOM_RATIO_RANGE);
124 res = deviceInfo->update(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS,
125 charKeys.data(), charKeys.size());
126 if (res != OK) {
127 ALOGE("%s: Failed to update REQUEST_AVAILABLE_CHARACTERISTICS_KEYS: %s (%d)",
128 __FUNCTION__, strerror(-res), res);
129 return res;
130 }
131
132 return OK;
133 }
134
ZoomRatioMapper(const CameraMetadata * deviceInfo,bool supportNativeZoomRatio,bool usePrecorrectArray)135 ZoomRatioMapper::ZoomRatioMapper(const CameraMetadata* deviceInfo,
136 bool supportNativeZoomRatio, bool usePrecorrectArray) {
137 initRemappedKeys();
138
139 int32_t arrayW = 0;
140 int32_t arrayH = 0;
141 int32_t arrayMaximumResolutionW = 0;
142 int32_t arrayMaximumResolutionH = 0;
143 int32_t activeW = 0;
144 int32_t activeH = 0;
145 int32_t activeMaximumResolutionW = 0;
146 int32_t activeMaximumResolutionH = 0;
147
148 if (!SessionConfigurationUtils::getArrayWidthAndHeight(deviceInfo,
149 ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE, &arrayW, &arrayH)) {
150 ALOGE("%s: Couldn't get pre correction active array size", __FUNCTION__);
151 return;
152 }
153 if (!SessionConfigurationUtils::getArrayWidthAndHeight(deviceInfo,
154 ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE, &activeW, &activeH)) {
155 ALOGE("%s: Couldn't get active array size", __FUNCTION__);
156 return;
157 }
158
159 bool supportsUltraHighResolutionCapture =
160 camera3::SessionConfigurationUtils::supportsUltraHighResolutionCapture(*deviceInfo);
161 if (supportsUltraHighResolutionCapture) {
162 if (!SessionConfigurationUtils::getArrayWidthAndHeight(deviceInfo,
163 ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION,
164 &arrayMaximumResolutionW, &arrayMaximumResolutionH)) {
165 ALOGE("%s: Couldn't get maximum resolution pre correction active array size",
166 __FUNCTION__);
167 return;
168 }
169 if (!SessionConfigurationUtils::getArrayWidthAndHeight(deviceInfo,
170 ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION,
171 &activeMaximumResolutionW, &activeMaximumResolutionH)) {
172 ALOGE("%s: Couldn't get maximum resolution pre correction active array size",
173 __FUNCTION__);
174 return;
175 }
176 }
177
178 if (usePrecorrectArray) {
179 mArrayWidth = arrayW;
180 mArrayHeight = arrayH;
181 mArrayWidthMaximumResolution = arrayMaximumResolutionW;
182 mArrayHeightMaximumResolution = arrayMaximumResolutionH;
183 } else {
184 mArrayWidth = activeW;
185 mArrayHeight = activeH;
186 mArrayWidthMaximumResolution = activeMaximumResolutionW;
187 mArrayHeightMaximumResolution = activeMaximumResolutionH;
188 }
189 mHalSupportsZoomRatio = supportNativeZoomRatio;
190
191 ALOGV("%s: array size: %d x %d, full res array size: %d x %d, mHalSupportsZoomRatio %d",
192 __FUNCTION__, mArrayWidth, mArrayHeight, mArrayWidthMaximumResolution,
193 mArrayHeightMaximumResolution, mHalSupportsZoomRatio);
194 mIsValid = true;
195 }
196
getArrayDimensionsToBeUsed(const CameraMetadata * settings,int32_t * arrayWidth,int32_t * arrayHeight)197 status_t ZoomRatioMapper::getArrayDimensionsToBeUsed(const CameraMetadata *settings,
198 int32_t *arrayWidth, int32_t *arrayHeight) {
199 if (settings == nullptr || arrayWidth == nullptr || arrayHeight == nullptr) {
200 return BAD_VALUE;
201 }
202 // First we get the sensorPixelMode from the settings metadata.
203 int32_t sensorPixelMode = ANDROID_SENSOR_PIXEL_MODE_DEFAULT;
204 camera_metadata_ro_entry sensorPixelModeEntry = settings->find(ANDROID_SENSOR_PIXEL_MODE);
205 if (sensorPixelModeEntry.count != 0) {
206 sensorPixelMode = sensorPixelModeEntry.data.u8[0];
207 if (sensorPixelMode != ANDROID_SENSOR_PIXEL_MODE_DEFAULT &&
208 sensorPixelMode != ANDROID_SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION) {
209 ALOGE("%s: Request sensor pixel mode is not one of the valid values %d",
210 __FUNCTION__, sensorPixelMode);
211 return BAD_VALUE;
212 }
213 }
214 if (sensorPixelMode == ANDROID_SENSOR_PIXEL_MODE_DEFAULT) {
215 *arrayWidth = mArrayWidth;
216 *arrayHeight = mArrayHeight;
217 } else {
218 *arrayWidth = mArrayWidthMaximumResolution;
219 *arrayHeight = mArrayHeightMaximumResolution;
220 }
221 return OK;
222 }
223
updateCaptureRequest(CameraMetadata * request)224 status_t ZoomRatioMapper::updateCaptureRequest(CameraMetadata* request) {
225 if (!mIsValid) return INVALID_OPERATION;
226
227 status_t res = OK;
228 bool zoomRatioIs1 = true;
229 camera_metadata_entry_t entry;
230 int arrayHeight, arrayWidth = 0;
231 res = getArrayDimensionsToBeUsed(request, &arrayWidth, &arrayHeight);
232 if (res != OK) {
233 return res;
234 }
235 entry = request->find(ANDROID_CONTROL_ZOOM_RATIO);
236 if (entry.count == 1 && entry.data.f[0] != 1.0f) {
237 zoomRatioIs1 = false;
238
239 // If cropRegion is windowboxing, override it with activeArray
240 camera_metadata_entry_t cropRegionEntry = request->find(ANDROID_SCALER_CROP_REGION);
241 if (cropRegionEntry.count == 4) {
242 int cropWidth = cropRegionEntry.data.i32[2];
243 int cropHeight = cropRegionEntry.data.i32[3];
244 if (cropWidth < arrayWidth && cropHeight < arrayHeight) {
245 cropRegionEntry.data.i32[0] = 0;
246 cropRegionEntry.data.i32[1] = 0;
247 cropRegionEntry.data.i32[2] = arrayWidth;
248 cropRegionEntry.data.i32[3] = arrayHeight;
249 }
250 }
251 }
252
253 if (mHalSupportsZoomRatio && zoomRatioIs1) {
254 res = separateZoomFromCropLocked(request, false/*isResult*/, arrayWidth, arrayHeight);
255 } else if (!mHalSupportsZoomRatio && !zoomRatioIs1) {
256 res = combineZoomAndCropLocked(request, false/*isResult*/, arrayWidth, arrayHeight);
257 }
258
259 // If CONTROL_ZOOM_RATIO is in request, but HAL doesn't support
260 // CONTROL_ZOOM_RATIO, remove it from the request.
261 if (!mHalSupportsZoomRatio && entry.count == 1) {
262 request->erase(ANDROID_CONTROL_ZOOM_RATIO);
263 }
264
265 return res;
266 }
267
updateCaptureResult(CameraMetadata * result,bool requestedZoomRatioIs1)268 status_t ZoomRatioMapper::updateCaptureResult(CameraMetadata* result, bool requestedZoomRatioIs1) {
269 if (!mIsValid) return INVALID_OPERATION;
270
271 status_t res = OK;
272
273 int arrayHeight, arrayWidth = 0;
274 res = getArrayDimensionsToBeUsed(result, &arrayWidth, &arrayHeight);
275 if (res != OK) {
276 return res;
277 }
278 if (mHalSupportsZoomRatio && requestedZoomRatioIs1) {
279 res = combineZoomAndCropLocked(result, true/*isResult*/, arrayWidth, arrayHeight);
280 } else if (!mHalSupportsZoomRatio && !requestedZoomRatioIs1) {
281 res = separateZoomFromCropLocked(result, true/*isResult*/, arrayWidth, arrayHeight);
282 } else {
283 camera_metadata_entry_t entry = result->find(ANDROID_CONTROL_ZOOM_RATIO);
284 if (entry.count == 0) {
285 float zoomRatio1x = 1.0f;
286 result->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio1x, 1);
287 }
288 }
289
290 return res;
291 }
292
deriveZoomRatio(const CameraMetadata * metadata,float * zoomRatioRet,int arrayWidth,int arrayHeight)293 status_t ZoomRatioMapper::deriveZoomRatio(const CameraMetadata* metadata, float *zoomRatioRet,
294 int arrayWidth, int arrayHeight) {
295 if (metadata == nullptr || zoomRatioRet == nullptr) {
296 return BAD_VALUE;
297 }
298 float zoomRatio = 1.0;
299
300 camera_metadata_ro_entry_t entry;
301 entry = metadata->find(ANDROID_SCALER_CROP_REGION);
302 if (entry.count != 4) {
303 *zoomRatioRet = 1;
304 return OK;
305 }
306 // Center of the preCorrection/active size
307 float arrayCenterX = arrayWidth / 2.0;
308 float arrayCenterY = arrayHeight / 2.0;
309
310 // Re-map crop region to coordinate system centered to (arrayCenterX,
311 // arrayCenterY).
312 float cropRegionLeft = arrayCenterX - entry.data.i32[0] ;
313 float cropRegionTop = arrayCenterY - entry.data.i32[1];
314 float cropRegionRight = entry.data.i32[0] + entry.data.i32[2] - arrayCenterX;
315 float cropRegionBottom = entry.data.i32[1] + entry.data.i32[3] - arrayCenterY;
316
317 // Calculate the scaling factor for left, top, bottom, right
318 float zoomRatioLeft = std::max(arrayWidth / (2 * cropRegionLeft), 1.0f);
319 float zoomRatioTop = std::max(arrayHeight / (2 * cropRegionTop), 1.0f);
320 float zoomRatioRight = std::max(arrayWidth / (2 * cropRegionRight), 1.0f);
321 float zoomRatioBottom = std::max(arrayHeight / (2 * cropRegionBottom), 1.0f);
322
323 // Use minimum scaling factor to handle letterboxing or pillarboxing
324 zoomRatio = std::min(std::min(zoomRatioLeft, zoomRatioRight),
325 std::min(zoomRatioTop, zoomRatioBottom));
326
327 ALOGV("%s: derived zoomRatio is %f", __FUNCTION__, zoomRatio);
328 *zoomRatioRet = zoomRatio;
329 return OK;
330 }
331
separateZoomFromCropLocked(CameraMetadata * metadata,bool isResult,int arrayWidth,int arrayHeight)332 status_t ZoomRatioMapper::separateZoomFromCropLocked(CameraMetadata* metadata, bool isResult,
333 int arrayWidth, int arrayHeight) {
334 float zoomRatio = 1.0;
335 status_t res = deriveZoomRatio(metadata, &zoomRatio, arrayWidth, arrayHeight);
336
337 if (res != OK) {
338 ALOGE("%s: Failed to derive zoom ratio: %s(%d)",
339 __FUNCTION__, strerror(-res), res);
340 return res;
341 }
342
343 // Update zoomRatio metadata tag
344 res = metadata->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
345 if (res != OK) {
346 ALOGE("%s: Failed to update ANDROID_CONTROL_ZOOM_RATIO: %s(%d)",
347 __FUNCTION__, strerror(-res), res);
348 return res;
349 }
350
351 // Scale regions using zoomRatio
352 camera_metadata_entry_t entry;
353 for (auto region : kMeteringRegionsToCorrect) {
354 entry = metadata->find(region);
355 for (size_t j = 0; j < entry.count; j += 5) {
356 int32_t weight = entry.data.i32[j + 4];
357 if (weight == 0) {
358 continue;
359 }
360 scaleRegion(entry.data.i32 + j, zoomRatio, arrayWidth,
361 arrayHeight);
362 }
363 }
364
365 for (auto rect : kRectsToCorrect) {
366 entry = metadata->find(rect);
367 scaleRects(entry.data.i32, entry.count / 4, zoomRatio, arrayWidth, arrayHeight);
368 }
369
370 if (isResult) {
371 for (auto pts : kResultPointsToCorrectNoClamp) {
372 entry = metadata->find(pts);
373 scaleCoordinates(entry.data.i32, entry.count / 2, zoomRatio, false /*clamp*/,
374 arrayWidth, arrayHeight);
375 }
376 }
377
378 return OK;
379 }
380
combineZoomAndCropLocked(CameraMetadata * metadata,bool isResult,int arrayWidth,int arrayHeight)381 status_t ZoomRatioMapper::combineZoomAndCropLocked(CameraMetadata* metadata, bool isResult,
382 int arrayWidth, int arrayHeight) {
383 float zoomRatio = 1.0f;
384 camera_metadata_entry_t entry;
385 entry = metadata->find(ANDROID_CONTROL_ZOOM_RATIO);
386 if (entry.count == 1) {
387 zoomRatio = entry.data.f[0];
388 }
389
390 // Unscale regions with zoomRatio
391 for (auto region : kMeteringRegionsToCorrect) {
392 entry = metadata->find(region);
393 for (size_t j = 0; j < entry.count; j += 5) {
394 int32_t weight = entry.data.i32[j + 4];
395 if (weight == 0) {
396 continue;
397 }
398 scaleRegion(entry.data.i32 + j, 1.0 / zoomRatio, arrayWidth,
399 arrayHeight);
400 }
401 }
402 for (auto rect : kRectsToCorrect) {
403 entry = metadata->find(rect);
404 scaleRects(entry.data.i32, entry.count / 4, 1.0 / zoomRatio, arrayWidth, arrayHeight);
405 }
406 if (isResult) {
407 for (auto pts : kResultPointsToCorrectNoClamp) {
408 entry = metadata->find(pts);
409 scaleCoordinates(entry.data.i32, entry.count / 2, 1.0 / zoomRatio, false /*clamp*/,
410 arrayWidth, arrayHeight);
411 }
412 }
413
414 zoomRatio = 1.0;
415 status_t res = metadata->update(ANDROID_CONTROL_ZOOM_RATIO, &zoomRatio, 1);
416 if (res != OK) {
417 return res;
418 }
419
420 return OK;
421 }
422
scaleCoordinates(int32_t * coordPairs,int coordCount,float scaleRatio,bool clamp,int32_t arrayWidth,int32_t arrayHeight)423 void ZoomRatioMapper::scaleCoordinates(int32_t* coordPairs, int coordCount,
424 float scaleRatio, bool clamp, int32_t arrayWidth, int32_t arrayHeight) {
425 // A pixel's coordinate is represented by the position of its top-left corner.
426 // To avoid the rounding error, we use the coordinate for the center of the
427 // pixel instead:
428 // 1. First shift the coordinate system half pixel both horizontally and
429 // vertically, so that [x, y] is the center of the pixel, not the top-left corner.
430 // 2. Do zoom operation to scale the coordinate relative to the center of
431 // the active array (shifted by 0.5 pixel as well).
432 // 3. Shift the coordinate system back by directly using the pixel center
433 // coordinate.
434 for (int i = 0; i < coordCount * 2; i += 2) {
435 float x = coordPairs[i];
436 float y = coordPairs[i + 1];
437 float xCentered = x - (arrayWidth - 2) / 2;
438 float yCentered = y - (arrayHeight - 2) / 2;
439 float scaledX = xCentered * scaleRatio;
440 float scaledY = yCentered * scaleRatio;
441 scaledX += (arrayWidth - 2) / 2;
442 scaledY += (arrayHeight - 2) / 2;
443 coordPairs[i] = static_cast<int32_t>(std::round(scaledX));
444 coordPairs[i+1] = static_cast<int32_t>(std::round(scaledY));
445 // Clamp to within activeArray/preCorrectionActiveArray
446 if (clamp) {
447 int32_t right = arrayWidth - 1;
448 int32_t bottom = arrayHeight - 1;
449 coordPairs[i] =
450 std::min(right, std::max(0, coordPairs[i]));
451 coordPairs[i+1] =
452 std::min(bottom, std::max(0, coordPairs[i+1]));
453 }
454 ALOGV("%s: coordinates: %d, %d", __FUNCTION__, coordPairs[i], coordPairs[i+1]);
455 }
456 }
457
scaleRegion(int32_t * region,float scaleRatio,int32_t arrayWidth,int32_t arrayHeight)458 void ZoomRatioMapper::scaleRegion(int32_t* region, float scaleRatio,
459 int32_t arrayWidth, int32_t arrayHeight) {
460 // Top-left (inclusive)
461 scaleCoordinates(region, 1, scaleRatio, true /*clamp*/, arrayWidth,
462 arrayHeight);
463 // Bottom-right (exclusive): Use adjacent inclusive pixel to
464 // calculate.
465 region[2] -= 1;
466 region[3] -= 1;
467 scaleCoordinates(region + 2, 1, scaleRatio, true /*clamp*/, arrayWidth,
468 arrayHeight);
469 region[2] += 1;
470 region[3] += 1;
471 // Make sure bottom-right >= top-left
472 region[2] = std::max(region[0], region[2]);
473 region[3] = std::max(region[1], region[3]);
474 }
475
scaleRects(int32_t * rects,int rectCount,float scaleRatio,int32_t arrayWidth,int32_t arrayHeight)476 void ZoomRatioMapper::scaleRects(int32_t* rects, int rectCount,
477 float scaleRatio, int32_t arrayWidth, int32_t arrayHeight) {
478 for (int i = 0; i < rectCount * 4; i += 4) {
479 // Map from (l, t, width, height) to (l, t, l+width-1, t+height-1),
480 // where both top-left and bottom-right are inclusive.
481 int32_t coords[4] = {
482 rects[i],
483 rects[i + 1],
484 rects[i] + rects[i + 2] - 1,
485 rects[i + 1] + rects[i + 3] - 1
486 };
487
488 // top-left
489 scaleCoordinates(coords, 1, scaleRatio, true /*clamp*/, arrayWidth, arrayHeight);
490 // bottom-right
491 scaleCoordinates(coords+2, 1, scaleRatio, true /*clamp*/, arrayWidth, arrayHeight);
492
493 // Map back to (l, t, width, height)
494 rects[i] = coords[0];
495 rects[i + 1] = coords[1];
496 rects[i + 2] = coords[2] - coords[0] + 1;
497 rects[i + 3] = coords[3] - coords[1] + 1;
498 }
499 }
500
501 } // namespace camera3
502
503 } // namespace android
504