You will receive this call immediately before onResume() when your 87 | * activity is re-starting. 88 | *
89 | * 90 | * @param requestCode The integer request code originally supplied to 91 | * startActivityForResult(), allowing you to identify who this 92 | * result came from. 93 | * @param resultCode The integer result code returned by the child activity 94 | * through its setResult(). 95 | * @param data An Intent, which can return result data to the caller 96 | * (various data can be attached to Intent "extras"). 97 | * @see #startActivityForResult 98 | * @see #createPendingResult 99 | * @see #setResult(int) 100 | */ 101 | @Override 102 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 103 | Log.d(TAG, "Activity exited"); 104 | if (requestCode == RC_BARCODE_CAPTURE) { 105 | Intent d = new Intent(); 106 | if (resultCode == CommonStatusCodes.SUCCESS) { 107 | if (data != null) { 108 | Barcode barcode = data.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject); 109 | d.putExtra(BarcodeObject, barcode); 110 | setResult(CommonStatusCodes.SUCCESS, data); 111 | } else { 112 | d.putExtra("err", "USER_CANCELLED"); 113 | setResult(CommonStatusCodes.ERROR, d); 114 | } 115 | } else { 116 | d.putExtra("err", "There was an error with the barcode reader."); 117 | setResult(CommonStatusCodes.ERROR, d); 118 | } 119 | finish(); 120 | } 121 | else { 122 | super.onActivityResult(requestCode, resultCode, data); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | cordova-gmv-barcode-scanner 3 | =========================== 4 | 5 | Purpose of this Project 6 | ----------------------- 7 | 8 | The purpose of this project is to provide a barcode scanner utilizing the Google Mobile Vision library for the Cordova framework on iOS and Android. The GMV library is incredibly performant and fast in comparison to any other barcode reader that I have used that are free. Additionally, I built it to perform live validity checks on VIN numbers for use as a VIN scanner and for drivers license scanning through the PDF 417 barcode on most identification cards. 9 | 10 |  11 | 12 | You can also check out a sample application [here](https://github.com/dealrinc/cordova-gmv-barcode-scanner-sampleapp) if you'd like to see the scanner in action. 13 | 14 | Installation 15 | ------------ 16 | 17 | ```` 18 | cordova plugin add cordova-gmv-barcode-scanner 19 | ```` 20 | 21 | Usage 22 | ----- 23 | 24 | To use the plugin simply call `window.plugins.GMVBarcodeScanner.scan(options, callback)`. See the sample below. 25 | 26 | ````javascript 27 | window.plugins.GMVBarcodeScanner.scan({}, function(err, result) { 28 | 29 | //Handle Errors 30 | if(err) return; 31 | 32 | //Do something with the data. 33 | alert(result); 34 | 35 | }); 36 | ```` 37 | 38 | You can also call `scanLicense` or `scanVIN` to use the other scanning abilities. Note that the only options available to these functions are `width` and `height` of the barcode detector. 39 | 40 | 41 | ````javascript 42 | window.plugins.GMVBarcodeScanner.scanVIN(function(err, result) { 43 | //Handle Errors 44 | if(err) return; 45 | 46 | //Do something with the data. 47 | alert(result); 48 | 49 | }, { width: .5, height: .7 }); 50 | ```` 51 | 52 | ````javascript 53 | window.plugins.GMVBarcodeScanner.scanLicense(function(err, result) { 54 | //Handle Errors 55 | if(err) return; 56 | 57 | //Do something with the data. 58 | alert(result); 59 | 60 | }, { width: .5, height: .7 }); 61 | ```` 62 | 63 | 64 | ### Output 65 | For the `scan` and `scanVIN` functions the output will be a plain string of the value scanned. For `scanLicense` the result will be an object something along the lines of 66 | 67 | ```` json 68 | { 69 | "LicenseNumber": "123456789", 70 | "FirstName": "Johnny", 71 | "MiddleName": "Allen", 72 | "LastName": "Appleseed", 73 | "BirthDate": "1/31/1990", 74 | "LicenseExpiration": "1/31/2025", 75 | "Address": { 76 | "Address": "1234 Main St.", 77 | "City": "Fairyland", 78 | "State": "AB", 79 | "Zip": "12345" 80 | }, 81 | "LicenseState":"AB" 82 | } 83 | 84 | ```` 85 | 86 | ### Plugin Options 87 | 88 | The default options are shown below. Note that the `detectorSize.width` and `detectorSize.height` values must be floats. If the values are greater than 1 then they will not be visible on the screen. Use them as decimal percentages to determine how large you want the scan area to be. 89 | ````javascript 90 | var options = { 91 | types: { 92 | Code128: true, 93 | Code39: true, 94 | Code93: true, 95 | CodaBar: true, 96 | DataMatrix: true, 97 | EAN13: true, 98 | EAN8: true, 99 | ITF: true, 100 | QRCode: true, 101 | UPCA: true, 102 | UPCE: true, 103 | PDF417: true, 104 | Aztec: true 105 | }, 106 | detectorSize: { 107 | width: .5, 108 | height: .7 109 | } 110 | } 111 | ```` 112 | 113 | 114 | ### Android Quirks 115 | 116 | The `detectorSize` option does not currently exclude the area around the detector from being scanned, which means that anything shown on the preview screen is up for grabs to the barcode detector. On iOS this is done automatically. 117 | 118 | ### VIN Scanning 119 | 120 | VIN scanning works on both iOS and Android and utilizes both Code39 and Data Matrix formats. The scanner has a VIN checksum validator that ensures that the 9th VIN digit is correctly calculated. If it is not, the barcode will simply be skipped and the scanner will continue until it finds a valid VIN. 121 | 122 | ### Driver's License Scanning 123 | 124 | Driver's license scanning works on both iOS and Android and scans the PDF417 format and decodes according to the AAMVA specification. It only pulls a few fields, but I believe they are the most important. The decoding is done in the Javascript portion of this plugin which means you could modify it if you'd like. 125 | 126 | ### Commercial Use 127 | This VIN scanner is the primary reason I built out this project, and is used in a commercial application for my company. Additionally, PDF417 scanning on drivers licenses is a massive benefit to the speed of the GMV library. I'd ask that any competitors don't utilize the VIN scanner for vehicles or PDF417 scanner for drivers licenses in applications that offer similar service to the [dealr.cloud](http://dealr.cloud) application. 128 | 129 | Maybe it's stupid for me to ask this, but I wanted to make this project MIT and open because I have always had trouble finding a good scanner for cordova and I wanted to help out other developers. Figured a bit of an ask is in order! :-) 130 | 131 | Project Info 132 | ------------ 133 | 134 | I am not a native developer and basically hacked both of the implementations together. That being said, in testing the plugins look fantastic, significantly more modern than other scanners, and they scan incredibly quickly. Please send @forrestmid a private message, or just submit a pull request, if you have any inclination towards assisting the development of this project! -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 |34 | * 35 | * Supports scaling and mirroring of the graphics relative the camera's preview properties. The 36 | * idea is that detection items are expressed in terms of a preview size, but need to be scaled up 37 | * to the full view size, and also mirrored in the case of the front-facing camera.
38 | * 39 | * Associated {@link Graphic} items should use the following methods to convert to view coordinates 40 | * for the graphics that are drawn: 41 | *
302 | * Note: It is possible that the permissions request interaction 303 | * with the user is interrupted. In this case you will receive empty permissions 304 | * and results arrays which should be treated as a cancellation. 305 | *
306 | * 307 | * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}. 308 | * @param permissions The requested permissions. Never null. 309 | * @param grantResults The grant results for the corresponding permissions 310 | * which is either {@link PackageManager#PERMISSION_GRANTED} 311 | * or {@link PackageManager#PERMISSION_DENIED}. Never null. 312 | * @see #requestPermissions(String[], int) 313 | */ 314 | @Override 315 | public void onRequestPermissionsResult(int requestCode, 316 | @NonNull String[] permissions, 317 | @NonNull int[] grantResults) { 318 | if (requestCode != RC_HANDLE_CAMERA_PERM) { 319 | Log.d(TAG, "Got unexpected permission result: " + requestCode); 320 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 321 | return; 322 | } 323 | 324 | if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 325 | Log.d(TAG, "Camera permission granted - initialize the camera source"); 326 | // we have permission, so create the camerasource 327 | DetectionTypes = getIntent().getIntExtra("DetectionTypes", 0); 328 | ViewFinderWidth = getIntent().getDoubleExtra("ViewFinderWidth", .5); 329 | ViewFinderHeight = getIntent().getDoubleExtra("ViewFinderHeight", .7); 330 | 331 | createCameraSource(true, false); 332 | return; 333 | } 334 | 335 | Log.e(TAG, "Permission not granted: results len = " + grantResults.length + 336 | " Result code = " + (grantResults.length > 0 ? grantResults[0] : "(empty)")); 337 | 338 | DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 339 | public void onClick(DialogInterface dialog, int id) { 340 | finish(); 341 | } 342 | }; 343 | 344 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 345 | builder.setTitle("Camera permission required") 346 | .setMessage(getResources().getIdentifier("no_camera_permission", "string", getPackageName())) 347 | .setPositiveButton(getResources().getIdentifier("ok", "string", getPackageName()), listener) 348 | .show(); 349 | } 350 | 351 | /** 352 | * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet 353 | * (e.g., because onResume was called before the camera source was created), this will be called 354 | * again when the camera source is created. 355 | */ 356 | private void startCameraSource() throws SecurityException { 357 | // check that the device has play services available. 358 | int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable( 359 | getApplicationContext()); 360 | if (code != ConnectionResult.SUCCESS) { 361 | Dialog dlg = 362 | GoogleApiAvailability.getInstance().getErrorDialog(this, code, RC_HANDLE_GMS); 363 | dlg.show(); 364 | } 365 | 366 | if (mCameraSource != null) { 367 | try { 368 | mPreview.start(mCameraSource, mGraphicOverlay); 369 | } catch (IOException e) { 370 | Log.e(TAG, "Unable to start camera source.", e); 371 | mCameraSource.release(); 372 | mCameraSource = null; 373 | } 374 | } 375 | } 376 | 377 | /** 378 | * onTap returns the tapped barcode result to the calling Activity. 379 | * 380 | * @param rawX - the raw position of the tap 381 | * @param rawY - the raw position of the tap. 382 | * @return true if the activity is ending. 383 | */ 384 | private boolean onTap(float rawX, float rawY) { 385 | // Find tap point in preview frame coordinates. 386 | return false; 387 | /* 388 | int[] location = new int[2]; 389 | mGraphicOverlay.getLocationOnScreen(location); 390 | float x = (rawX - location[0]) / mGraphicOverlay.getWidthScaleFactor(); 391 | float y = (rawY - location[1]) / mGraphicOverlay.getHeightScaleFactor(); 392 | 393 | // Find the barcode whose center is closest to the tapped point. 394 | Barcode best = null; 395 | float bestDistance = Float.MAX_VALUE; 396 | for (BarcodeGraphic graphic : mGraphicOverlay.getGraphics()) { 397 | Barcode barcode = graphic.getBarcode(); 398 | if (barcode.getBoundingBox().contains((int) x, (int) y)) { 399 | // Exact hit, no need to keep looking. 400 | best = barcode; 401 | break; 402 | } 403 | float dx = x - barcode.getBoundingBox().centerX(); 404 | float dy = y - barcode.getBoundingBox().centerY(); 405 | float distance = (dx * dx) + (dy * dy); // actually squared distance 406 | if (distance < bestDistance) { 407 | best = barcode; 408 | bestDistance = distance; 409 | } 410 | } 411 | 412 | if (best != null) { 413 | Intent data = new Intent(); 414 | data.putExtra(BarcodeObject, best); 415 | setResult(CommonStatusCodes.SUCCESS, data); 416 | finish(); 417 | return true; 418 | } 419 | return false;*/ 420 | } 421 | 422 | private class CaptureGestureListener extends GestureDetector.SimpleOnGestureListener { 423 | @Override 424 | public boolean onSingleTapConfirmed(MotionEvent e) { 425 | return onTap(e.getRawX(), e.getRawY()) || super.onSingleTapConfirmed(e); 426 | } 427 | } 428 | 429 | private class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener { 430 | 431 | /** 432 | * Responds to scaling events for a gesture in progress. 433 | * Reported by pointer motion. 434 | * 435 | * @param detector The detector reporting the event - use this to 436 | * retrieve extended info about event state. 437 | * @return Whether or not the detector should consider this event 438 | * as handled. If an event was not handled, the detector 439 | * will continue to accumulate movement until an event is 440 | * handled. This can be useful if an application, for example, 441 | * only wants to update scaling factors if the change is 442 | * greater than 0.01. 443 | */ 444 | @Override 445 | public boolean onScale(ScaleGestureDetector detector) { 446 | return false; 447 | } 448 | 449 | /** 450 | * Responds to the beginning of a scaling gesture. Reported by 451 | * new pointers going down. 452 | * 453 | * @param detector The detector reporting the event - use this to 454 | * retrieve extended info about event state. 455 | * @return Whether or not the detector should continue recognizing 456 | * this gesture. For example, if a gesture is beginning 457 | * with a focal point outside of a region where it makes 458 | * sense, onScaleBegin() may return false to ignore the 459 | * rest of the gesture. 460 | */ 461 | @Override 462 | public boolean onScaleBegin(ScaleGestureDetector detector) { 463 | return true; 464 | } 465 | 466 | /** 467 | * Responds to the end of a scale gesture. Reported by existing 468 | * pointers going up. 469 | * 470 | * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 471 | * and {@link ScaleGestureDetector#getFocusY()} will return focal point 472 | * of the pointers remaining on the screen. 473 | * 474 | * @param detector The detector reporting the event - use this to 475 | * retrieve extended info about event state. 476 | */ 477 | @Override 478 | public void onScaleEnd(ScaleGestureDetector detector) { 479 | mCameraSource.doZoom(detector.getScaleFactor()); 480 | } 481 | } 482 | 483 | private static int transliterate(char c) { 484 | return "0123456789.ABCDEFGH..JKLMN.P.R..STUVWXYZ".indexOf(c) % 10; 485 | } 486 | 487 | private static char getCheckDigit(String vin) { 488 | String map = "0123456789X"; 489 | String weights = "8765432X098765432"; 490 | int sum = 0; 491 | for (int i = 0; i < 17; ++i) { 492 | sum += transliterate(vin.charAt(i)) * map.indexOf(weights.charAt(i)); 493 | } 494 | return map.charAt(sum % 11); 495 | } 496 | 497 | private static boolean validateVin(String vin) { 498 | if(vin.length()!=17) return false; 499 | return getCheckDigit(vin) == vin.charAt(8); 500 | } 501 | 502 | @Override 503 | public void onBarcodeDetected(Barcode barcode) { 504 | //do something with barcode data returned 505 | 506 | if(DetectionTypes == 0) { 507 | String val = barcode.rawValue; 508 | if(val.length() < 17) { 509 | return; 510 | } 511 | val = val.replaceAll("[ioqIOQ]", ""); 512 | 513 | val = val.substring(0, Math.min(val.length(), 17)); 514 | 515 | barcode.rawValue = val; 516 | 517 | if(validateVin(val)) { 518 | Intent data = new Intent(); 519 | data.putExtra(BarcodeObject, barcode); 520 | setResult(CommonStatusCodes.SUCCESS, data); 521 | finish(); 522 | } 523 | 524 | } else { 525 | Intent data = new Intent(); 526 | data.putExtra(BarcodeObject, barcode); 527 | setResult(CommonStatusCodes.SUCCESS, data); 528 | finish(); 529 | } 530 | 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/ios/CameraViewController.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present Google Inc. All Rights Reserved. 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 | @import AVFoundation; 18 | @import Vision; 19 | @import GoogleMobileVision; 20 | 21 | #import "CameraViewController.h" 22 | 23 | 24 | @interface CameraViewController ()success set to true.
289 | *
290 | * The auto-focus routine does not lock auto-exposure and auto-white
291 | * balance after it completes.
292 | *
293 | * @param success true if focus was successful, false if otherwise
294 | */
295 | void onAutoFocus(boolean success);
296 | }
297 |
298 | /**
299 | * Callback interface used to notify on auto focus start and stop.
300 | *
301 | * This is only supported in continuous autofocus modes -- {@link 302 | * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link 303 | * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show 304 | * autofocus animation based on this.
305 | */ 306 | public interface AutoFocusMoveCallback { 307 | /** 308 | * Called when the camera auto focus starts or stops. 309 | * 310 | * @param start true if focus starts to move, false if focus stops to move 311 | */ 312 | void onAutoFocusMoving(boolean start); 313 | } 314 | 315 | //============================================================================================== 316 | // Public 317 | //============================================================================================== 318 | 319 | /** 320 | * Stops the camera and releases the resources of the camera and underlying detector. 321 | */ 322 | public void release() { 323 | synchronized (mCameraLock) { 324 | stop(); 325 | mFrameProcessor.release(); 326 | } 327 | } 328 | 329 | /** 330 | * Opens the camera and starts sending preview frames to the underlying detector. The preview 331 | * frames are not displayed. 332 | * 333 | * @throws IOException if the camera's preview texture or display could not be initialized 334 | */ 335 | @RequiresPermission(Manifest.permission.CAMERA) 336 | public CameraSource start() throws IOException { 337 | synchronized (mCameraLock) { 338 | if (mCamera != null) { 339 | return this; 340 | } 341 | 342 | mCamera = createCamera(); 343 | 344 | // SurfaceTexture was introduced in Honeycomb (11), so if we are running and 345 | // old version of Android. fall back to use SurfaceView. 346 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 347 | mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); 348 | mCamera.setPreviewTexture(mDummySurfaceTexture); 349 | } else { 350 | mDummySurfaceView = new SurfaceView(mContext); 351 | mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); 352 | } 353 | mCamera.startPreview(); 354 | 355 | mProcessingThread = new Thread(mFrameProcessor); 356 | mFrameProcessor.setActive(true); 357 | mProcessingThread.start(); 358 | } 359 | return this; 360 | } 361 | 362 | /** 363 | * Opens the camera and starts sending preview frames to the underlying detector. The supplied 364 | * surface holder is used for the preview so frames can be displayed to the user. 365 | * 366 | * @param surfaceHolder the surface holder to use for the preview frames 367 | * @throws IOException if the supplied surface holder could not be used as the preview display 368 | */ 369 | @RequiresPermission(Manifest.permission.CAMERA) 370 | public CameraSource start(SurfaceHolder surfaceHolder) throws IOException { 371 | synchronized (mCameraLock) { 372 | if (mCamera != null) { 373 | return this; 374 | } 375 | 376 | mCamera = createCamera(); 377 | mCamera.setPreviewDisplay(surfaceHolder); 378 | mCamera.startPreview(); 379 | 380 | mProcessingThread = new Thread(mFrameProcessor); 381 | mFrameProcessor.setActive(true); 382 | mProcessingThread.start(); 383 | } 384 | return this; 385 | } 386 | 387 | /** 388 | * Closes the camera and stops sending frames to the underlying frame detector. 389 | * 390 | * This camera source may be restarted again by calling {@link #start()} or 391 | * {@link #start(SurfaceHolder)}. 392 | * 393 | * Call {@link #release()} instead to completely shut down this camera source and release the 394 | * resources of the underlying detector. 395 | */ 396 | public void stop() { 397 | synchronized (mCameraLock) { 398 | mFrameProcessor.setActive(false); 399 | if (mProcessingThread != null) { 400 | try { 401 | // Wait for the thread to complete to ensure that we can't have multiple threads 402 | // executing at the same time (i.e., which would happen if we called start too 403 | // quickly after stop). 404 | mProcessingThread.join(); 405 | } catch (InterruptedException e) { 406 | Log.d(TAG, "Frame processing thread interrupted on release."); 407 | } 408 | mProcessingThread = null; 409 | } 410 | 411 | // clear the buffer to prevent oom exceptions 412 | mBytesToByteBuffer.clear(); 413 | 414 | if (mCamera != null) { 415 | mCamera.stopPreview(); 416 | mCamera.setPreviewCallbackWithBuffer(null); 417 | try { 418 | // We want to be compatible back to Gingerbread, but SurfaceTexture 419 | // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the 420 | // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't 421 | // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. 422 | 423 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 424 | mCamera.setPreviewTexture(null); 425 | 426 | } else { 427 | mCamera.setPreviewDisplay(null); 428 | } 429 | } catch (Exception e) { 430 | Log.e(TAG, "Failed to clear camera preview: " + e); 431 | } 432 | mCamera.release(); 433 | mCamera = null; 434 | } 435 | } 436 | } 437 | 438 | /** 439 | * Returns the preview size that is currently in use by the underlying camera. 440 | */ 441 | public Size getPreviewSize() { 442 | return mPreviewSize; 443 | } 444 | 445 | /** 446 | * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or 447 | * {@link #CAMERA_FACING_FRONT}. 448 | */ 449 | public int getCameraFacing() { 450 | return mFacing; 451 | } 452 | 453 | public int doZoom(float scale) { 454 | synchronized (mCameraLock) { 455 | if (mCamera == null) { 456 | return 0; 457 | } 458 | int currentZoom = 0; 459 | int maxZoom; 460 | Camera.Parameters parameters = mCamera.getParameters(); 461 | if (!parameters.isZoomSupported()) { 462 | Log.w(TAG, "Zoom is not supported on this device"); 463 | return currentZoom; 464 | } 465 | maxZoom = parameters.getMaxZoom(); 466 | 467 | currentZoom = parameters.getZoom() + 1; 468 | float newZoom; 469 | if (scale > 1) { 470 | newZoom = currentZoom + scale * (maxZoom / 10); 471 | } else { 472 | newZoom = currentZoom * scale; 473 | } 474 | currentZoom = Math.round(newZoom) - 1; 475 | if (currentZoom < 0) { 476 | currentZoom = 0; 477 | } else if (currentZoom > maxZoom) { 478 | currentZoom = maxZoom; 479 | } 480 | parameters.setZoom(currentZoom); 481 | mCamera.setParameters(parameters); 482 | return currentZoom; 483 | } 484 | } 485 | 486 | /** 487 | * Initiates taking a picture, which happens asynchronously. The camera source should have been 488 | * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera 489 | * preview is suspended while the picture is being taken, but will resume once picture taking is 490 | * done. 491 | * 492 | * @param shutter the callback for image capture moment, or null 493 | * @param jpeg the callback for JPEG image data, or null 494 | */ 495 | public void takePicture(ShutterCallback shutter, PictureCallback jpeg) { 496 | synchronized (mCameraLock) { 497 | if (mCamera != null) { 498 | PictureStartCallback startCallback = new PictureStartCallback(); 499 | startCallback.mDelegate = shutter; 500 | PictureDoneCallback doneCallback = new PictureDoneCallback(); 501 | doneCallback.mDelegate = jpeg; 502 | mCamera.takePicture(startCallback, null, null, doneCallback); 503 | } 504 | } 505 | } 506 | 507 | /** 508 | * Gets the current focus mode setting. 509 | * 510 | * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link 511 | * #autoFocus(AutoFocusCallback)} to start the focus if focus 512 | * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO. 513 | * @see Camera.Parameters#FOCUS_MODE_AUTO 514 | * @see Camera.Parameters#FOCUS_MODE_INFINITY 515 | * @see Camera.Parameters#FOCUS_MODE_MACRO 516 | * @see Camera.Parameters#FOCUS_MODE_FIXED 517 | * @see Camera.Parameters#FOCUS_MODE_EDOF 518 | * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO 519 | * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE 520 | */ 521 | @Nullable 522 | @FocusMode 523 | public String getFocusMode() { 524 | return mFocusMode; 525 | } 526 | 527 | /** 528 | * Sets the focus mode. 529 | * 530 | * @param mode the focus mode 531 | * @return {@code true} if the focus mode is set, {@code false} otherwise 532 | * @see #getFocusMode() 533 | */ 534 | public boolean setFocusMode(@FocusMode String mode) { 535 | synchronized (mCameraLock) { 536 | if (mCamera != null && mode != null) { 537 | Camera.Parameters parameters = mCamera.getParameters(); 538 | if (parameters.getSupportedFocusModes().contains(mode)) { 539 | parameters.setFocusMode(mode); 540 | mCamera.setParameters(parameters); 541 | mFocusMode = mode; 542 | return true; 543 | } 544 | } 545 | 546 | return false; 547 | } 548 | } 549 | 550 | /** 551 | * Gets the current flash mode setting. 552 | * 553 | * @return current flash mode. null if flash mode setting is not 554 | * supported or the camera is not yet created. 555 | * @see Camera.Parameters#FLASH_MODE_OFF 556 | * @see Camera.Parameters#FLASH_MODE_AUTO 557 | * @see Camera.Parameters#FLASH_MODE_ON 558 | * @see Camera.Parameters#FLASH_MODE_RED_EYE 559 | * @see Camera.Parameters#FLASH_MODE_TORCH 560 | */ 561 | @Nullable 562 | @FlashMode 563 | public String getFlashMode() { 564 | return mFlashMode; 565 | } 566 | 567 | /** 568 | * Sets the flash mode. 569 | * 570 | * @param mode flash mode. 571 | * @return {@code true} if the flash mode is set, {@code false} otherwise 572 | * @see #getFlashMode() 573 | */ 574 | public boolean setFlashMode(@FlashMode String mode) { 575 | synchronized (mCameraLock) { 576 | if (mCamera != null && mode != null) { 577 | Camera.Parameters parameters = mCamera.getParameters(); 578 | if (parameters.getSupportedFlashModes().contains(mode)) { 579 | parameters.setFlashMode(mode); 580 | mCamera.setParameters(parameters); 581 | mFlashMode = mode; 582 | return true; 583 | } 584 | } 585 | 586 | return false; 587 | } 588 | } 589 | 590 | /** 591 | * Starts camera auto-focus and registers a callback function to run when 592 | * the camera is focused. This method is only valid when preview is active 593 | * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}). 594 | * 595 | *Callers should check 596 | * {@link #getFocusMode()} to determine if 597 | * this method should be called. If the camera does not support auto-focus, 598 | * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)} 599 | * callback will be called immediately. 600 | *
601 | *If the current flash mode is not 602 | * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be 603 | * fired during auto-focus, depending on the driver and camera hardware.
604 | * 605 | * @param cb the callback to run 606 | * @see #cancelAutoFocus() 607 | */ 608 | public void autoFocus(@Nullable AutoFocusCallback cb) { 609 | synchronized (mCameraLock) { 610 | if (mCamera != null) { 611 | CameraAutoFocusCallback autoFocusCallback = null; 612 | if (cb != null) { 613 | autoFocusCallback = new CameraAutoFocusCallback(); 614 | autoFocusCallback.mDelegate = cb; 615 | } 616 | mCamera.autoFocus(autoFocusCallback); 617 | } 618 | } 619 | } 620 | 621 | /** 622 | * Cancels any auto-focus function in progress. 623 | * Whether or not auto-focus is currently in progress, 624 | * this function will return the focus position to the default. 625 | * If the camera does not support auto-focus, this is a no-op. 626 | * 627 | * @see #autoFocus(AutoFocusCallback) 628 | */ 629 | public void cancelAutoFocus() { 630 | synchronized (mCameraLock) { 631 | if (mCamera != null) { 632 | mCamera.cancelAutoFocus(); 633 | } 634 | } 635 | } 636 | 637 | /** 638 | * Sets camera auto-focus move callback. 639 | * 640 | * @param cb the callback to run 641 | * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise 642 | */ 643 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 644 | public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) { 645 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { 646 | return false; 647 | } 648 | 649 | synchronized (mCameraLock) { 650 | if (mCamera != null) { 651 | CameraAutoFocusMoveCallback autoFocusMoveCallback = null; 652 | if (cb != null) { 653 | autoFocusMoveCallback = new CameraAutoFocusMoveCallback(); 654 | autoFocusMoveCallback.mDelegate = cb; 655 | } 656 | mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback); 657 | } 658 | } 659 | 660 | return true; 661 | } 662 | 663 | //============================================================================================== 664 | // Private 665 | //============================================================================================== 666 | 667 | /** 668 | * Only allow creation via the builder class. 669 | */ 670 | private CameraSource() { 671 | } 672 | 673 | /** 674 | * Wraps the camera1 shutter callback so that the deprecated API isn't exposed. 675 | */ 676 | private class PictureStartCallback implements Camera.ShutterCallback { 677 | private ShutterCallback mDelegate; 678 | 679 | @Override 680 | public void onShutter() { 681 | if (mDelegate != null) { 682 | mDelegate.onShutter(); 683 | } 684 | } 685 | } 686 | 687 | /** 688 | * Wraps the final callback in the camera sequence, so that we can automatically turn the camera 689 | * preview back on after the picture has been taken. 690 | */ 691 | private class PictureDoneCallback implements Camera.PictureCallback { 692 | private PictureCallback mDelegate; 693 | 694 | @Override 695 | public void onPictureTaken(byte[] data, Camera camera) { 696 | if (mDelegate != null) { 697 | mDelegate.onPictureTaken(data); 698 | } 699 | synchronized (mCameraLock) { 700 | if (mCamera != null) { 701 | mCamera.startPreview(); 702 | } 703 | } 704 | } 705 | } 706 | 707 | /** 708 | * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed. 709 | */ 710 | private class CameraAutoFocusCallback implements Camera.AutoFocusCallback { 711 | private AutoFocusCallback mDelegate; 712 | 713 | @Override 714 | public void onAutoFocus(boolean success, Camera camera) { 715 | if (mDelegate != null) { 716 | mDelegate.onAutoFocus(success); 717 | } 718 | } 719 | } 720 | 721 | /** 722 | * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed. 723 | */ 724 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 725 | private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback { 726 | private AutoFocusMoveCallback mDelegate; 727 | 728 | @Override 729 | public void onAutoFocusMoving(boolean start, Camera camera) { 730 | if (mDelegate != null) { 731 | mDelegate.onAutoFocusMoving(start); 732 | } 733 | } 734 | } 735 | 736 | /** 737 | * Opens the camera and applies the user settings. 738 | * 739 | * @throws RuntimeException if the method fails 740 | */ 741 | @SuppressLint("InlinedApi") 742 | private Camera createCamera() { 743 | int requestedCameraId = getIdForRequestedCamera(mFacing); 744 | if (requestedCameraId == -1) { 745 | throw new RuntimeException("Could not find requested camera."); 746 | } 747 | Camera camera = Camera.open(requestedCameraId); 748 | 749 | SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight); 750 | if (sizePair == null) { 751 | throw new RuntimeException("Could not find suitable preview size."); 752 | } 753 | Size pictureSize = sizePair.pictureSize(); 754 | mPreviewSize = sizePair.previewSize(); 755 | 756 | int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps); 757 | if (previewFpsRange == null) { 758 | throw new RuntimeException("Could not find suitable preview frames per second range."); 759 | } 760 | 761 | Camera.Parameters parameters = camera.getParameters(); 762 | 763 | if (pictureSize != null) { 764 | parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); 765 | } 766 | 767 | parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); 768 | parameters.setPreviewFpsRange( 769 | previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], 770 | previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); 771 | parameters.setPreviewFormat(ImageFormat.NV21); 772 | 773 | setRotation(camera, parameters, requestedCameraId); 774 | 775 | if (mFocusMode != null) { 776 | if (parameters.getSupportedFocusModes().contains( 777 | mFocusMode)) { 778 | parameters.setFocusMode(mFocusMode); 779 | } else { 780 | Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device."); 781 | } 782 | } 783 | 784 | // setting mFocusMode to the one set in the params 785 | mFocusMode = parameters.getFocusMode(); 786 | 787 | if (mFlashMode != null) { 788 | if (parameters.getSupportedFlashModes() != null) { 789 | if (parameters.getSupportedFlashModes().contains( 790 | mFlashMode)) { 791 | parameters.setFlashMode(mFlashMode); 792 | } else { 793 | Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device."); 794 | } 795 | } 796 | } 797 | 798 | // setting mFlashMode to the one set in the params 799 | mFlashMode = parameters.getFlashMode(); 800 | 801 | camera.setParameters(parameters); 802 | 803 | // Four frame buffers are needed for working with the camera: 804 | // 805 | // one for the frame that is currently being executed upon in doing detection 806 | // one for the next pending frame to process immediately upon completing detection 807 | // two for the frames that the camera uses to populate future preview images 808 | camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); 809 | camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); 810 | camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); 811 | camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); 812 | camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); 813 | 814 | return camera; 815 | } 816 | 817 | /** 818 | * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such 819 | * camera was found. 820 | * 821 | * @param facing the desired camera (front-facing or rear-facing) 822 | */ 823 | private static int getIdForRequestedCamera(int facing) { 824 | CameraInfo cameraInfo = new CameraInfo(); 825 | for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { 826 | Camera.getCameraInfo(i, cameraInfo); 827 | if (cameraInfo.facing == facing) { 828 | return i; 829 | } 830 | } 831 | return -1; 832 | } 833 | 834 | /** 835 | * Selects the most suitable preview and picture size, given the desired width and height. 836 | *
837 | * Even though we may only need the preview size, it's necessary to find both the preview 838 | * size and the picture size of the camera together, because these need to have the same aspect 839 | * ratio. On some hardware, if you would only set the preview size, you will get a distorted 840 | * image. 841 | * 842 | * @param camera the camera to select a preview size from 843 | * @param desiredWidth the desired width of the camera preview frames 844 | * @param desiredHeight the desired height of the camera preview frames 845 | * @return the selected preview and picture size pair 846 | */ 847 | private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { 848 | List