• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 #include "Bitmap.h"
17 
18 #include "Caches.h"
19 #include "renderthread/EglManager.h"
20 #include "renderthread/RenderThread.h"
21 #include "renderthread/RenderProxy.h"
22 #include "utils/Color.h"
23 
24 #include <sys/mman.h>
25 
26 #include <log/log.h>
27 #include <cutils/ashmem.h>
28 
29 #include <GLES2/gl2.h>
30 #include <GLES2/gl2ext.h>
31 #include <EGL/egl.h>
32 #include <EGL/eglext.h>
33 
34 #include <private/gui/ComposerService.h>
35 #include <binder/IServiceManager.h>
36 #include <ui/PixelFormat.h>
37 
38 #include <SkCanvas.h>
39 
40 namespace android {
41 
computeAllocationSize(size_t rowBytes,int height,size_t * size)42 static bool computeAllocationSize(size_t rowBytes, int height, size_t* size) {
43     int32_t rowBytes32 = SkToS32(rowBytes);
44     int64_t bigSize = (int64_t) height * rowBytes32;
45     if (rowBytes32 < 0 || !sk_64_isS32(bigSize)) {
46         return false; // allocation will be too large
47     }
48 
49     *size = sk_64_asS32(bigSize);
50     return true;
51 }
52 
53 typedef sk_sp<Bitmap> (*AllocPixeRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes,
54         SkColorTable* ctable);
55 
allocateBitmap(SkBitmap * bitmap,SkColorTable * ctable,AllocPixeRef alloc)56 static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, SkColorTable* ctable, AllocPixeRef alloc) {
57     const SkImageInfo& info = bitmap->info();
58     if (info.colorType() == kUnknown_SkColorType) {
59         LOG_ALWAYS_FATAL("unknown bitmap configuration");
60         return nullptr;
61     }
62 
63     size_t size;
64 
65     // we must respect the rowBytes value already set on the bitmap instead of
66     // attempting to compute our own.
67     const size_t rowBytes = bitmap->rowBytes();
68     if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
69         return nullptr;
70     }
71 
72     auto wrapper = alloc(size, info, rowBytes, ctable);
73     if (wrapper) {
74         wrapper->getSkBitmap(bitmap);
75         // since we're already allocated, we lockPixels right away
76         // HeapAllocator behaves this way too
77         bitmap->lockPixels();
78     }
79     return wrapper;
80 }
81 
allocateAshmemBitmap(SkBitmap * bitmap,SkColorTable * ctable)82 sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap, SkColorTable* ctable) {
83    return allocateBitmap(bitmap, ctable, &Bitmap::allocateAshmemBitmap);
84 }
85 
allocateHeapBitmap(size_t size,const SkImageInfo & info,size_t rowBytes,SkColorTable * ctable)86 static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
87         SkColorTable* ctable) {
88     void* addr = calloc(size, 1);
89     if (!addr) {
90         return nullptr;
91     }
92     return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
93 }
94 
95 #define FENCE_TIMEOUT 2000000000
96 
97 // TODO: handle SRGB sanely
internalFormatToPixelFormat(GLint internalFormat)98 static PixelFormat internalFormatToPixelFormat(GLint internalFormat) {
99     switch (internalFormat) {
100     case GL_LUMINANCE:
101         return PIXEL_FORMAT_RGBA_8888;
102     case GL_SRGB8_ALPHA8:
103         return PIXEL_FORMAT_RGBA_8888;
104     case GL_RGBA:
105         return PIXEL_FORMAT_RGBA_8888;
106     case GL_RGB:
107         return PIXEL_FORMAT_RGB_565;
108     case GL_RGBA16F:
109         return PIXEL_FORMAT_RGBA_FP16;
110     default:
111         LOG_ALWAYS_FATAL("Unsupported bitmap colorType: %d", internalFormat);
112         return PIXEL_FORMAT_UNKNOWN;
113     }
114 }
115 
116 class AutoEglFence {
117 public:
AutoEglFence(EGLDisplay display)118     AutoEglFence(EGLDisplay display)
119             : mDisplay(display) {
120         fence = eglCreateSyncKHR(mDisplay, EGL_SYNC_FENCE_KHR, NULL);
121     }
122 
~AutoEglFence()123     ~AutoEglFence() {
124         if (fence != EGL_NO_SYNC_KHR) {
125             eglDestroySyncKHR(mDisplay, fence);
126         }
127     }
128 
129     EGLSyncKHR fence = EGL_NO_SYNC_KHR;
130 private:
131     EGLDisplay mDisplay = EGL_NO_DISPLAY;
132 };
133 
134 class AutoEglImage {
135 public:
AutoEglImage(EGLDisplay display,EGLClientBuffer clientBuffer)136     AutoEglImage(EGLDisplay display, EGLClientBuffer clientBuffer)
137             : mDisplay(display) {
138         EGLint imageAttrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };
139         image = eglCreateImageKHR(display, EGL_NO_CONTEXT,
140                 EGL_NATIVE_BUFFER_ANDROID, clientBuffer, imageAttrs);
141     }
142 
~AutoEglImage()143     ~AutoEglImage() {
144         if (image != EGL_NO_IMAGE_KHR) {
145             eglDestroyImageKHR(mDisplay, image);
146         }
147     }
148 
149     EGLImageKHR image = EGL_NO_IMAGE_KHR;
150 private:
151     EGLDisplay mDisplay = EGL_NO_DISPLAY;
152 };
153 
154 class AutoGlTexture {
155 public:
AutoGlTexture(uirenderer::Caches & caches)156     AutoGlTexture(uirenderer::Caches& caches)
157             : mCaches(caches) {
158         glGenTextures(1, &mTexture);
159         caches.textureState().bindTexture(mTexture);
160     }
161 
~AutoGlTexture()162     ~AutoGlTexture() {
163         mCaches.textureState().deleteTexture(mTexture);
164     }
165 
166 private:
167     uirenderer::Caches& mCaches;
168     GLuint mTexture = 0;
169 };
170 
uploadBitmapToGraphicBuffer(uirenderer::Caches & caches,SkBitmap & bitmap,GraphicBuffer & buffer,GLint format,GLint type)171 static bool uploadBitmapToGraphicBuffer(uirenderer::Caches& caches, SkBitmap& bitmap,
172         GraphicBuffer& buffer, GLint format, GLint type) {
173     SkAutoLockPixels alp(bitmap);
174     EGLDisplay display = eglGetCurrentDisplay();
175     LOG_ALWAYS_FATAL_IF(display == EGL_NO_DISPLAY,
176                 "Failed to get EGL_DEFAULT_DISPLAY! err=%s",
177                 uirenderer::renderthread::EglManager::eglErrorString());
178     // We use an EGLImage to access the content of the GraphicBuffer
179     // The EGL image is later bound to a 2D texture
180     EGLClientBuffer clientBuffer = (EGLClientBuffer) buffer.getNativeBuffer();
181     AutoEglImage autoImage(display, clientBuffer);
182     if (autoImage.image == EGL_NO_IMAGE_KHR) {
183         ALOGW("Could not create EGL image, err =%s",
184                 uirenderer::renderthread::EglManager::eglErrorString());
185         return false;
186     }
187     AutoGlTexture glTexture(caches);
188     glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, autoImage.image);
189 
190     GL_CHECKPOINT(MODERATE);
191 
192     glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bitmap.width(), bitmap.height(),
193             format, type, bitmap.getPixels());
194 
195     GL_CHECKPOINT(MODERATE);
196 
197     // The fence is used to wait for the texture upload to finish
198     // properly. We cannot rely on glFlush() and glFinish() as
199     // some drivers completely ignore these API calls
200     AutoEglFence autoFence(display);
201     if (autoFence.fence == EGL_NO_SYNC_KHR) {
202         LOG_ALWAYS_FATAL("Could not create sync fence %#x", eglGetError());
203         return false;
204     }
205     // The flag EGL_SYNC_FLUSH_COMMANDS_BIT_KHR will trigger a
206     // pipeline flush (similar to what a glFlush() would do.)
207     EGLint waitStatus = eglClientWaitSyncKHR(display, autoFence.fence,
208             EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, FENCE_TIMEOUT);
209     if (waitStatus != EGL_CONDITION_SATISFIED_KHR) {
210         LOG_ALWAYS_FATAL("Failed to wait for the fence %#x", eglGetError());
211         return false;
212     }
213     return true;
214 }
215 
allocateHardwareBitmap(uirenderer::renderthread::RenderThread & renderThread,SkBitmap & skBitmap)216 sk_sp<Bitmap> Bitmap::allocateHardwareBitmap(uirenderer::renderthread::RenderThread& renderThread,
217         SkBitmap& skBitmap) {
218     renderThread.eglManager().initialize();
219     uirenderer::Caches& caches = uirenderer::Caches::getInstance();
220 
221     const SkImageInfo& info = skBitmap.info();
222     if (info.colorType() == kUnknown_SkColorType || info.colorType() == kAlpha_8_SkColorType) {
223         ALOGW("unable to create hardware bitmap of colortype: %d", info.colorType());
224         return nullptr;
225     }
226 
227     bool needSRGB = uirenderer::transferFunctionCloseToSRGB(skBitmap.info().colorSpace());
228     bool hasLinearBlending = caches.extensions().hasLinearBlending();
229     GLint format, type, internalFormat;
230     uirenderer::Texture::colorTypeToGlFormatAndType(caches, skBitmap.colorType(),
231             needSRGB && hasLinearBlending, &internalFormat, &format, &type);
232 
233     PixelFormat pixelFormat = internalFormatToPixelFormat(internalFormat);
234     sp<GraphicBuffer> buffer = new GraphicBuffer(info.width(), info.height(), pixelFormat,
235             GraphicBuffer::USAGE_HW_TEXTURE |
236             GraphicBuffer::USAGE_SW_WRITE_NEVER |
237             GraphicBuffer::USAGE_SW_READ_NEVER,
238             std::string("Bitmap::allocateHardwareBitmap pid [") + std::to_string(getpid()) + "]");
239 
240     status_t error = buffer->initCheck();
241     if (error < 0) {
242         ALOGW("createGraphicBuffer() failed in GraphicBuffer.create()");
243         return nullptr;
244     }
245 
246     SkBitmap bitmap;
247     if (CC_UNLIKELY(uirenderer::Texture::hasUnsupportedColorType(skBitmap.info(),
248             hasLinearBlending))) {
249         sk_sp<SkColorSpace> sRGB = SkColorSpace::MakeSRGB();
250         bitmap = uirenderer::Texture::uploadToN32(skBitmap, hasLinearBlending, std::move(sRGB));
251     } else {
252         bitmap = skBitmap;
253     }
254 
255     if (!uploadBitmapToGraphicBuffer(caches, bitmap, *buffer, format, type)) {
256         return nullptr;
257     }
258     return sk_sp<Bitmap>(new Bitmap(buffer.get(), bitmap.info()));
259 }
260 
allocateHardwareBitmap(SkBitmap & bitmap)261 sk_sp<Bitmap> Bitmap::allocateHardwareBitmap(SkBitmap& bitmap) {
262     return uirenderer::renderthread::RenderProxy::allocateHardwareBitmap(bitmap);
263 }
264 
allocateHeapBitmap(SkBitmap * bitmap,SkColorTable * ctable)265 sk_sp<Bitmap> Bitmap::allocateHeapBitmap(SkBitmap* bitmap, SkColorTable* ctable) {
266    return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
267 }
268 
allocateHeapBitmap(const SkImageInfo & info)269 sk_sp<Bitmap> Bitmap::allocateHeapBitmap(const SkImageInfo& info) {
270     size_t size;
271     if (!computeAllocationSize(info.minRowBytes(), info.height(), &size)) {
272         LOG_ALWAYS_FATAL("trying to allocate too large bitmap");
273         return nullptr;
274     }
275     return android::allocateHeapBitmap(size, info, info.minRowBytes(), nullptr);
276 }
277 
allocateAshmemBitmap(size_t size,const SkImageInfo & info,size_t rowBytes,SkColorTable * ctable)278 sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info,
279         size_t rowBytes, SkColorTable* ctable) {
280     // Create new ashmem region with read/write priv
281     int fd = ashmem_create_region("bitmap", size);
282     if (fd < 0) {
283         return nullptr;
284     }
285 
286     void* addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
287     if (addr == MAP_FAILED) {
288         close(fd);
289         return nullptr;
290     }
291 
292     if (ashmem_set_prot_region(fd, PROT_READ) < 0) {
293         munmap(addr, size);
294         close(fd);
295         return nullptr;
296     }
297     return sk_sp<Bitmap>(new Bitmap(addr, fd, size, info, rowBytes, ctable));
298 }
299 
FreePixelRef(void * addr,void * context)300 void FreePixelRef(void* addr, void* context) {
301     auto pixelRef = (SkPixelRef*) context;
302     pixelRef->unlockPixels();
303     pixelRef->unref();
304 }
305 
createFrom(const SkImageInfo & info,SkPixelRef & pixelRef)306 sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, SkPixelRef& pixelRef) {
307     pixelRef.ref();
308     pixelRef.lockPixels();
309     return sk_sp<Bitmap>(new Bitmap((void*) pixelRef.pixels(), (void*) &pixelRef, FreePixelRef,
310             info, pixelRef.rowBytes(), pixelRef.colorTable()));
311 }
312 
createFrom(sp<GraphicBuffer> graphicBuffer)313 sk_sp<Bitmap> Bitmap::createFrom(sp<GraphicBuffer> graphicBuffer) {
314     PixelFormat format = graphicBuffer->getPixelFormat();
315     if (!graphicBuffer.get() ||
316             (format != PIXEL_FORMAT_RGBA_8888 && format != PIXEL_FORMAT_RGBA_FP16)) {
317         return nullptr;
318     }
319     SkImageInfo info = SkImageInfo::Make(graphicBuffer->getWidth(), graphicBuffer->getHeight(),
320             kRGBA_8888_SkColorType, kPremul_SkAlphaType,
321             SkColorSpace::MakeSRGB());
322     return sk_sp<Bitmap>(new Bitmap(graphicBuffer.get(), info));
323 }
324 
setColorSpace(sk_sp<SkColorSpace> colorSpace)325 void Bitmap::setColorSpace(sk_sp<SkColorSpace> colorSpace) {
326     // TODO: See todo in reconfigure() below
327     SkImageInfo* myInfo = const_cast<SkImageInfo*>(&this->info());
328     *myInfo = info().makeColorSpace(std::move(colorSpace));
329 }
330 
reconfigure(const SkImageInfo & newInfo,size_t rowBytes,SkColorTable * ctable)331 void Bitmap::reconfigure(const SkImageInfo& newInfo, size_t rowBytes, SkColorTable* ctable) {
332     if (kIndex_8_SkColorType != newInfo.colorType()) {
333         ctable = nullptr;
334     }
335     mRowBytes = rowBytes;
336     if (mColorTable.get() != ctable) {
337         mColorTable.reset(SkSafeRef(ctable));
338     }
339 
340     // Need to validate the alpha type to filter against the color type
341     // to prevent things like a non-opaque RGB565 bitmap
342     SkAlphaType alphaType;
343     LOG_ALWAYS_FATAL_IF(!SkColorTypeValidateAlphaType(
344             newInfo.colorType(), newInfo.alphaType(), &alphaType),
345             "Failed to validate alpha type!");
346 
347     // Dirty hack is dirty
348     // TODO: Figure something out here, Skia's current design makes this
349     // really hard to work with. Skia really, really wants immutable objects,
350     // but with the nested-ref-count hackery going on that's just not
351     // feasible without going insane trying to figure it out
352     SkImageInfo* myInfo = const_cast<SkImageInfo*>(&this->info());
353     *myInfo = newInfo;
354     changeAlphaType(alphaType);
355 
356     // Docs say to only call this in the ctor, but we're going to call
357     // it anyway even if this isn't always the ctor.
358     // TODO: Fix this too as part of the above TODO
359     setPreLocked(getStorage(), mRowBytes, mColorTable.get());
360 }
361 
Bitmap(void * address,size_t size,const SkImageInfo & info,size_t rowBytes,SkColorTable * ctable)362 Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
363             : SkPixelRef(info)
364             , mPixelStorageType(PixelStorageType::Heap) {
365     mPixelStorage.heap.address = address;
366     mPixelStorage.heap.size = size;
367     reconfigure(info, rowBytes, ctable);
368 }
369 
Bitmap(void * address,void * context,FreeFunc freeFunc,const SkImageInfo & info,size_t rowBytes,SkColorTable * ctable)370 Bitmap::Bitmap(void* address, void* context, FreeFunc freeFunc,
371                 const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
372             : SkPixelRef(info)
373             , mPixelStorageType(PixelStorageType::External) {
374     mPixelStorage.external.address = address;
375     mPixelStorage.external.context = context;
376     mPixelStorage.external.freeFunc = freeFunc;
377     reconfigure(info, rowBytes, ctable);
378 }
379 
Bitmap(void * address,int fd,size_t mappedSize,const SkImageInfo & info,size_t rowBytes,SkColorTable * ctable)380 Bitmap::Bitmap(void* address, int fd, size_t mappedSize,
381                 const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
382             : SkPixelRef(info)
383             , mPixelStorageType(PixelStorageType::Ashmem) {
384     mPixelStorage.ashmem.address = address;
385     mPixelStorage.ashmem.fd = fd;
386     mPixelStorage.ashmem.size = mappedSize;
387     reconfigure(info, rowBytes, ctable);
388 }
389 
Bitmap(GraphicBuffer * buffer,const SkImageInfo & info)390 Bitmap::Bitmap(GraphicBuffer* buffer, const SkImageInfo& info)
391         : SkPixelRef(info)
392         , mPixelStorageType(PixelStorageType::Hardware) {
393     mPixelStorage.hardware.buffer = buffer;
394     buffer->incStrong(buffer);
395     mRowBytes = bytesPerPixel(buffer->getPixelFormat()) * buffer->getStride();
396 }
397 
~Bitmap()398 Bitmap::~Bitmap() {
399     switch (mPixelStorageType) {
400     case PixelStorageType::External:
401         mPixelStorage.external.freeFunc(mPixelStorage.external.address,
402                 mPixelStorage.external.context);
403         break;
404     case PixelStorageType::Ashmem:
405         munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
406         close(mPixelStorage.ashmem.fd);
407         break;
408     case PixelStorageType::Heap:
409         free(mPixelStorage.heap.address);
410         break;
411     case PixelStorageType::Hardware:
412         auto buffer = mPixelStorage.hardware.buffer;
413         buffer->decStrong(buffer);
414         mPixelStorage.hardware.buffer = nullptr;
415         break;
416 
417     }
418 
419     android::uirenderer::renderthread::RenderProxy::onBitmapDestroyed(getStableID());
420 }
421 
hasHardwareMipMap() const422 bool Bitmap::hasHardwareMipMap() const {
423     return mHasHardwareMipMap;
424 }
425 
setHasHardwareMipMap(bool hasMipMap)426 void Bitmap::setHasHardwareMipMap(bool hasMipMap) {
427     mHasHardwareMipMap = hasMipMap;
428 }
429 
getStorage() const430 void* Bitmap::getStorage() const {
431     switch (mPixelStorageType) {
432     case PixelStorageType::External:
433         return mPixelStorage.external.address;
434     case PixelStorageType::Ashmem:
435         return mPixelStorage.ashmem.address;
436     case PixelStorageType::Heap:
437         return mPixelStorage.heap.address;
438     case PixelStorageType::Hardware:
439         return nullptr;
440     }
441 }
442 
onNewLockPixels(LockRec * rec)443 bool Bitmap::onNewLockPixels(LockRec* rec) {
444     rec->fPixels = getStorage();
445     rec->fRowBytes = mRowBytes;
446     rec->fColorTable = mColorTable.get();
447     return true;
448 }
449 
getAllocatedSizeInBytes() const450 size_t Bitmap::getAllocatedSizeInBytes() const {
451     return info().getSafeSize(mRowBytes);
452 }
453 
getAshmemFd() const454 int Bitmap::getAshmemFd() const {
455     switch (mPixelStorageType) {
456     case PixelStorageType::Ashmem:
457         return mPixelStorage.ashmem.fd;
458     default:
459         return -1;
460     }
461 }
462 
getAllocationByteCount() const463 size_t Bitmap::getAllocationByteCount() const {
464     switch (mPixelStorageType) {
465     case PixelStorageType::Heap:
466         return mPixelStorage.heap.size;
467     default:
468         return rowBytes() * height();
469     }
470 }
471 
reconfigure(const SkImageInfo & info)472 void Bitmap::reconfigure(const SkImageInfo& info) {
473     reconfigure(info, info.minRowBytes(), nullptr);
474 }
475 
setAlphaType(SkAlphaType alphaType)476 void Bitmap::setAlphaType(SkAlphaType alphaType) {
477     if (!SkColorTypeValidateAlphaType(info().colorType(), alphaType, &alphaType)) {
478         return;
479     }
480 
481     changeAlphaType(alphaType);
482 }
483 
getSkBitmap(SkBitmap * outBitmap)484 void Bitmap::getSkBitmap(SkBitmap* outBitmap) {
485     outBitmap->setHasHardwareMipMap(mHasHardwareMipMap);
486     if (isHardware()) {
487         if (uirenderer::Properties::isSkiaEnabled()) {
488             // TODO: add color correctness for Skia pipeline - pass null color space for now
489             outBitmap->allocPixels(SkImageInfo::Make(info().width(), info().height(),
490                     info().colorType(), info().alphaType(), nullptr));
491         } else {
492             outBitmap->allocPixels(info());
493         }
494         uirenderer::renderthread::RenderProxy::copyGraphicBufferInto(graphicBuffer(), outBitmap);
495         return;
496     }
497     outBitmap->setInfo(info(), rowBytes());
498     outBitmap->setPixelRef(this);
499 }
500 
getSkBitmapForShaders(SkBitmap * outBitmap)501 void Bitmap::getSkBitmapForShaders(SkBitmap* outBitmap) {
502     if (isHardware() && uirenderer::Properties::isSkiaEnabled()) {
503         getSkBitmap(outBitmap);
504     } else {
505         outBitmap->setInfo(info(), rowBytes());
506         outBitmap->setPixelRef(this);
507         outBitmap->setHasHardwareMipMap(mHasHardwareMipMap);
508     }
509 }
510 
getBounds(SkRect * bounds) const511 void Bitmap::getBounds(SkRect* bounds) const {
512     SkASSERT(bounds);
513     bounds->set(0, 0, SkIntToScalar(info().width()), SkIntToScalar(info().height()));
514 }
515 
graphicBuffer()516 GraphicBuffer* Bitmap::graphicBuffer() {
517     if (isHardware()) {
518         return mPixelStorage.hardware.buffer;
519     }
520     return nullptr;
521 }
522 
523 } // namespace android
524