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
17 #include <jni.h>
18 #include <time.h>
19 #include <stdio.h>
20 #include <memory>
21 #include <vector>
22
23 #include <android/log.h>
24
25 #include "GifTranscoder.h"
26
27 #define SQUARE(a) ((a)*(a))
28
29 // GIF does not support partial transparency, so our alpha channels are always 0x0 or 0xff.
30 static const ColorARGB TRANSPARENT = 0x0;
31
32 #define ALPHA(color) (((color) >> 24) & 0xff)
33 #define RED(color) (((color) >> 16) & 0xff)
34 #define GREEN(color) (((color) >> 8) & 0xff)
35 #define BLUE(color) (((color) >> 0) & 0xff)
36
37 #define MAKE_COLOR_ARGB(a, r, g, b) \
38 ((a) << 24 | (r) << 16 | (g) << 8 | (b))
39
40 #define MAX_COLOR_DISTANCE (255 * 255 * 255)
41
42 #define TAG "GifTranscoder.cpp"
43 #define LOGD_ENABLED 0
44 #if LOGD_ENABLED
45 #define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__))
46 #else
47 #define LOGD(...) ((void)0)
48 #endif
49 #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__))
50 #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__))
51 #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__))
52
53 // This macro expects the assertion to pass, but logs a FATAL if not.
54 #define ASSERT(cond, ...) \
55 ( (__builtin_expect((cond) == 0, 0)) \
56 ? ((void)__android_log_assert(#cond, TAG, ## __VA_ARGS__)) \
57 : (void) 0 )
58 #define ASSERT_ENABLED 1
59
60 namespace {
61
62 // Current time in milliseconds since Unix epoch.
now(void)63 double now(void) {
64 struct timespec res;
65 clock_gettime(CLOCK_REALTIME, &res);
66 return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6;
67 }
68
69 // Gets the pixel at position (x,y) from a buffer that uses row-major order to store an image with
70 // the specified width.
71 template <typename T>
getPixel(T * buffer,int width,int x,int y)72 T* getPixel(T* buffer, int width, int x, int y) {
73 return buffer + (y * width + x);
74 }
75
76 } // namespace
77
transcode(const char * pathIn,const char * pathOut)78 int GifTranscoder::transcode(const char* pathIn, const char* pathOut) {
79 int error;
80 double t0;
81 GifFileType* gifIn;
82 GifFileType* gifOut;
83
84 // Automatically closes the GIF files when this method returns
85 GifFilesCloser closer;
86
87 gifIn = DGifOpenFileName(pathIn, &error);
88 if (gifIn) {
89 closer.setGifIn(gifIn);
90 LOGD("Opened input GIF: %s", pathIn);
91 } else {
92 LOGE("Could not open input GIF: %s, error = %d", pathIn, error);
93 return GIF_ERROR;
94 }
95
96 gifOut = EGifOpenFileName(pathOut, false, &error);
97 if (gifOut) {
98 closer.setGifOut(gifOut);
99 LOGD("Opened output GIF: %s", pathOut);
100 } else {
101 LOGE("Could not open output GIF: %s, error = %d", pathOut, error);
102 return GIF_ERROR;
103 }
104
105 t0 = now();
106 if (resizeBoxFilter(gifIn, gifOut)) {
107 LOGD("Resized GIF in %.2f ms", now() - t0);
108 } else {
109 LOGE("Could not resize GIF");
110 return GIF_ERROR;
111 }
112
113 return GIF_OK;
114 }
115
resizeBoxFilter(GifFileType * gifIn,GifFileType * gifOut)116 bool GifTranscoder::resizeBoxFilter(GifFileType* gifIn, GifFileType* gifOut) {
117 ASSERT(gifIn != NULL, "gifIn cannot be NULL");
118 ASSERT(gifOut != NULL, "gifOut cannot be NULL");
119
120 if (gifIn->SWidth < 0 || gifIn->SHeight < 0) {
121 LOGE("Input GIF has invalid size: %d x %d", gifIn->SWidth, gifIn->SHeight);
122 return false;
123 }
124
125 // Output GIF will be 50% the size of the original.
126 if (EGifPutScreenDesc(gifOut,
127 gifIn->SWidth / 2,
128 gifIn->SHeight / 2,
129 gifIn->SColorResolution,
130 gifIn->SBackGroundColor,
131 gifIn->SColorMap) == GIF_ERROR) {
132 LOGE("Could not write screen descriptor");
133 return false;
134 }
135 LOGD("Wrote screen descriptor");
136
137 // Index of the current image.
138 int imageIndex = 0;
139
140 // Transparent color of the current image.
141 int transparentColor = NO_TRANSPARENT_COLOR;
142
143 // Buffer for reading raw images from the input GIF.
144 std::vector<GifByteType> srcBuffer(gifIn->SWidth * gifIn->SHeight);
145
146 // Buffer for rendering images from the input GIF.
147 std::unique_ptr<ColorARGB[]> renderBuffer(new ColorARGB[gifIn->SWidth * gifIn->SHeight]);
148
149 // Buffer for writing new images to output GIF (one row at a time).
150 std::unique_ptr<GifByteType[]> dstRowBuffer(new GifByteType[gifOut->SWidth]);
151
152 // Many GIFs use DISPOSE_DO_NOT to make images draw on top of previous images. They can also
153 // use DISPOSE_BACKGROUND to clear the last image region before drawing the next one. We need
154 // to keep track of the disposal mode as we go along to properly render the GIF.
155 int disposalMode = DISPOSAL_UNSPECIFIED;
156 int prevImageDisposalMode = DISPOSAL_UNSPECIFIED;
157 GifImageDesc prevImageDimens;
158
159 // Background color (applies to entire GIF).
160 ColorARGB bgColor = TRANSPARENT;
161
162 GifRecordType recordType;
163 do {
164 if (DGifGetRecordType(gifIn, &recordType) == GIF_ERROR) {
165 LOGE("Could not get record type");
166 return false;
167 }
168 LOGD("Read record type: %d", recordType);
169 switch (recordType) {
170 case IMAGE_DESC_RECORD_TYPE: {
171 if (DGifGetImageDesc(gifIn) == GIF_ERROR) {
172 LOGE("Could not read image descriptor (%d)", imageIndex);
173 return false;
174 }
175
176 // Check the current image position.
177 if (gifIn->Image.Left < 0 ||
178 gifIn->Image.Top < 0 ||
179 gifIn->Image.Left + gifIn->Image.Width > gifIn->SWidth ||
180 gifIn->Image.Top + gifIn->Image.Height > gifIn->SHeight) {
181 LOGE("GIF image extends beyond logical screen");
182 return false;
183 }
184
185 // Write the new image descriptor.
186 if (EGifPutImageDesc(gifOut,
187 0, // Left
188 0, // Top
189 gifOut->SWidth,
190 gifOut->SHeight,
191 false, // Interlace
192 gifIn->Image.ColorMap) == GIF_ERROR) {
193 LOGE("Could not write image descriptor (%d)", imageIndex);
194 return false;
195 }
196
197 // Read the image from the input GIF. The buffer is already initialized to the
198 // size of the GIF, which is usually equal to the size of all the images inside it.
199 // If not, the call to resize below ensures that the buffer is the right size.
200 srcBuffer.resize(gifIn->Image.Width * gifIn->Image.Height);
201 if (readImage(gifIn, srcBuffer.data()) == false) {
202 LOGE("Could not read image data (%d)", imageIndex);
203 return false;
204 }
205 LOGD("Read image data (%d)", imageIndex);
206 // Render the image from the input GIF.
207 if (renderImage(gifIn,
208 srcBuffer.data(),
209 imageIndex,
210 transparentColor,
211 renderBuffer.get(),
212 bgColor,
213 prevImageDimens,
214 prevImageDisposalMode) == false) {
215 LOGE("Could not render %d", imageIndex);
216 return false;
217 }
218 LOGD("Rendered image (%d)", imageIndex);
219
220 // Generate the image in the output GIF.
221 for (int y = 0; y < gifOut->SHeight; y++) {
222 for (int x = 0; x < gifOut->SWidth; x++) {
223 const GifByteType dstColorIndex = computeNewColorIndex(
224 gifIn, transparentColor, renderBuffer.get(), x, y);
225 *(dstRowBuffer.get() + x) = dstColorIndex;
226 }
227 if (EGifPutLine(gifOut, dstRowBuffer.get(), gifOut->SWidth) == GIF_ERROR) {
228 LOGE("Could not write raster data (%d)", imageIndex);
229 return false;
230 }
231 }
232 LOGD("Wrote raster data (%d)", imageIndex);
233
234 // Save the disposal mode for rendering the next image.
235 // We only support DISPOSE_DO_NOT and DISPOSE_BACKGROUND.
236 prevImageDisposalMode = disposalMode;
237 if (prevImageDisposalMode == DISPOSAL_UNSPECIFIED) {
238 prevImageDisposalMode = DISPOSE_DO_NOT;
239 } else if (prevImageDisposalMode == DISPOSE_PREVIOUS) {
240 prevImageDisposalMode = DISPOSE_BACKGROUND;
241 }
242 if (prevImageDisposalMode == DISPOSE_BACKGROUND) {
243 prevImageDimens.Left = gifIn->Image.Left;
244 prevImageDimens.Top = gifIn->Image.Top;
245 prevImageDimens.Width = gifIn->Image.Width;
246 prevImageDimens.Height = gifIn->Image.Height;
247 }
248
249 if (gifOut->Image.ColorMap) {
250 GifFreeMapObject(gifOut->Image.ColorMap);
251 gifOut->Image.ColorMap = NULL;
252 }
253
254 imageIndex++;
255 } break;
256 case EXTENSION_RECORD_TYPE: {
257 int extCode;
258 GifByteType* ext;
259 if (DGifGetExtension(gifIn, &extCode, &ext) == GIF_ERROR) {
260 LOGE("Could not read extension block");
261 return false;
262 }
263 LOGD("Read extension block, code: %d", extCode);
264 if (extCode == GRAPHICS_EXT_FUNC_CODE) {
265 GraphicsControlBlock gcb;
266 if (DGifExtensionToGCB(ext[0], ext + 1, &gcb) == GIF_ERROR) {
267 LOGE("Could not interpret GCB extension");
268 return false;
269 }
270 transparentColor = gcb.TransparentColor;
271
272 // This logic for setting the background color based on the first GCB
273 // doesn't quite match the GIF spec, but empirically it seems to work and it
274 // matches what libframesequence (Rastermill) does.
275 if (imageIndex == 0 && gifIn->SColorMap) {
276 if (gcb.TransparentColor == NO_TRANSPARENT_COLOR) {
277 if (gifIn->SBackGroundColor < 0 ||
278 gifIn->SBackGroundColor >= gifIn->SColorMap->ColorCount) {
279 LOGE("SBackGroundColor overflow");
280 return false;
281 }
282 GifColorType bgColorIndex =
283 gifIn->SColorMap->Colors[gifIn->SBackGroundColor];
284 bgColor = gifColorToColorARGB(bgColorIndex);
285 LOGD("Set background color based on first GCB");
286 }
287 }
288
289 // Record the original disposal mode and then update it.
290 disposalMode = gcb.DisposalMode;
291 gcb.DisposalMode = DISPOSE_BACKGROUND;
292 EGifGCBToExtension(&gcb, ext + 1);
293 }
294 if (EGifPutExtensionLeader(gifOut, extCode) == GIF_ERROR) {
295 LOGE("Could not write extension leader");
296 return false;
297 }
298 if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) {
299 LOGE("Could not write extension block");
300 return false;
301 }
302 LOGD("Wrote extension block");
303 while (ext != NULL) {
304 if (DGifGetExtensionNext(gifIn, &ext) == GIF_ERROR) {
305 LOGE("Could not read extension continuation");
306 return false;
307 }
308 if (ext != NULL) {
309 LOGD("Read extension continuation");
310 if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) {
311 LOGE("Could not write extension continuation");
312 return false;
313 }
314 LOGD("Wrote extension continuation");
315 }
316 }
317 if (EGifPutExtensionTrailer(gifOut) == GIF_ERROR) {
318 LOGE("Could not write extension trailer");
319 return false;
320 }
321 } break;
322 }
323
324 } while (recordType != TERMINATE_RECORD_TYPE);
325 LOGD("No more records");
326
327 return true;
328 }
329
readImage(GifFileType * gifIn,GifByteType * rasterBits)330 bool GifTranscoder::readImage(GifFileType* gifIn, GifByteType* rasterBits) {
331 if (gifIn->Image.Interlace) {
332 int interlacedOffset[] = { 0, 4, 2, 1 };
333 int interlacedJumps[] = { 8, 8, 4, 2 };
334
335 // Need to perform 4 passes on the image
336 for (int i = 0; i < 4; i++) {
337 for (int j = interlacedOffset[i]; j < gifIn->Image.Height; j += interlacedJumps[i]) {
338 if (DGifGetLine(gifIn,
339 rasterBits + j * gifIn->Image.Width,
340 gifIn->Image.Width) == GIF_ERROR) {
341 LOGE("Could not read interlaced raster data");
342 return false;
343 }
344 }
345 }
346 } else {
347 if (DGifGetLine(gifIn, rasterBits, gifIn->Image.Width * gifIn->Image.Height) == GIF_ERROR) {
348 LOGE("Could not read raster data");
349 return false;
350 }
351 }
352 return true;
353 }
354
renderImage(GifFileType * gifIn,GifByteType * rasterBits,int imageIndex,int transparentColorIndex,ColorARGB * renderBuffer,ColorARGB bgColor,GifImageDesc prevImageDimens,int prevImageDisposalMode)355 bool GifTranscoder::renderImage(GifFileType* gifIn,
356 GifByteType* rasterBits,
357 int imageIndex,
358 int transparentColorIndex,
359 ColorARGB* renderBuffer,
360 ColorARGB bgColor,
361 GifImageDesc prevImageDimens,
362 int prevImageDisposalMode) {
363 ASSERT(imageIndex < gifIn->ImageCount,
364 "Image index %d is out of bounds (count=%d)", imageIndex, gifIn->ImageCount);
365
366 ColorMapObject* colorMap = getColorMap(gifIn);
367 if (colorMap == NULL) {
368 LOGE("No GIF color map found");
369 return false;
370 }
371
372 // Clear all or part of the background, before drawing the first image and maybe before drawing
373 // subsequent images (depending on the DisposalMode).
374 if (imageIndex == 0) {
375 fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight,
376 0, 0, gifIn->SWidth, gifIn->SHeight, bgColor);
377 } else if (prevImageDisposalMode == DISPOSE_BACKGROUND) {
378 fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight,
379 prevImageDimens.Left, prevImageDimens.Top,
380 prevImageDimens.Width, prevImageDimens.Height, TRANSPARENT);
381 }
382
383 // Paint this image onto the canvas
384 for (int y = 0; y < gifIn->Image.Height; y++) {
385 for (int x = 0; x < gifIn->Image.Width; x++) {
386 GifByteType colorIndex = *getPixel(rasterBits, gifIn->Image.Width, x, y);
387 if (colorIndex >= colorMap->ColorCount) {
388 LOGE("Color Index %d is out of bounds (count=%d)", colorIndex,
389 colorMap->ColorCount);
390 return false;
391 }
392
393 // This image may be smaller than the GIF's "logical screen"
394 int renderX = x + gifIn->Image.Left;
395 int renderY = y + gifIn->Image.Top;
396
397 // Skip drawing transparent pixels if this image renders on top of the last one
398 if (imageIndex > 0 && prevImageDisposalMode == DISPOSE_DO_NOT &&
399 colorIndex == transparentColorIndex) {
400 continue;
401 }
402
403 ColorARGB* renderPixel = getPixel(renderBuffer, gifIn->SWidth, renderX, renderY);
404 *renderPixel = getColorARGB(colorMap, transparentColorIndex, colorIndex);
405 }
406 }
407 return true;
408 }
409
fillRect(ColorARGB * renderBuffer,int imageWidth,int imageHeight,int left,int top,int width,int height,ColorARGB color)410 void GifTranscoder::fillRect(ColorARGB* renderBuffer,
411 int imageWidth,
412 int imageHeight,
413 int left,
414 int top,
415 int width,
416 int height,
417 ColorARGB color) {
418 ASSERT(left + width <= imageWidth, "Rectangle is outside image bounds");
419 ASSERT(top + height <= imageHeight, "Rectangle is outside image bounds");
420
421 for (int y = 0; y < height; y++) {
422 for (int x = 0; x < width; x++) {
423 ColorARGB* renderPixel = getPixel(renderBuffer, imageWidth, x + left, y + top);
424 *renderPixel = color;
425 }
426 }
427 }
428
computeNewColorIndex(GifFileType * gifIn,int transparentColorIndex,ColorARGB * renderBuffer,int x,int y)429 GifByteType GifTranscoder::computeNewColorIndex(GifFileType* gifIn,
430 int transparentColorIndex,
431 ColorARGB* renderBuffer,
432 int x,
433 int y) {
434 ColorMapObject* colorMap = getColorMap(gifIn);
435
436 // Compute the average color of 4 adjacent pixels from the input image.
437 ColorARGB c1 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2);
438 ColorARGB c2 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2);
439 ColorARGB c3 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2 + 1);
440 ColorARGB c4 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2 + 1);
441 ColorARGB avgColor = computeAverage(c1, c2, c3, c4);
442
443 // Search the color map for the best match.
444 return findBestColor(colorMap, transparentColorIndex, avgColor);
445 }
446
computeAverage(ColorARGB c1,ColorARGB c2,ColorARGB c3,ColorARGB c4)447 ColorARGB GifTranscoder::computeAverage(ColorARGB c1, ColorARGB c2, ColorARGB c3, ColorARGB c4) {
448 char avgAlpha = (char)(((int) ALPHA(c1) + (int) ALPHA(c2) +
449 (int) ALPHA(c3) + (int) ALPHA(c4)) / 4);
450 char avgRed = (char)(((int) RED(c1) + (int) RED(c2) +
451 (int) RED(c3) + (int) RED(c4)) / 4);
452 char avgGreen = (char)(((int) GREEN(c1) + (int) GREEN(c2) +
453 (int) GREEN(c3) + (int) GREEN(c4)) / 4);
454 char avgBlue = (char)(((int) BLUE(c1) + (int) BLUE(c2) +
455 (int) BLUE(c3) + (int) BLUE(c4)) / 4);
456 return MAKE_COLOR_ARGB(avgAlpha, avgRed, avgGreen, avgBlue);
457 }
458
findBestColor(ColorMapObject * colorMap,int transparentColorIndex,ColorARGB targetColor)459 GifByteType GifTranscoder::findBestColor(ColorMapObject* colorMap, int transparentColorIndex,
460 ColorARGB targetColor) {
461 // Return the transparent color if the average alpha is zero.
462 char alpha = ALPHA(targetColor);
463 if (alpha == 0 && transparentColorIndex != NO_TRANSPARENT_COLOR) {
464 return transparentColorIndex;
465 }
466
467 GifByteType closestColorIndex = 0;
468 int closestColorDistance = MAX_COLOR_DISTANCE;
469 for (int i = 0; i < colorMap->ColorCount; i++) {
470 // Skip the transparent color (we've already eliminated that option).
471 if (i == transparentColorIndex) {
472 continue;
473 }
474 ColorARGB indexedColor = gifColorToColorARGB(colorMap->Colors[i]);
475 int distance = computeDistance(targetColor, indexedColor);
476 if (distance < closestColorDistance) {
477 closestColorIndex = i;
478 closestColorDistance = distance;
479 }
480 }
481 return closestColorIndex;
482 }
483
computeDistance(ColorARGB c1,ColorARGB c2)484 int GifTranscoder::computeDistance(ColorARGB c1, ColorARGB c2) {
485 return SQUARE(RED(c1) - RED(c2)) +
486 SQUARE(GREEN(c1) - GREEN(c2)) +
487 SQUARE(BLUE(c1) - BLUE(c2));
488 }
489
getColorMap(GifFileType * gifIn)490 ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) {
491 if (gifIn->Image.ColorMap) {
492 return gifIn->Image.ColorMap;
493 }
494 return gifIn->SColorMap;
495 }
496
getColorARGB(ColorMapObject * colorMap,int transparentColorIndex,GifByteType colorIndex)497 ColorARGB GifTranscoder::getColorARGB(ColorMapObject* colorMap, int transparentColorIndex,
498 GifByteType colorIndex) {
499 if (colorIndex == transparentColorIndex) {
500 return TRANSPARENT;
501 }
502 return gifColorToColorARGB(colorMap->Colors[colorIndex]);
503 }
504
gifColorToColorARGB(const GifColorType & color)505 ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) {
506 return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue);
507 }
508
~GifFilesCloser()509 GifFilesCloser::~GifFilesCloser() {
510 if (mGifIn) {
511 DGifCloseFile(mGifIn, NULL);
512 mGifIn = NULL;
513 }
514 if (mGifOut) {
515 EGifCloseFile(mGifOut, NULL);
516 mGifOut = NULL;
517 }
518 }
519
setGifIn(GifFileType * gifIn)520 void GifFilesCloser::setGifIn(GifFileType* gifIn) {
521 ASSERT(mGifIn == NULL, "mGifIn is already set");
522 mGifIn = gifIn;
523 }
524
releaseGifIn()525 void GifFilesCloser::releaseGifIn() {
526 ASSERT(mGifIn != NULL, "mGifIn is already NULL");
527 mGifIn = NULL;
528 }
529
setGifOut(GifFileType * gifOut)530 void GifFilesCloser::setGifOut(GifFileType* gifOut) {
531 ASSERT(mGifOut == NULL, "mGifOut is already set");
532 mGifOut = gifOut;
533 }
534
releaseGifOut()535 void GifFilesCloser::releaseGifOut() {
536 ASSERT(mGifOut != NULL, "mGifOut is already NULL");
537 mGifOut = NULL;
538 }
539
540 // JNI stuff
541
transcode(JNIEnv * env,jobject clazz,jstring filePath,jstring outFilePath)542 jboolean transcode(JNIEnv* env, jobject clazz, jstring filePath, jstring outFilePath) {
543 const char* pathIn = env->GetStringUTFChars(filePath, JNI_FALSE);
544 const char* pathOut = env->GetStringUTFChars(outFilePath, JNI_FALSE);
545
546 GifTranscoder transcoder;
547 int gifCode = transcoder.transcode(pathIn, pathOut);
548
549 env->ReleaseStringUTFChars(filePath, pathIn);
550 env->ReleaseStringUTFChars(outFilePath, pathOut);
551
552 return (gifCode == GIF_OK);
553 }
554
555 const char *kClassPathName = "com/android/messaging/util/GifTranscoder";
556
557 JNINativeMethod kMethods[] = {
558 { "transcodeInternal", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)transcode },
559 };
560
registerNativeMethods(JNIEnv * env,const char * className,JNINativeMethod * gMethods,int numMethods)561 int registerNativeMethods(JNIEnv* env, const char* className,
562 JNINativeMethod* gMethods, int numMethods) {
563 jclass clazz = env->FindClass(className);
564 if (clazz == NULL) {
565 return JNI_FALSE;
566 }
567 if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
568 return JNI_FALSE;
569 }
570 return JNI_TRUE;
571 }
572
JNI_OnLoad(JavaVM * vm,void * reserved)573 jint JNI_OnLoad(JavaVM* vm, void* reserved) {
574 JNIEnv* env;
575 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
576 return -1;
577 }
578 if (!registerNativeMethods(env, kClassPathName,
579 kMethods, sizeof(kMethods) / sizeof(kMethods[0]))) {
580 return -1;
581 }
582 return JNI_VERSION_1_6;
583 }
584