mBytesToByteBuffer = new HashMap<>();
+
+ //==============================================================================================
+ // Builder
+ //==============================================================================================
+
+ /**
+ * Builder for configuring and creating an associated camera source.
+ */
+ public static class Builder {
+ private final Detector> mDetector;
+ private CameraSource mCameraSource = new CameraSource();
+
+ /**
+ * Creates a camera source builder with the supplied context and detector. Camera preview
+ * images will be streamed to the associated detector upon starting the camera source.
+ */
+ public Builder(Context context, Detector> detector) {
+ if (context == null) {
+ throw new IllegalArgumentException("No context supplied.");
+ }
+ if (detector == null) {
+ throw new IllegalArgumentException("No detector supplied.");
+ }
+
+ mDetector = detector;
+ mCameraSource.mContext = context;
+ }
+
+ /**
+ * Sets the requested frame rate in frames per second. If the exact requested value is not
+ * not available, the best matching available value is selected. Default: 30.
+ */
+ public Builder setRequestedFps(float fps) {
+ if (fps <= 0) {
+ throw new IllegalArgumentException("Invalid fps: " + fps);
+ }
+ mCameraSource.mRequestedFps = fps;
+ return this;
+ }
+
+ public Builder setFocusMode(@FocusMode String mode) {
+ mCameraSource.mFocusMode = mode;
+ return this;
+ }
+
+ public Builder setFlashMode(@FlashMode String mode) {
+ mCameraSource.mFlashMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets the desired width and height of the camera frames in pixels. If the exact desired
+ * values are not available options, the best matching available options are selected.
+ * Also, we try to select a preview size which corresponds to the aspect ratio of an
+ * associated full picture size, if applicable. Default: 1024x768.
+ */
+ public Builder setRequestedPreviewSize(int width, int height) {
+ // Restrict the requested range to something within the realm of possibility. The
+ // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that
+ // devices can support. We bound this to avoid int overflow in the code later.
+ final int MAX = 1000000;
+ if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) {
+ throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height);
+ }
+ mCameraSource.mRequestedPreviewWidth = width;
+ mCameraSource.mRequestedPreviewHeight = height;
+ return this;
+ }
+
+ /**
+ * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or
+ * {@link #CAMERA_FACING_FRONT}). Default: back facing.
+ */
+ public Builder setFacing(int facing) {
+ if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) {
+ throw new IllegalArgumentException("Invalid camera: " + facing);
+ }
+ mCameraSource.mFacing = facing;
+ return this;
+ }
+
+ /**
+ * Creates an instance of the camera source.
+ */
+ public CameraSource build() {
+ mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector);
+ return mCameraSource;
+ }
+ }
+
+ //==============================================================================================
+ // Bridge Functionality for the Camera1 API
+ //==============================================================================================
+
+ /**
+ * Callback interface used to signal the moment of actual image capture.
+ */
+ public interface ShutterCallback {
+ /**
+ * Called as near as possible to the moment when a photo is captured from the sensor. This
+ * is a good opportunity to play a shutter sound or give other feedback of camera operation.
+ * This may be some time after the photo was triggered, but some time before the actual data
+ * is available.
+ */
+ void onShutter();
+ }
+
+ /**
+ * Callback interface used to supply image data from a photo capture.
+ */
+ public interface PictureCallback {
+ /**
+ * Called when image data is available after a picture is taken. The format of the data
+ * is a jpeg binary.
+ */
+ void onPictureTaken(byte[] data);
+ }
+
+ /**
+ * Callback interface used to notify on completion of camera auto focus.
+ */
+ public interface AutoFocusCallback {
+ /**
+ * Called when the camera auto focus completes. If the camera
+ * does not support auto-focus and autoFocus is called,
+ * onAutoFocus will be called immediately with a fake value of
+ * success
set to true
.
+ *
+ * The auto-focus routine does not lock auto-exposure and auto-white
+ * balance after it completes.
+ *
+ * @param success true if focus was successful, false if otherwise
+ */
+ void onAutoFocus(boolean success);
+ }
+
+ /**
+ * Callback interface used to notify on auto focus start and stop.
+ *
+ * This is only supported in continuous autofocus modes -- {@link
+ * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link
+ * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show
+ * autofocus animation based on this.
+ */
+ public interface AutoFocusMoveCallback {
+ /**
+ * Called when the camera auto focus starts or stops.
+ *
+ * @param start true if focus starts to move, false if focus stops to move
+ */
+ void onAutoFocusMoving(boolean start);
+ }
+
+ //==============================================================================================
+ // Public
+ //==============================================================================================
+
+ /**
+ * Stops the camera and releases the resources of the camera and underlying detector.
+ */
+ public void release() {
+ synchronized (mCameraLock) {
+ stop();
+ mFrameProcessor.release();
+ }
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The preview
+ * frames are not displayed.
+ *
+ * @throws IOException if the camera's preview texture or display could not be initialized
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public CameraSource start() throws IOException {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ return this;
+ }
+
+ mCamera = createCamera();
+
+ // SurfaceTexture was introduced in Honeycomb (11), so if we are running and
+ // old version of Android. fall back to use SurfaceView.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME);
+ mCamera.setPreviewTexture(mDummySurfaceTexture);
+ } else {
+ mDummySurfaceView = new SurfaceView(mContext);
+ mCamera.setPreviewDisplay(mDummySurfaceView.getHolder());
+ }
+ mCamera.startPreview();
+
+ mProcessingThread = new Thread(mFrameProcessor);
+ mFrameProcessor.setActive(true);
+ mProcessingThread.start();
+ }
+ return this;
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The supplied
+ * surface holder is used for the preview so frames can be displayed to the user.
+ *
+ * @param surfaceHolder the surface holder to use for the preview frames
+ * @throws IOException if the supplied surface holder could not be used as the preview display
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ return this;
+ }
+
+ mCamera = createCamera();
+ mCamera.setPreviewDisplay(surfaceHolder);
+ mCamera.startPreview();
+
+ mProcessingThread = new Thread(mFrameProcessor);
+ mFrameProcessor.setActive(true);
+ mProcessingThread.start();
+ }
+ return this;
+ }
+
+ /**
+ * Closes the camera and stops sending frames to the underlying frame detector.
+ *
+ * This camera source may be restarted again by calling {@link #start()} or
+ * {@link #start(SurfaceHolder)}.
+ *
+ * Call {@link #release()} instead to completely shut down this camera source and release the
+ * resources of the underlying detector.
+ */
+ public void stop() {
+ synchronized (mCameraLock) {
+ mFrameProcessor.setActive(false);
+ if (mProcessingThread != null) {
+ try {
+ // Wait for the thread to complete to ensure that we can't have multiple threads
+ // executing at the same time (i.e., which would happen if we called start too
+ // quickly after stop).
+ mProcessingThread.join();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing thread interrupted on release.");
+ }
+ mProcessingThread = null;
+ }
+
+ // clear the buffer to prevent oom exceptions
+ mBytesToByteBuffer.clear();
+
+ if (mCamera != null) {
+ mCamera.stopPreview();
+ mCamera.setPreviewCallbackWithBuffer(null);
+ try {
+ // We want to be compatible back to Gingerbread, but SurfaceTexture
+ // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the
+ // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't
+ // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb.
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mCamera.setPreviewTexture(null);
+
+ } else {
+ mCamera.setPreviewDisplay(null);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to clear camera preview: " + e);
+ }
+ mCamera.release();
+ mCamera = null;
+ }
+ }
+ }
+
+ /**
+ * Returns the preview size that is currently in use by the underlying camera.
+ */
+ public Size getPreviewSize() {
+ return mPreviewSize;
+ }
+
+ /**
+ * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or
+ * {@link #CAMERA_FACING_FRONT}.
+ */
+ public int getCameraFacing() {
+ return mFacing;
+ }
+
+ public int doZoom(float scale) {
+ synchronized (mCameraLock) {
+ if (mCamera == null) {
+ return 0;
+ }
+ int currentZoom = 0;
+ int maxZoom;
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (!parameters.isZoomSupported()) {
+ Log.w(TAG, "Zoom is not supported on this device");
+ return currentZoom;
+ }
+ maxZoom = parameters.getMaxZoom();
+
+ currentZoom = parameters.getZoom() + 1;
+ float newZoom;
+ if (scale > 1) {
+ newZoom = currentZoom + scale * (maxZoom / 10);
+ } else {
+ newZoom = currentZoom * scale;
+ }
+ currentZoom = Math.round(newZoom) - 1;
+ if (currentZoom < 0) {
+ currentZoom = 0;
+ } else if (currentZoom > maxZoom) {
+ currentZoom = maxZoom;
+ }
+ parameters.setZoom(currentZoom);
+ mCamera.setParameters(parameters);
+ return currentZoom;
+ }
+ }
+
+ /**
+ * Initiates taking a picture, which happens asynchronously. The camera source should have been
+ * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera
+ * preview is suspended while the picture is being taken, but will resume once picture taking is
+ * done.
+ *
+ * @param shutter the callback for image capture moment, or null
+ * @param jpeg the callback for JPEG image data, or null
+ */
+ public void takePicture(ShutterCallback shutter, PictureCallback jpeg) {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ PictureStartCallback startCallback = new PictureStartCallback();
+ startCallback.mDelegate = shutter;
+ PictureDoneCallback doneCallback = new PictureDoneCallback();
+ doneCallback.mDelegate = jpeg;
+ mCamera.takePicture(startCallback, null, null, doneCallback);
+ }
+ }
+ }
+
+ /**
+ * Gets the current focus mode setting.
+ *
+ * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link
+ * #autoFocus(AutoFocusCallback)} to start the focus if focus
+ * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO.
+ * @see Camera.Parameters#FOCUS_MODE_AUTO
+ * @see Camera.Parameters#FOCUS_MODE_INFINITY
+ * @see Camera.Parameters#FOCUS_MODE_MACRO
+ * @see Camera.Parameters#FOCUS_MODE_FIXED
+ * @see Camera.Parameters#FOCUS_MODE_EDOF
+ * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO
+ * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE
+ */
+ @Nullable
+ @FocusMode
+ public String getFocusMode() {
+ return mFocusMode;
+ }
+
+ /**
+ * Sets the focus mode.
+ *
+ * @param mode the focus mode
+ * @return {@code true} if the focus mode is set, {@code false} otherwise
+ * @see #getFocusMode()
+ */
+ public boolean setFocusMode(@FocusMode String mode) {
+ synchronized (mCameraLock) {
+ if (mCamera != null && mode != null) {
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (parameters.getSupportedFocusModes().contains(mode)) {
+ parameters.setFocusMode(mode);
+ mCamera.setParameters(parameters);
+ mFocusMode = mode;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Gets the current flash mode setting.
+ *
+ * @return current flash mode. null if flash mode setting is not
+ * supported or the camera is not yet created.
+ * @see Camera.Parameters#FLASH_MODE_OFF
+ * @see Camera.Parameters#FLASH_MODE_AUTO
+ * @see Camera.Parameters#FLASH_MODE_ON
+ * @see Camera.Parameters#FLASH_MODE_RED_EYE
+ * @see Camera.Parameters#FLASH_MODE_TORCH
+ */
+ @Nullable
+ @FlashMode
+ public String getFlashMode() {
+ return mFlashMode;
+ }
+
+ /**
+ * Sets the flash mode.
+ *
+ * @param mode flash mode.
+ * @return {@code true} if the flash mode is set, {@code false} otherwise
+ * @see #getFlashMode()
+ */
+ public boolean setFlashMode(@FlashMode String mode) {
+ synchronized (mCameraLock) {
+ if (mCamera != null && mode != null) {
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (parameters.getSupportedFlashModes().contains(mode)) {
+ parameters.setFlashMode(mode);
+ mCamera.setParameters(parameters);
+ mFlashMode = mode;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Starts camera auto-focus and registers a callback function to run when
+ * the camera is focused. This method is only valid when preview is active
+ * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}).
+ *
+ * Callers should check
+ * {@link #getFocusMode()} to determine if
+ * this method should be called. If the camera does not support auto-focus,
+ * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)}
+ * callback will be called immediately.
+ *
+ * If the current flash mode is not
+ * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be
+ * fired during auto-focus, depending on the driver and camera hardware.
+ *
+ * @param cb the callback to run
+ * @see #cancelAutoFocus()
+ */
+ public void autoFocus(@Nullable AutoFocusCallback cb) {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ CameraAutoFocusCallback autoFocusCallback = null;
+ if (cb != null) {
+ autoFocusCallback = new CameraAutoFocusCallback();
+ autoFocusCallback.mDelegate = cb;
+ }
+ mCamera.autoFocus(autoFocusCallback);
+ }
+ }
+ }
+
+ /**
+ * Cancels any auto-focus function in progress.
+ * Whether or not auto-focus is currently in progress,
+ * this function will return the focus position to the default.
+ * If the camera does not support auto-focus, this is a no-op.
+ *
+ * @see #autoFocus(AutoFocusCallback)
+ */
+ public void cancelAutoFocus() {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ mCamera.cancelAutoFocus();
+ }
+ }
+ }
+
+ /**
+ * Sets camera auto-focus move callback.
+ *
+ * @param cb the callback to run
+ * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ return false;
+ }
+
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ CameraAutoFocusMoveCallback autoFocusMoveCallback = null;
+ if (cb != null) {
+ autoFocusMoveCallback = new CameraAutoFocusMoveCallback();
+ autoFocusMoveCallback.mDelegate = cb;
+ }
+ mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback);
+ }
+ }
+
+ return true;
+ }
+
+ //==============================================================================================
+ // Private
+ //==============================================================================================
+
+ /**
+ * Only allow creation via the builder class.
+ */
+ private CameraSource() {
+ }
+
+ /**
+ * Wraps the camera1 shutter callback so that the deprecated API isn't exposed.
+ */
+ private class PictureStartCallback implements Camera.ShutterCallback {
+ private ShutterCallback mDelegate;
+
+ @Override
+ public void onShutter() {
+ if (mDelegate != null) {
+ mDelegate.onShutter();
+ }
+ }
+ }
+
+ /**
+ * Wraps the final callback in the camera sequence, so that we can automatically turn the camera
+ * preview back on after the picture has been taken.
+ */
+ private class PictureDoneCallback implements Camera.PictureCallback {
+ private PictureCallback mDelegate;
+
+ @Override
+ public void onPictureTaken(byte[] data, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onPictureTaken(data);
+ }
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ mCamera.startPreview();
+ }
+ }
+ }
+ }
+
+ /**
+ * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed.
+ */
+ private class CameraAutoFocusCallback implements Camera.AutoFocusCallback {
+ private AutoFocusCallback mDelegate;
+
+ @Override
+ public void onAutoFocus(boolean success, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onAutoFocus(success);
+ }
+ }
+ }
+
+ /**
+ * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback {
+ private AutoFocusMoveCallback mDelegate;
+
+ @Override
+ public void onAutoFocusMoving(boolean start, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onAutoFocusMoving(start);
+ }
+ }
+ }
+
+ /**
+ * Opens the camera and applies the user settings.
+ *
+ * @throws RuntimeException if the method fails
+ */
+ @SuppressLint("InlinedApi")
+ private Camera createCamera() {
+ int requestedCameraId = getIdForRequestedCamera(mFacing);
+ if (requestedCameraId == -1) {
+ throw new RuntimeException("Could not find requested camera.");
+ }
+ Camera camera = Camera.open(requestedCameraId);
+
+ SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight);
+ if (sizePair == null) {
+ throw new RuntimeException("Could not find suitable preview size.");
+ }
+ Size pictureSize = sizePair.pictureSize();
+ mPreviewSize = sizePair.previewSize();
+
+ int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps);
+ if (previewFpsRange == null) {
+ throw new RuntimeException("Could not find suitable preview frames per second range.");
+ }
+
+ Camera.Parameters parameters = camera.getParameters();
+
+ if (pictureSize != null) {
+ parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
+ }
+
+ parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ parameters.setPreviewFpsRange(
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+ parameters.setPreviewFormat(ImageFormat.NV21);
+
+ setRotation(camera, parameters, requestedCameraId);
+
+ if (mFocusMode != null) {
+ if (parameters.getSupportedFocusModes().contains(
+ mFocusMode)) {
+ parameters.setFocusMode(mFocusMode);
+ } else {
+ Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device.");
+ }
+ }
+
+ // setting mFocusMode to the one set in the params
+ mFocusMode = parameters.getFocusMode();
+
+ if (mFlashMode != null) {
+ if (parameters.getSupportedFlashModes() != null) {
+ if (parameters.getSupportedFlashModes().contains(
+ mFlashMode)) {
+ parameters.setFlashMode(mFlashMode);
+ } else {
+ Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device.");
+ }
+ }
+ }
+
+ // setting mFlashMode to the one set in the params
+ mFlashMode = parameters.getFlashMode();
+
+ camera.setParameters(parameters);
+
+ // Four frame buffers are needed for working with the camera:
+ //
+ // one for the frame that is currently being executed upon in doing detection
+ // one for the next pending frame to process immediately upon completing detection
+ // two for the frames that the camera uses to populate future preview images
+ camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback());
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+
+ return camera;
+ }
+
+ /**
+ * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such
+ * camera was found.
+ *
+ * @param facing the desired camera (front-facing or rear-facing)
+ */
+ private static int getIdForRequestedCamera(int facing) {
+ CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == facing) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Selects the most suitable preview and picture size, given the desired width and height.
+ *
+ * Even though we may only need the preview size, it's necessary to find both the preview
+ * size and the picture size of the camera together, because these need to have the same aspect
+ * ratio. On some hardware, if you would only set the preview size, you will get a distorted
+ * image.
+ *
+ * @param camera the camera to select a preview size from
+ * @param desiredWidth the desired width of the camera preview frames
+ * @param desiredHeight the desired height of the camera preview frames
+ * @return the selected preview and picture size pair
+ */
+ private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) {
+ List validPreviewSizes = generateValidPreviewSizeList(camera);
+
+ // The method for selecting the best size is to minimize the sum of the differences between
+ // the desired values and the actual values for width and height. This is certainly not the
+ // only way to select the best size, but it provides a decent tradeoff between using the
+ // closest aspect ratio vs. using the closest pixel area.
+ SizePair selectedPair = null;
+ int minDiff = Integer.MAX_VALUE;
+ for (SizePair sizePair : validPreviewSizes) {
+ Size size = sizePair.previewSize();
+ int diff = Math.abs(size.getWidth() - desiredWidth) +
+ Math.abs(size.getHeight() - desiredHeight);
+ if (diff < minDiff) {
+ selectedPair = sizePair;
+ minDiff = diff;
+ }
+ }
+
+ return selectedPair;
+ }
+
+ /**
+ * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted
+ * preview images on some devices, the picture size must be set to a size that is the same
+ * aspect ratio as the preview size or the preview may end up being distorted. If the picture
+ * size is null, then there is no picture size with the same aspect ratio as the preview size.
+ */
+ private static class SizePair {
+ private Size mPreview;
+ private Size mPicture;
+
+ public SizePair(android.hardware.Camera.Size previewSize,
+ android.hardware.Camera.Size pictureSize) {
+ mPreview = new Size(previewSize.width, previewSize.height);
+ if (pictureSize != null) {
+ mPicture = new Size(pictureSize.width, pictureSize.height);
+ }
+ }
+
+ public Size previewSize() {
+ return mPreview;
+ }
+
+ @SuppressWarnings("unused")
+ public Size pictureSize() {
+ return mPicture;
+ }
+ }
+
+ /**
+ * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is
+ * not a corresponding picture size of the same aspect ratio. If there is a corresponding
+ * picture size of the same aspect ratio, the picture size is paired up with the preview size.
+ *
+ * This is necessary because even if we don't use still pictures, the still picture size must be
+ * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the
+ * preview images may be distorted on some devices.
+ */
+ private static List generateValidPreviewSizeList(Camera camera) {
+ Camera.Parameters parameters = camera.getParameters();
+ List supportedPreviewSizes =
+ parameters.getSupportedPreviewSizes();
+ List supportedPictureSizes =
+ parameters.getSupportedPictureSizes();
+ List validPreviewSizes = new ArrayList<>();
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ float previewAspectRatio = (float) previewSize.width / (float) previewSize.height;
+
+ // By looping through the picture sizes in order, we favor the higher resolutions.
+ // We choose the highest resolution in order to support taking the full resolution
+ // picture later.
+ for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) {
+ float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height;
+ if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
+ validPreviewSizes.add(new SizePair(previewSize, pictureSize));
+ break;
+ }
+ }
+ }
+
+ // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all
+ // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we
+ // still account for it.
+ if (validPreviewSizes.size() == 0) {
+ Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size");
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ // The null picture size will let us know that we shouldn't set a picture size.
+ validPreviewSizes.add(new SizePair(previewSize, null));
+ }
+ }
+
+ return validPreviewSizes;
+ }
+
+ /**
+ * Selects the most suitable preview frames per second range, given the desired frames per
+ * second.
+ *
+ * @param camera the camera to select a frames per second range from
+ * @param desiredPreviewFps the desired frames per second for the camera preview frames
+ * @return the selected preview frames per second range
+ */
+ private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) {
+ // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame
+ // rates.
+ int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f);
+
+ // The method for selecting the best range is to minimize the sum of the differences between
+ // the desired value and the upper and lower bounds of the range. This may select a range
+ // that the desired value is outside of, but this is often preferred. For example, if the
+ // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the
+ // range (15, 30).
+ int[] selectedFpsRange = null;
+ int minDiff = Integer.MAX_VALUE;
+ List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange();
+ for (int[] range : previewFpsRangeList) {
+ int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+ int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+ int diff = Math.abs(deltaMin) + Math.abs(deltaMax);
+ if (diff < minDiff) {
+ selectedFpsRange = range;
+ minDiff = diff;
+ }
+ }
+ return selectedFpsRange;
+ }
+
+ /**
+ * Calculates the correct rotation for the given camera id and sets the rotation in the
+ * parameters. It also sets the camera's display orientation and rotation.
+ *
+ * @param parameters the camera parameters for which to set the rotation
+ * @param cameraId the camera id to set rotation based on
+ */
+ private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) {
+ WindowManager windowManager =
+ (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ int degrees = 0;
+ int rotation = windowManager.getDefaultDisplay().getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ Log.e(TAG, "Bad rotation value: " + rotation);
+ }
+
+ CameraInfo cameraInfo = new CameraInfo();
+ Camera.getCameraInfo(cameraId, cameraInfo);
+
+ int angle;
+ int displayAngle;
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ angle = (cameraInfo.orientation + degrees) % 360;
+ displayAngle = (360 - angle) % 360; // compensate for it being mirrored
+ } else { // back-facing
+ angle = (cameraInfo.orientation - degrees + 360) % 360;
+ displayAngle = angle;
+ }
+
+ // This corresponds to the rotation constants in {@link Frame}.
+ mRotation = angle / 90;
+
+ camera.setDisplayOrientation(displayAngle);
+ parameters.setRotation(angle);
+ }
+
+ /**
+ * Creates one buffer for the camera preview callback. The size of the buffer is based off of
+ * the camera preview size and the format of the camera image.
+ *
+ * @return a new preview buffer of the appropriate size for the current camera settings
+ */
+ private byte[] createPreviewBuffer(Size previewSize) {
+ int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
+ long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel;
+ int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1;
+
+ //
+ // NOTICE: This code only works when using play services v. 8.1 or higher.
+ //
+
+ // Creating the byte array this way and wrapping it, as opposed to using .allocate(),
+ // should guarantee that there will be an array to work with.
+ byte[] byteArray = new byte[bufferSize];
+ ByteBuffer buffer = ByteBuffer.wrap(byteArray);
+ if (!buffer.hasArray() || (buffer.array() != byteArray)) {
+ // I don't think that this will ever happen. But if it does, then we wouldn't be
+ // passing the preview content to the underlying detector later.
+ throw new IllegalStateException("Failed to create valid buffer for camera source.");
+ }
+
+ mBytesToByteBuffer.put(byteArray, buffer);
+ return byteArray;
+ }
+
+ //==============================================================================================
+ // Frame processing
+ //==============================================================================================
+
+ /**
+ * Called when the camera has a new preview frame.
+ */
+ private class CameraPreviewCallback implements Camera.PreviewCallback {
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ mFrameProcessor.setNextFrame(data, camera);
+ }
+ }
+
+ /**
+ * This runnable controls access to the underlying receiver, calling it to process frames when
+ * available from the camera. This is designed to run detection on frames as fast as possible
+ * (i.e., without unnecessary context switching or waiting on the next frame).
+ *
+ * While detection is running on a frame, new frames may be received from the camera. As these
+ * frames come in, the most recent frame is held onto as pending. As soon as detection and its
+ * associated processing are done for the previous frame, detection on the mostly recently
+ * received frame will immediately start on the same thread.
+ */
+ private class FrameProcessingRunnable implements Runnable {
+ private Detector> mDetector;
+ private long mStartTimeMillis = SystemClock.elapsedRealtime();
+
+ // This lock guards all of the member variables below.
+ private final Object mLock = new Object();
+ private boolean mActive = true;
+
+ // These pending variables hold the state associated with the new frame awaiting processing.
+ private long mPendingTimeMillis;
+ private int mPendingFrameId = 0;
+ private ByteBuffer mPendingFrameData;
+
+ FrameProcessingRunnable(Detector> detector) {
+ mDetector = detector;
+ }
+
+ /**
+ * Releases the underlying receiver. This is only safe to do after the associated thread
+ * has completed, which is managed in camera source's release method above.
+ */
+ @SuppressLint("Assert")
+ void release() {
+ assert (mProcessingThread.getState() == State.TERMINATED);
+ mDetector.release();
+ mDetector = null;
+ }
+
+ /**
+ * Marks the runnable as active/not active. Signals any blocked threads to continue.
+ */
+ void setActive(boolean active) {
+ synchronized (mLock) {
+ mActive = active;
+ mLock.notifyAll();
+ }
+ }
+
+ /**
+ * Sets the frame data received from the camera. This adds the previous unused frame buffer
+ * (if present) back to the camera, and keeps a pending reference to the frame data for
+ * future use.
+ */
+ void setNextFrame(byte[] data, Camera camera) {
+ synchronized (mLock) {
+ if (mPendingFrameData != null) {
+ camera.addCallbackBuffer(mPendingFrameData.array());
+ mPendingFrameData = null;
+ }
+
+ if (!mBytesToByteBuffer.containsKey(data)) {
+ Log.d(TAG,
+ "Skipping frame. Could not find ByteBuffer associated with the image " +
+ "data from the camera.");
+ return;
+ }
+
+ // Timestamp and frame ID are maintained here, which will give downstream code some
+ // idea of the timing of frames received and when frames were dropped along the way.
+ mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis;
+ mPendingFrameId++;
+ mPendingFrameData = mBytesToByteBuffer.get(data);
+
+ // Notify the processor thread if it is waiting on the next frame (see below).
+ mLock.notifyAll();
+ }
+ }
+
+ /**
+ * As long as the processing thread is active, this executes detection on frames
+ * continuously. The next pending frame is either immediately available or hasn't been
+ * received yet. Once it is available, we transfer the frame info to local variables and
+ * run detection on that frame. It immediately loops back for the next frame without
+ * pausing.
+ *
+ * If detection takes longer than the time in between new frames from the camera, this will
+ * mean that this loop will run without ever waiting on a frame, avoiding any context
+ * switching or frame acquisition time latency.
+ *
+ * If you find that this is using more CPU than you'd like, you should probably decrease the
+ * FPS setting above to allow for some idle time in between frames.
+ */
+ @Override
+ public void run() {
+ Frame outputFrame;
+ ByteBuffer data;
+
+ while (true) {
+ synchronized (mLock) {
+ while (mActive && (mPendingFrameData == null)) {
+ try {
+ // Wait for the next frame to be received from the camera, since we
+ // don't have it yet.
+ mLock.wait();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing loop terminated.", e);
+ return;
+ }
+ }
+
+ if (!mActive) {
+ // Exit the loop once this camera source is stopped or released. We check
+ // this here, immediately after the wait() above, to handle the case where
+ // setActive(false) had been called, triggering the termination of this
+ // loop.
+ return;
+ }
+
+ outputFrame = new Frame.Builder()
+ .setImageData(mPendingFrameData, mPreviewSize.getWidth(),
+ mPreviewSize.getHeight(), ImageFormat.NV21)
+ .setId(mPendingFrameId)
+ .setTimestampMillis(mPendingTimeMillis)
+ .setRotation(mRotation)
+ .build();
+
+ // Hold onto the frame data locally, so that we can use this for detection
+ // below. We need to clear mPendingFrameData to ensure that this buffer isn't
+ // recycled back to the camera before we are done using that data.
+ data = mPendingFrameData;
+ mPendingFrameData = null;
+ }
+
+ // The code below needs to run outside of synchronization, because this will allow
+ // the camera to add pending frame(s) while we are running detection on the current
+ // frame.
+
+ try {
+ mDetector.receiveFrame(outputFrame);
+ } catch (Throwable t) {
+ Log.e(TAG, "Exception thrown from receiver.", t);
+ } finally {
+ mCamera.addCallbackBuffer(data.array());
+ }
+ }
+ }
+ }
+}
diff --git a/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/CameraSourcePreview.java b/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/CameraSourcePreview.java
new file mode 100644
index 00000000..2543f276
--- /dev/null
+++ b/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/CameraSourcePreview.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Modifications Copyright (C) QC Coders
+
+package com.google.android.gms.samples.vision.barcodereader.ui.camera;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.support.annotation.RequiresPermission;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import com.google.android.gms.common.images.Size;
+
+import java.io.IOException;
+
+public class CameraSourcePreview extends ViewGroup {
+ private static final String TAG = "CameraSourcePreview";
+
+ private Context mContext;
+ private SurfaceView mSurfaceView;
+ private boolean mStartRequested;
+ private boolean mSurfaceAvailable;
+ private CameraSource mCameraSource;
+
+ private GraphicOverlay mOverlay;
+
+ public CameraSourcePreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mStartRequested = false;
+ mSurfaceAvailable = false;
+
+ mSurfaceView = new SurfaceView(context);
+ mSurfaceView.getHolder().addCallback(new SurfaceCallback());
+ addView(mSurfaceView);
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public void start(CameraSource cameraSource) throws IOException, SecurityException {
+ if (cameraSource == null) {
+ stop();
+ }
+
+ mCameraSource = cameraSource;
+
+ if (mCameraSource != null) {
+ mStartRequested = true;
+ startIfReady();
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException {
+ mOverlay = overlay;
+ start(cameraSource);
+ }
+
+ public void stop() {
+ if (mCameraSource != null) {
+ mCameraSource.stop();
+ }
+ }
+
+ public void release() {
+ if (mCameraSource != null) {
+ mCameraSource.release();
+ mCameraSource = null;
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ private void startIfReady() throws IOException, SecurityException {
+ if (mStartRequested && mSurfaceAvailable) {
+ mCameraSource.start(mSurfaceView.getHolder());
+ if (mOverlay != null) {
+ Size size = mCameraSource.getPreviewSize();
+ int min = Math.min(size.getWidth(), size.getHeight());
+ int max = Math.max(size.getWidth(), size.getHeight());
+ if (isPortraitMode()) {
+ // Swap width and height sizes when in portrait, since it will be rotated by
+ // 90 degrees
+ mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing());
+ } else {
+ mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing());
+ }
+ mOverlay.clear();
+ }
+ mStartRequested = false;
+ }
+ }
+
+ private class SurfaceCallback implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceCreated(SurfaceHolder surface) {
+ mSurfaceAvailable = true;
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG,"Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surface) {
+ mSurfaceAvailable = false;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int width = 320;
+ int height = 240;
+ if (mCameraSource != null) {
+ Size size = mCameraSource.getPreviewSize();
+ if (size != null) {
+ width = size.getWidth();
+ height = size.getHeight();
+ }
+ }
+
+ // Swap width and height sizes when in portrait, since it will be rotated 90 degrees
+ if (isPortraitMode()) {
+ int tmp = width;
+ //noinspection SuspiciousNameCombination
+ width = height;
+ height = tmp;
+ }
+
+ final int layoutWidth = right - left;
+ final int layoutHeight = bottom - top;
+
+ // Computes height and width for potentially doing fit width.
+ int childWidth = layoutWidth;
+ int childHeight = (int)(((float) layoutWidth / (float) width) * height);
+
+ // If height is too tall using fit width, does fit height instead.
+ if (childHeight > layoutHeight) {
+ childHeight = layoutHeight;
+ childWidth = (int)(((float) layoutHeight / (float) height) * width);
+ }
+
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChildAt(i).layout(0, 0, childWidth, childHeight);
+ }
+
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG,"Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ private boolean isPortraitMode() {
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return false;
+ }
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ return true;
+ }
+
+ Log.d(TAG, "isPortraitMode returning false by default");
+ return false;
+ }
+}
diff --git a/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/GraphicOverlay.java b/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/GraphicOverlay.java
new file mode 100644
index 00000000..4b0440da
--- /dev/null
+++ b/mobile-new/app/src/main/java/com/google/android/gms/samples/vision/barcodereader/ui/camera/GraphicOverlay.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Modifications Copyright (C) QC Coders
+
+package com.google.android.gms.samples.vision.barcodereader.ui.camera;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.google.android.gms.vision.CameraSource;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * A view which renders a series of custom graphics to be overlayed on top of an associated preview
+ * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove
+ * them, triggering the appropriate drawing and invalidation within the view.
+ *
+ * Supports scaling and mirroring of the graphics relative the camera's preview properties. The
+ * idea is that detection items are expressed in terms of a preview size, but need to be scaled up
+ * to the full view size, and also mirrored in the case of the front-facing camera.
+ *
+ * Associated {@link Graphic} items should use the following methods to convert to view coordinates
+ * for the graphics that are drawn:
+ *
+ * - {@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of the
+ * supplied value from the preview scale to the view scale.
+ * - {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the coordinate
+ * from the preview's coordinate system to the view coordinate system.
+ *
+ */
+public class GraphicOverlay extends View {
+ private final Object mLock = new Object();
+ private int mPreviewWidth;
+ private float mWidthScaleFactor = 1.0f;
+ private int mPreviewHeight;
+ private float mHeightScaleFactor = 1.0f;
+ private int mFacing = CameraSource.CAMERA_FACING_BACK;
+ private Set mGraphics = new HashSet<>();
+
+ /**
+ * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
+ * this and implement the {@link Graphic#draw(Canvas)} method to define the
+ * graphics element. Add instances to the overlay using {@link GraphicOverlay#add(Graphic)}.
+ */
+ public static abstract class Graphic {
+ private GraphicOverlay mOverlay;
+
+ public Graphic(GraphicOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ /**
+ * Draw the graphic on the supplied canvas. Drawing should use the following methods to
+ * convert to view coordinates for the graphics that are drawn:
+ *
+ * - {@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of
+ * the supplied value from the preview scale to the view scale.
+ * - {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the
+ * coordinate from the preview's coordinate system to the view coordinate system.
+ *
+ *
+ * @param canvas drawing canvas
+ */
+ public abstract void draw(Canvas canvas);
+
+ /**
+ * Adjusts a horizontal value of the supplied value from the preview scale to the view
+ * scale.
+ */
+ public float scaleX(float horizontal) {
+ return horizontal * mOverlay.mWidthScaleFactor;
+ }
+
+ /**
+ * Adjusts a vertical value of the supplied value from the preview scale to the view scale.
+ */
+ public float scaleY(float vertical) {
+ return vertical * mOverlay.mHeightScaleFactor;
+ }
+
+ /**
+ * Adjusts the x coordinate from the preview's coordinate system to the view coordinate
+ * system.
+ */
+ public float translateX(float x) {
+ if (mOverlay.mFacing == CameraSource.CAMERA_FACING_FRONT) {
+ return mOverlay.getWidth() - scaleX(x);
+ } else {
+ return scaleX(x);
+ }
+ }
+
+ /**
+ * Adjusts the y coordinate from the preview's coordinate system to the view coordinate
+ * system.
+ */
+ public float translateY(float y) {
+ return scaleY(y);
+ }
+
+ public void postInvalidate() {
+ mOverlay.postInvalidate();
+ }
+ }
+
+ public GraphicOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Removes all graphics from the overlay.
+ */
+ public void clear() {
+ synchronized (mLock) {
+ mGraphics.clear();
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Adds a graphic to the overlay.
+ */
+ public void add(T graphic) {
+ synchronized (mLock) {
+ mGraphics.add(graphic);
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Removes a graphic from the overlay.
+ */
+ public void remove(T graphic) {
+ synchronized (mLock) {
+ mGraphics.remove(graphic);
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Returns a copy (as a list) of the set of all active graphics.
+ * @return list of all active graphics.
+ */
+ public List getGraphics() {
+ synchronized (mLock) {
+ return new Vector(mGraphics);
+ }
+ }
+
+ /**
+ * Returns the horizontal scale factor.
+ */
+ public float getWidthScaleFactor() {
+ return mWidthScaleFactor;
+ }
+
+ /**
+ * Returns the vertical scale factor.
+ */
+ public float getHeightScaleFactor() {
+ return mHeightScaleFactor;
+ }
+
+ /**
+ * Sets the camera attributes for size and facing direction, which informs how to transform
+ * image coordinates later.
+ */
+ public void setCameraInfo(int previewWidth, int previewHeight, int facing) {
+ synchronized (mLock) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ mFacing = facing;
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Draws the overlay with its associated graphic objects.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ synchronized (mLock) {
+ if ((mPreviewWidth != 0) && (mPreviewHeight != 0)) {
+ mWidthScaleFactor = (float) canvas.getWidth() / (float) mPreviewWidth;
+ mHeightScaleFactor = (float) canvas.getHeight() / (float) mPreviewHeight;
+ }
+
+ for (Graphic graphic : mGraphics) {
+ graphic.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/mobile-new/app/src/main/java/org/qccoders/qcvoc/MainActivity.java b/mobile-new/app/src/main/java/org/qccoders/qcvoc/MainActivity.java
new file mode 100644
index 00000000..ba46744a
--- /dev/null
+++ b/mobile-new/app/src/main/java/org/qccoders/qcvoc/MainActivity.java
@@ -0,0 +1,61 @@
+/*
+ Copyright (c) QC Coders. All rights reserved. Licensed under the GPLv3 license. See LICENSE file
+ in the project root for full license information.
+*/
+
+package org.qccoders.qcvoc;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.google.android.gms.samples.vision.barcodereader.BarcodeCaptureActivity;
+import com.google.android.gms.vision.barcode.Barcode;
+
+public class MainActivity extends AppCompatActivity {
+ private WebView webview;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ webview = findViewById(R.id.webView);
+ webview.addJavascriptInterface(this, "Android");
+ webview.setWebViewClient(new WebViewClient());
+ webview.getSettings().setJavaScriptEnabled(true);
+ webview.getSettings().setDomStorageEnabled(true);
+ webview.loadUrl("http://dev.qcvoc.qccoders.org");
+ }
+
+ @JavascriptInterface
+ public void scanBarcode() {
+ Intent intent = new Intent(this, BarcodeCaptureActivity.class);
+ intent.putExtra(BarcodeCaptureActivity.AutoFocus, true);
+
+ startActivityForResult(intent, 42);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (data != null) {
+ KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+
+ Barcode barcode = data.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject);
+ String outputString = "^" + barcode.displayValue + "$";
+ KeyEvent[] outputKeyEvents = keymap.getEvents(outputString.toCharArray());
+
+ Log.d("MainActivity", outputString);
+
+ for (int i = 0; i < outputKeyEvents.length; i++) {
+ dispatchKeyEvent(outputKeyEvents[i]);
+ }
+ }
+ }
+}
diff --git a/mobile-new/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile-new/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..c7bd21db
--- /dev/null
+++ b/mobile-new/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml b/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..d5fccc53
--- /dev/null
+++ b/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-new/app/src/main/res/layout-land/barcode_capture.xml b/mobile-new/app/src/main/res/layout-land/barcode_capture.xml
new file mode 100644
index 00000000..1f7137d9
--- /dev/null
+++ b/mobile-new/app/src/main/res/layout-land/barcode_capture.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-new/app/src/main/res/layout/activity_main.xml b/mobile-new/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..3638c91e
--- /dev/null
+++ b/mobile-new/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile-new/app/src/main/res/layout/barcode_capture.xml b/mobile-new/app/src/main/res/layout/barcode_capture.xml
new file mode 100644
index 00000000..3f9efdd1
--- /dev/null
+++ b/mobile-new/app/src/main/res/layout/barcode_capture.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a2f59082
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1b523998
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..ff10afd6
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..115a4c76
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..dcd3cd80
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..459ca609
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..8ca12fe0
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..8e19b410
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b824ebdd
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..4c19a13c
Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/mobile-new/app/src/main/res/values/colors.xml b/mobile-new/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..3ab3e9cb
--- /dev/null
+++ b/mobile-new/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/mobile-new/app/src/main/res/values/strings.xml b/mobile-new/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..adeba700
--- /dev/null
+++ b/mobile-new/app/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+
+ QCVOC
+ OK
+ Access to the camera is needed for detection
+ This application cannot run because it does not have the camera permission. The application will now exit.
+ Face detector dependencies cannot be downloaded due to low device storage
+ Barcode Reader Sample
+ Click "Read Barcode" to read a barcode
+ Read Barcode
+ Auto Focus
+ Use Flash
+ Barcode read successfully
+ No barcode captured
+ "Error reading barcode: %1$s"
+
diff --git a/mobile-new/app/src/main/res/values/styles.xml b/mobile-new/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..0eb88fe3
--- /dev/null
+++ b/mobile-new/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/mobile-new/app/src/test/java/org/qccoders/qcvoc/ExampleUnitTest.java b/mobile-new/app/src/test/java/org/qccoders/qcvoc/ExampleUnitTest.java
new file mode 100644
index 00000000..2ea6dbe8
--- /dev/null
+++ b/mobile-new/app/src/test/java/org/qccoders/qcvoc/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package org.qccoders.qcvoc;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/mobile-new/build.gradle b/mobile-new/build.gradle
new file mode 100644
index 00000000..077cb2fc
--- /dev/null
+++ b/mobile-new/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/mobile-new/gradle.properties b/mobile-new/gradle.properties
new file mode 100644
index 00000000..743d692c
--- /dev/null
+++ b/mobile-new/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/mobile-new/gradle/wrapper/gradle-wrapper.jar b/mobile-new/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7a3265ee
Binary files /dev/null and b/mobile-new/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/mobile-new/gradle/wrapper/gradle-wrapper.properties b/mobile-new/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..4c61289c
--- /dev/null
+++ b/mobile-new/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Sep 22 20:58:31 CDT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/mobile-new/gradlew b/mobile-new/gradlew
new file mode 100644
index 00000000..cccdd3d5
--- /dev/null
+++ b/mobile-new/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/mobile-new/gradlew.bat b/mobile-new/gradlew.bat
new file mode 100644
index 00000000..f9553162
--- /dev/null
+++ b/mobile-new/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mobile-new/settings.gradle b/mobile-new/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/mobile-new/settings.gradle
@@ -0,0 +1 @@
+include ':app'