android_database_CursorWindow.cpp revision 6141e13f6e84846ae531358a8bcbf6d2102b1bd4
1/* 2 * Copyright (C) 2007 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#undef LOG_TAG 18#define LOG_TAG "CursorWindow" 19 20#include <jni.h> 21#include <JNIHelp.h> 22#include <android_runtime/AndroidRuntime.h> 23 24#include <utils/Log.h> 25#include <utils/String8.h> 26#include <utils/String16.h> 27 28#include <stdio.h> 29#include <string.h> 30#include <unistd.h> 31 32#include "binder/CursorWindow.h" 33#include "sqlite3_exception.h" 34#include "android_util_Binder.h" 35 36 37namespace android { 38 39static jfieldID gWindowField; 40static jfieldID gBufferField; 41static jfieldID gSizeCopiedField; 42 43#define GET_WINDOW(env, object) ((CursorWindow *)env->GetIntField(object, gWindowField)) 44#define SET_WINDOW(env, object, window) (env->SetIntField(object, gWindowField, (int)window)) 45#define SET_BUFFER(env, object, buf) (env->SetObjectField(object, gBufferField, buf)) 46#define SET_SIZE_COPIED(env, object, size) (env->SetIntField(object, gSizeCopiedField, size)) 47 48CursorWindow * get_window_from_object(JNIEnv * env, jobject javaWindow) 49{ 50 return GET_WINDOW(env, javaWindow); 51} 52 53static jint native_init_empty(JNIEnv * env, jobject object, jint cursorWindowSize, 54 jboolean localOnly) 55{ 56 uint8_t * data; 57 size_t size; 58 CursorWindow * window; 59 60 window = new CursorWindow(cursorWindowSize); 61 if (!window) { 62 return 1; 63 } 64 if (!window->initBuffer(localOnly)) { 65 delete window; 66 return 1; 67 } 68 69 LOG_WINDOW("native_init_empty: window = %p", window); 70 SET_WINDOW(env, object, window); 71 return 0; 72} 73 74static jint native_init_memory(JNIEnv * env, jobject object, jobject memObj) 75{ 76 sp<IMemory> memory = interface_cast<IMemory>(ibinderForJavaObject(env, memObj)); 77 if (memory == NULL) { 78 jniThrowException(env, "java/lang/IllegalStateException", "Couldn't get native binder"); 79 return 1; 80 } 81 82 CursorWindow * window = new CursorWindow(); 83 if (!window) { 84 return 1; 85 } 86 if (!window->setMemory(memory)) { 87 delete window; 88 return 1; 89 } 90 91 LOG_WINDOW("native_init_memory: numRows = %d, numColumns = %d, window = %p", window->getNumRows(), window->getNumColumns(), window); 92 SET_WINDOW(env, object, window); 93 return 0; 94} 95 96static jobject native_getBinder(JNIEnv * env, jobject object) 97{ 98 CursorWindow * window = GET_WINDOW(env, object); 99 if (window) { 100 sp<IMemory> memory = window->getMemory(); 101 if (memory != NULL) { 102 sp<IBinder> binder = memory->asBinder(); 103 return javaObjectForIBinder(env, binder); 104 } 105 } 106 return NULL; 107} 108 109static void native_clear(JNIEnv * env, jobject object) 110{ 111 CursorWindow * window = GET_WINDOW(env, object); 112LOG_WINDOW("Clearing window %p", window); 113 if (window == NULL) { 114 jniThrowException(env, "java/lang/IllegalStateException", "clear() called after close()"); 115 return; 116 } 117 window->clear(); 118} 119 120static void native_close(JNIEnv * env, jobject object) 121{ 122 CursorWindow * window = GET_WINDOW(env, object); 123 if (window) { 124LOG_WINDOW("Closing window %p", window); 125 delete window; 126 SET_WINDOW(env, object, 0); 127 } 128} 129 130static void throwExceptionWithRowCol(JNIEnv * env, jint row, jint column) 131{ 132 char buf[200]; 133 snprintf(buf, sizeof(buf), "Couldn't read row %d, col %d from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it", 134 row, column); 135 jniThrowException(env, "java/lang/IllegalStateException", buf); 136} 137 138static void throwUnknowTypeException(JNIEnv * env, jint type) 139{ 140 char buf[80]; 141 snprintf(buf, sizeof(buf), "UNKNOWN type %d", type); 142 jniThrowException(env, "java/lang/IllegalStateException", buf); 143} 144 145static jlong getLong_native(JNIEnv * env, jobject object, jint row, jint column) 146{ 147 int32_t err; 148 CursorWindow * window = GET_WINDOW(env, object); 149LOG_WINDOW("Getting long for %d,%d from %p", row, column, window); 150 151 field_slot_t field; 152 err = window->read_field_slot(row, column, &field); 153 if (err != 0) { 154 throwExceptionWithRowCol(env, row, column); 155 return 0; 156 } 157 158 uint8_t type = field.type; 159 if (type == FIELD_TYPE_INTEGER) { 160 int64_t value; 161 if (window->getLong(row, column, &value)) { 162 return value; 163 } 164 return 0; 165 } else if (type == FIELD_TYPE_STRING) { 166 uint32_t size = field.data.buffer.size; 167 if (size > 0) { 168#if WINDOW_STORAGE_UTF8 169 return strtoll((char const *)window->offsetToPtr(field.data.buffer.offset), NULL, 0); 170#else 171 String8 ascii((char16_t *) window->offsetToPtr(field.data.buffer.offset), size / 2); 172 char const * str = ascii.string(); 173 return strtoll(str, NULL, 0); 174#endif 175 } else { 176 return 0; 177 } 178 } else if (type == FIELD_TYPE_FLOAT) { 179 double value; 180 if (window->getDouble(row, column, &value)) { 181 return value; 182 } 183 return 0; 184 } else if (type == FIELD_TYPE_NULL) { 185 return 0; 186 } else if (type == FIELD_TYPE_BLOB) { 187 throw_sqlite3_exception(env, "Unable to convert BLOB to long"); 188 return 0; 189 } else { 190 throwUnknowTypeException(env, type); 191 return 0; 192 } 193} 194 195static jbyteArray getBlob_native(JNIEnv* env, jobject object, jint row, jint column) 196{ 197 int32_t err; 198 CursorWindow * window = GET_WINDOW(env, object); 199LOG_WINDOW("Getting blob for %d,%d from %p", row, column, window); 200 201 field_slot_t field; 202 err = window->read_field_slot(row, column, &field); 203 if (err != 0) { 204 throwExceptionWithRowCol(env, row, column); 205 return NULL; 206 } 207 208 uint8_t type = field.type; 209 if (type == FIELD_TYPE_BLOB || type == FIELD_TYPE_STRING) { 210 jbyteArray byteArray = env->NewByteArray(field.data.buffer.size); 211 LOG_ASSERT(byteArray, "Native could not create new byte[]"); 212 env->SetByteArrayRegion(byteArray, 0, field.data.buffer.size, 213 (const jbyte*)window->offsetToPtr(field.data.buffer.offset)); 214 return byteArray; 215 } else if (type == FIELD_TYPE_INTEGER) { 216 throw_sqlite3_exception(env, "INTEGER data in getBlob_native "); 217 } else if (type == FIELD_TYPE_FLOAT) { 218 throw_sqlite3_exception(env, "FLOAT data in getBlob_native "); 219 } else if (type == FIELD_TYPE_NULL) { 220 // do nothing 221 } else { 222 throwUnknowTypeException(env, type); 223 } 224 return NULL; 225} 226 227static jstring getString_native(JNIEnv* env, jobject object, jint row, jint column) 228{ 229 int32_t err; 230 CursorWindow * window = GET_WINDOW(env, object); 231LOG_WINDOW("Getting string for %d,%d from %p", row, column, window); 232 233 field_slot_t field; 234 err = window->read_field_slot(row, column, &field); 235 if (err != 0) { 236 throwExceptionWithRowCol(env, row, column); 237 return NULL; 238 } 239 240 uint8_t type = field.type; 241 if (type == FIELD_TYPE_STRING) { 242 uint32_t size = field.data.buffer.size; 243 if (size > 0) { 244#if WINDOW_STORAGE_UTF8 245 // Pass size - 1 since the UTF8 is null terminated and we don't want a null terminator on the UTF16 string 246 String16 utf16((char const *)window->offsetToPtr(field.data.buffer.offset), size - 1); 247 return env->NewString((jchar const *)utf16.string(), utf16.size()); 248#else 249 return env->NewString((jchar const *)window->offsetToPtr(field.data.buffer.offset), size / 2); 250#endif 251 } else { 252 return env->NewStringUTF(""); 253 } 254 } else if (type == FIELD_TYPE_INTEGER) { 255 int64_t value; 256 if (window->getLong(row, column, &value)) { 257 char buf[32]; 258 snprintf(buf, sizeof(buf), "%lld", value); 259 return env->NewStringUTF(buf); 260 } 261 return NULL; 262 } else if (type == FIELD_TYPE_FLOAT) { 263 double value; 264 if (window->getDouble(row, column, &value)) { 265 char buf[32]; 266 snprintf(buf, sizeof(buf), "%g", value); 267 return env->NewStringUTF(buf); 268 } 269 return NULL; 270 } else if (type == FIELD_TYPE_NULL) { 271 return NULL; 272 } else if (type == FIELD_TYPE_BLOB) { 273 throw_sqlite3_exception(env, "Unable to convert BLOB to string"); 274 return NULL; 275 } else { 276 throwUnknowTypeException(env, type); 277 return NULL; 278 } 279} 280 281/** 282 * Use this only to convert characters that are known to be within the 283 * 0-127 range for direct conversion to UTF-16 284 */ 285static jint charToJchar(const char* src, jchar* dst, jint bufferSize) 286{ 287 int32_t len = strlen(src); 288 289 if (bufferSize < len) { 290 len = bufferSize; 291 } 292 293 for (int i = 0; i < len; i++) { 294 *dst++ = (*src++ & 0x7F); 295 } 296 return len; 297} 298 299static jcharArray copyStringToBuffer_native(JNIEnv* env, jobject object, jint row, 300 jint column, jint bufferSize, jobject buf) 301{ 302 int32_t err; 303 CursorWindow * window = GET_WINDOW(env, object); 304LOG_WINDOW("Copying string for %d,%d from %p", row, column, window); 305 306 field_slot_t field; 307 err = window->read_field_slot(row, column, &field); 308 if (err != 0) { 309 jniThrowException(env, "java/lang/IllegalStateException", "Unable to get field slot"); 310 return NULL; 311 } 312 313 jcharArray buffer = (jcharArray)env->GetObjectField(buf, gBufferField); 314 if (buffer == NULL) { 315 jniThrowException(env, "java/lang/IllegalStateException", "buf should not be null"); 316 return NULL; 317 } 318 jchar* dst = env->GetCharArrayElements(buffer, NULL); 319 uint8_t type = field.type; 320 uint32_t sizeCopied = 0; 321 jcharArray newArray = NULL; 322 if (type == FIELD_TYPE_STRING) { 323 uint32_t size = field.data.buffer.size; 324 if (size > 0) { 325#if WINDOW_STORAGE_UTF8 326 // Pass size - 1 since the UTF8 is null terminated and we don't want a null terminator on the UTF16 string 327 String16 utf16((char const *)window->offsetToPtr(field.data.buffer.offset), size - 1); 328 int32_t strSize = utf16.size(); 329 if (strSize > bufferSize || dst == NULL) { 330 newArray = env->NewCharArray(strSize); 331 env->SetCharArrayRegion(newArray, 0, strSize, (jchar const *)utf16.string()); 332 } else { 333 memcpy(dst, (jchar const *)utf16.string(), strSize * 2); 334 } 335 sizeCopied = strSize; 336#else 337 sizeCopied = size/2 + size % 2; 338 if (size > bufferSize * 2 || dst == NULL) { 339 newArray = env->NewCharArray(sizeCopied); 340 memcpy(newArray, (jchar const *)window->offsetToPtr(field.data.buffer.offset), size); 341 } else { 342 memcpy(dst, (jchar const *)window->offsetToPtr(field.data.buffer.offset), size); 343 } 344#endif 345 } 346 } else if (type == FIELD_TYPE_INTEGER) { 347 int64_t value; 348 if (window->getLong(row, column, &value)) { 349 char buf[32]; 350 int len; 351 snprintf(buf, sizeof(buf), "%lld", value); 352 jchar* dst = env->GetCharArrayElements(buffer, NULL); 353 sizeCopied = charToJchar(buf, dst, bufferSize); 354 } 355 } else if (type == FIELD_TYPE_FLOAT) { 356 double value; 357 if (window->getDouble(row, column, &value)) { 358 char tempbuf[32]; 359 snprintf(tempbuf, sizeof(tempbuf), "%g", value); 360 jchar* dst = env->GetCharArrayElements(buffer, NULL); 361 sizeCopied = charToJchar(tempbuf, dst, bufferSize); 362 } 363 } else if (type == FIELD_TYPE_NULL) { 364 } else if (type == FIELD_TYPE_BLOB) { 365 throw_sqlite3_exception(env, "Unable to convert BLOB to string"); 366 } else { 367 LOGE("Unknown field type %d", type); 368 throw_sqlite3_exception(env, "UNKNOWN type in copyStringToBuffer_native()"); 369 } 370 SET_SIZE_COPIED(env, buf, sizeCopied); 371 env->ReleaseCharArrayElements(buffer, dst, JNI_OK); 372 return newArray; 373} 374 375static jdouble getDouble_native(JNIEnv* env, jobject object, jint row, jint column) 376{ 377 int32_t err; 378 CursorWindow * window = GET_WINDOW(env, object); 379LOG_WINDOW("Getting double for %d,%d from %p", row, column, window); 380 381 field_slot_t field; 382 err = window->read_field_slot(row, column, &field); 383 if (err != 0) { 384 throwExceptionWithRowCol(env, row, column); 385 return 0.0; 386 } 387 388 uint8_t type = field.type; 389 if (type == FIELD_TYPE_FLOAT) { 390 double value; 391 if (window->getDouble(row, column, &value)) { 392 return value; 393 } 394 return 0.0; 395 } else if (type == FIELD_TYPE_STRING) { 396 uint32_t size = field.data.buffer.size; 397 if (size > 0) { 398#if WINDOW_STORAGE_UTF8 399 return strtod((char const *)window->offsetToPtr(field.data.buffer.offset), NULL); 400#else 401 String8 ascii((char16_t *) window->offsetToPtr(field.data.buffer.offset), size / 2); 402 char const * str = ascii.string(); 403 return strtod(str, NULL); 404#endif 405 } else { 406 return 0.0; 407 } 408 } else if (type == FIELD_TYPE_INTEGER) { 409 int64_t value; 410 if (window->getLong(row, column, &value)) { 411 return (double) value; 412 } 413 return 0.0; 414 } else if (type == FIELD_TYPE_NULL) { 415 return 0.0; 416 } else if (type == FIELD_TYPE_BLOB) { 417 throw_sqlite3_exception(env, "Unable to convert BLOB to double"); 418 return 0.0; 419 } else { 420 throwUnknowTypeException(env, type); 421 return 0.0; 422 } 423} 424 425bool isNull_native(CursorWindow *window, jint row, jint column) 426{ 427 LOG_WINDOW("Checking for NULL at %d,%d from %p", row, column, window); 428 429 bool isNull; 430 if (window->getNull(row, column, &isNull)) { 431 return isNull; 432 } 433 434 //TODO throw execption? 435 return true; 436} 437 438static jint getNumRows(JNIEnv * env, jobject object) 439{ 440 CursorWindow * window = GET_WINDOW(env, object); 441 return window->getNumRows(); 442} 443 444static jboolean setNumColumns(JNIEnv * env, jobject object, jint columnNum) 445{ 446 CursorWindow * window = GET_WINDOW(env, object); 447 return window->setNumColumns(columnNum); 448} 449 450static jboolean allocRow(JNIEnv * env, jobject object) 451{ 452 CursorWindow * window = GET_WINDOW(env, object); 453 return window->allocRow() != NULL; 454} 455 456static jboolean putBlob_native(JNIEnv * env, jobject object, jbyteArray value, jint row, jint col) 457{ 458 CursorWindow * window = GET_WINDOW(env, object); 459 if (!value) { 460 LOG_WINDOW("How did a null value send to here"); 461 return false; 462 } 463 field_slot_t * fieldSlot = window->getFieldSlotWithCheck(row, col); 464 if (fieldSlot == NULL) { 465 LOG_WINDOW(" getFieldSlotWithCheck error "); 466 return false; 467 } 468 469 jint len = env->GetArrayLength(value); 470 int offset = window->alloc(len); 471 if (!offset) { 472 LOG_WINDOW("Failed allocating %u bytes", len); 473 return false; 474 } 475 jbyte * bytes = env->GetByteArrayElements(value, NULL); 476 window->copyIn(offset, (uint8_t const *)bytes, len); 477 478 // This must be updated after the call to alloc(), since that 479 // may move the field around in the window 480 fieldSlot->type = FIELD_TYPE_BLOB; 481 fieldSlot->data.buffer.offset = offset; 482 fieldSlot->data.buffer.size = len; 483 env->ReleaseByteArrayElements(value, bytes, JNI_ABORT); 484 LOG_WINDOW("%d,%d is BLOB with %u bytes @ %d", row, col, len, offset); 485 return true; 486} 487 488static jboolean putString_native(JNIEnv * env, jobject object, jstring value, jint row, jint col) 489{ 490 CursorWindow * window = GET_WINDOW(env, object); 491 if (!value) { 492 LOG_WINDOW("How did a null value send to here"); 493 return false; 494 } 495 field_slot_t * fieldSlot = window->getFieldSlotWithCheck(row, col); 496 if (fieldSlot == NULL) { 497 LOG_WINDOW(" getFieldSlotWithCheck error "); 498 return false; 499 } 500 501#if WINDOW_STORAGE_UTF8 502 int len = env->GetStringUTFLength(value) + 1; 503 char const * valStr = env->GetStringUTFChars(value, NULL); 504#else 505 int len = env->GetStringLength(value); 506 // GetStringLength return number of chars and one char takes 2 bytes 507 len *= 2; 508 const jchar* valStr = env->GetStringChars(value, NULL); 509#endif 510 if (!valStr) { 511 LOG_WINDOW("value can't be transfer to UTFChars"); 512 return false; 513 } 514 515 int offset = window->alloc(len); 516 if (!offset) { 517 LOG_WINDOW("Failed allocating %u bytes", len); 518#if WINDOW_STORAGE_UTF8 519 env->ReleaseStringUTFChars(value, valStr); 520#else 521 env->ReleaseStringChars(value, valStr); 522#endif 523 return false; 524 } 525 526 window->copyIn(offset, (uint8_t const *)valStr, len); 527 528 // This must be updated after the call to alloc(), since that 529 // may move the field around in the window 530 fieldSlot->type = FIELD_TYPE_STRING; 531 fieldSlot->data.buffer.offset = offset; 532 fieldSlot->data.buffer.size = len; 533 534 LOG_WINDOW("%d,%d is TEXT with %u bytes @ %d", row, col, len, offset); 535#if WINDOW_STORAGE_UTF8 536 env->ReleaseStringUTFChars(value, valStr); 537#else 538 env->ReleaseStringChars(value, valStr); 539#endif 540 541 return true; 542} 543 544static jboolean putLong_native(JNIEnv * env, jobject object, jlong value, jint row, jint col) 545{ 546 CursorWindow * window = GET_WINDOW(env, object); 547 if (!window->putLong(row, col, value)) { 548 LOG_WINDOW(" getFieldSlotWithCheck error "); 549 return false; 550 } 551 552 LOG_WINDOW("%d,%d is INTEGER 0x%016llx", row, col, value); 553 554 return true; 555} 556 557static jboolean putDouble_native(JNIEnv * env, jobject object, jdouble value, jint row, jint col) 558{ 559 CursorWindow * window = GET_WINDOW(env, object); 560 if (!window->putDouble(row, col, value)) { 561 LOG_WINDOW(" getFieldSlotWithCheck error "); 562 return false; 563 } 564 565 LOG_WINDOW("%d,%d is FLOAT %lf", row, col, value); 566 567 return true; 568} 569 570static jboolean putNull_native(JNIEnv * env, jobject object, jint row, jint col) 571{ 572 CursorWindow * window = GET_WINDOW(env, object); 573 if (!window->putNull(row, col)) { 574 LOG_WINDOW(" getFieldSlotWithCheck error "); 575 return false; 576 } 577 578 LOG_WINDOW("%d,%d is NULL", row, col); 579 580 return true; 581} 582 583// free the last row 584static void freeLastRow(JNIEnv * env, jobject object) { 585 CursorWindow * window = GET_WINDOW(env, object); 586 window->freeLastRow(); 587} 588 589static jint getType_native(JNIEnv* env, jobject object, jint row, jint column) 590{ 591 int32_t err; 592 CursorWindow * window = GET_WINDOW(env, object); 593 LOG_WINDOW("returning column type affinity for %d,%d from %p", row, column, window); 594 595 if (isNull_native(window, row, column)) { 596 return FIELD_TYPE_NULL; 597 } 598 599 field_slot_t field; 600 err = window->read_field_slot(row, column, &field); 601 if (err != 0) { 602 throwExceptionWithRowCol(env, row, column); 603 return NULL; 604 } 605 606 return field.type; 607} 608 609static JNINativeMethod sMethods[] = 610{ 611 /* name, signature, funcPtr */ 612 {"native_init", "(IZ)I", (void *)native_init_empty}, 613 {"native_init", "(Landroid/os/IBinder;)I", (void *)native_init_memory}, 614 {"native_getBinder", "()Landroid/os/IBinder;", (void *)native_getBinder}, 615 {"native_clear", "()V", (void *)native_clear}, 616 {"close_native", "()V", (void *)native_close}, 617 {"getLong_native", "(II)J", (void *)getLong_native}, 618 {"getBlob_native", "(II)[B", (void *)getBlob_native}, 619 {"getString_native", "(II)Ljava/lang/String;", (void *)getString_native}, 620 {"copyStringToBuffer_native", "(IIILandroid/database/CharArrayBuffer;)[C", (void *)copyStringToBuffer_native}, 621 {"getDouble_native", "(II)D", (void *)getDouble_native}, 622 {"getNumRows_native", "()I", (void *)getNumRows}, 623 {"setNumColumns_native", "(I)Z", (void *)setNumColumns}, 624 {"allocRow_native", "()Z", (void *)allocRow}, 625 {"putBlob_native", "([BII)Z", (void *)putBlob_native}, 626 {"putString_native", "(Ljava/lang/String;II)Z", (void *)putString_native}, 627 {"putLong_native", "(JII)Z", (void *)putLong_native}, 628 {"putDouble_native", "(DII)Z", (void *)putDouble_native}, 629 {"freeLastRow_native", "()V", (void *)freeLastRow}, 630 {"putNull_native", "(II)Z", (void *)putNull_native}, 631 {"getType_native", "(II)I", (void *)getType_native}, 632}; 633 634int register_android_database_CursorWindow(JNIEnv * env) 635{ 636 jclass clazz; 637 638 clazz = env->FindClass("android/database/CursorWindow"); 639 if (clazz == NULL) { 640 LOGE("Can't find android/database/CursorWindow"); 641 return -1; 642 } 643 644 gWindowField = env->GetFieldID(clazz, "nWindow", "I"); 645 646 if (gWindowField == NULL) { 647 LOGE("Error locating fields"); 648 return -1; 649 } 650 651 clazz = env->FindClass("android/database/CharArrayBuffer"); 652 if (clazz == NULL) { 653 LOGE("Can't find android/database/CharArrayBuffer"); 654 return -1; 655 } 656 657 gBufferField = env->GetFieldID(clazz, "data", "[C"); 658 659 if (gBufferField == NULL) { 660 LOGE("Error locating fields data in CharArrayBuffer"); 661 return -1; 662 } 663 664 gSizeCopiedField = env->GetFieldID(clazz, "sizeCopied", "I"); 665 666 if (gSizeCopiedField == NULL) { 667 LOGE("Error locating fields sizeCopied in CharArrayBuffer"); 668 return -1; 669 } 670 671 return AndroidRuntime::registerNativeMethods(env, "android/database/CursorWindow", 672 sMethods, NELEM(sMethods)); 673} 674 675} // namespace android 676