wu98hay пре 1 година
родитељ
комит
b7bf7ff0aa
32 измењених фајлова са 4829 додато и 0 уклоњено
  1. 21 0
      barcode/src/main/AndroidManifest.xml
  2. 310 0
      barcode/src/main/java/com/kathline/barcode/BitmapUtils.java
  3. 36 0
      barcode/src/main/java/com/kathline/barcode/CameraImageGraphic.java
  4. 789 0
      barcode/src/main/java/com/kathline/barcode/CameraSource.java
  5. 177 0
      barcode/src/main/java/com/kathline/barcode/CameraSourcePreview.java
  6. 70 0
      barcode/src/main/java/com/kathline/barcode/FrameMetadata.java
  7. 146 0
      barcode/src/main/java/com/kathline/barcode/GestureDetectorUtil.java
  8. 265 0
      barcode/src/main/java/com/kathline/barcode/GraphicOverlay.java
  9. 82 0
      barcode/src/main/java/com/kathline/barcode/InferenceInfoGraphic.java
  10. 329 0
      barcode/src/main/java/com/kathline/barcode/MLKit.java
  11. 511 0
      barcode/src/main/java/com/kathline/barcode/PermissionUtil.java
  12. 124 0
      barcode/src/main/java/com/kathline/barcode/PreferenceUtils.java
  13. 62 0
      barcode/src/main/java/com/kathline/barcode/ScopedExecutor.java
  14. 185 0
      barcode/src/main/java/com/kathline/barcode/UriUtils.java
  15. 568 0
      barcode/src/main/java/com/kathline/barcode/ViewfinderView.java
  16. 38 0
      barcode/src/main/java/com/kathline/barcode/VisionImageProcessor.java
  17. 270 0
      barcode/src/main/java/com/kathline/barcode/VisionProcessorBase.java
  18. 98 0
      barcode/src/main/java/com/kathline/barcode/barcodescanner/BarcodeGraphic.java
  19. 143 0
      barcode/src/main/java/com/kathline/barcode/barcodescanner/BarcodeScannerProcessor.java
  20. 235 0
      barcode/src/main/java/com/kathline/barcode/barcodescanner/WxGraphic.java
  21. 123 0
      barcode/src/main/java/com/kathline/barcode/hardware/BeepManager.java
  22. 35 0
      barcode/src/main/java/com/kathline/barcode/hardware/FrontLightMode.java
  23. 9 0
      barcode/src/main/res/drawable/ic_switch_camera_white_48dp.xml
  24. 9 0
      barcode/src/main/res/drawable/toggle_style.xml
  25. 8 0
      barcode/src/main/res/layout/settings_style.xml
  26. BIN
      barcode/src/main/res/mipmap-xhdpi/ic_settings_white_24dp.png
  27. BIN
      barcode/src/main/res/mipmap-xhdpi/ic_switch_camera_white_48dp_inset.png
  28. BIN
      barcode/src/main/res/mipmap-xhdpi/ico_wechat.png
  29. BIN
      barcode/src/main/res/raw/beep.ogg
  30. 45 0
      barcode/src/main/res/values/attrs.xml
  31. 10 0
      barcode/src/main/res/values/colors.xml
  32. 131 0
      barcode/src/main/res/values/strings.xml

+ 21 - 0
barcode/src/main/AndroidManifest.xml

@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.kathline.barcode">
+
+    <uses-feature android:name="android.hardware.camera"/>
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+    <uses-feature android:name="android.hardware.camera.flash" />
+
+    <uses-permission android:name="android.permission.FLASHLIGHT" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+
+    <application android:allowBackup="true"
+        android:requestLegacyExternalStorage="true">
+
+    </application>
+</manifest>

+ 310 - 0
barcode/src/main/java/com/kathline/barcode/BitmapUtils.java

@@ -0,0 +1,310 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.media.Image;
+import android.media.Image.Plane;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/** Utils functions for bitmap conversions. */
+public class BitmapUtils {
+  private static final String TAG = "BitmapUtils";
+
+  /** Converts NV21 format byte buffer to bitmap. */
+  @Nullable
+  public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) {
+    data.rewind();
+    byte[] imageInBuffer = new byte[data.limit()];
+    data.get(imageInBuffer, 0, imageInBuffer.length);
+    try {
+      YuvImage image =
+              new YuvImage(
+                      imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null);
+      ByteArrayOutputStream stream = new ByteArrayOutputStream();
+      image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream);
+
+      Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
+
+      stream.close();
+      return rotateBitmap(bmp, metadata.getRotation(), false, false);
+    } catch (Exception e) {
+      Log.e("VisionProcessorBase", "Error: " + e.getMessage());
+    }
+    return null;
+  }
+
+  /** Rotates a bitmap if it is converted from a bytebuffer. */
+  private static Bitmap rotateBitmap(
+          Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) {
+    Matrix matrix = new Matrix();
+
+    // Rotate the image back to straight.
+    matrix.postRotate(rotationDegrees);
+
+    // Mirror the image along the X or Y axis.
+    matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f);
+    Bitmap rotatedBitmap =
+            Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+
+    // Recycle the old bitmap if it has changed.
+    if (rotatedBitmap != bitmap) {
+      bitmap.recycle();
+    }
+    return rotatedBitmap;
+  }
+
+  @Nullable
+  public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri)
+          throws IOException {
+    Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri);
+    if (decodedBitmap == null) {
+      return null;
+    }
+    int orientation = getExifOrientationTag(contentResolver, imageUri);
+
+    int rotationDegrees = 0;
+    boolean flipX = false;
+    boolean flipY = false;
+    // See e.g. https://magnushoff.com/articles/jpeg-orientation/ for a detailed explanation on each
+    // orientation.
+    switch (orientation) {
+      case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+        flipX = true;
+        break;
+      case ExifInterface.ORIENTATION_ROTATE_90:
+        rotationDegrees = 90;
+        break;
+      case ExifInterface.ORIENTATION_TRANSPOSE:
+        rotationDegrees = 90;
+        flipX = true;
+        break;
+      case ExifInterface.ORIENTATION_ROTATE_180:
+        rotationDegrees = 180;
+        break;
+      case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+        flipY = true;
+        break;
+      case ExifInterface.ORIENTATION_ROTATE_270:
+        rotationDegrees = -90;
+        break;
+      case ExifInterface.ORIENTATION_TRANSVERSE:
+        rotationDegrees = -90;
+        flipX = true;
+        break;
+      case ExifInterface.ORIENTATION_UNDEFINED:
+      case ExifInterface.ORIENTATION_NORMAL:
+      default:
+        // No transformations necessary in this case.
+    }
+
+    return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY);
+  }
+
+  private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) {
+    // We only support parsing EXIF orientation tag from local file on the device.
+    // See also:
+    // https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html
+    if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme())
+            && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) {
+      return 0;
+    }
+
+    ExifInterface exif;
+    try (InputStream inputStream = resolver.openInputStream(imageUri)) {
+      if (inputStream == null) {
+        return 0;
+      }
+
+      exif = new ExifInterface(inputStream);
+    } catch (IOException e) {
+      Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e);
+      return 0;
+    }
+
+    return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+  }
+
+  /**
+   * Converts YUV_420_888 to NV21 bytebuffer.
+   *
+   * <p>The NV21 format consists of a single byte array containing the Y, U and V values. For an
+   * image of size S, the first S positions of the array contain all the Y values. The remaining
+   * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both
+   * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain
+   * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
+   *
+   * <p>YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled
+   * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and
+   * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into
+   * the first part of the NV21 array. The U and V planes may already have the representation in the
+   * NV21 format. This happens if the planes share the same buffer, the V buffer is one position
+   * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy
+   * them to the NV21 array.
+   */
+  @RequiresApi(VERSION_CODES.KITKAT)
+  private static ByteBuffer yuv420ThreePlanesToNV21(
+          Plane[] yuv420888planes, int width, int height) {
+    int imageSize = width * height;
+    byte[] out = new byte[imageSize + 2 * (imageSize / 4)];
+
+    if (areUVPlanesNV21(yuv420888planes, width, height)) {
+      // Copy the Y values.
+      yuv420888planes[0].getBuffer().get(out, 0, imageSize);
+
+      ByteBuffer uBuffer = yuv420888planes[1].getBuffer();
+      ByteBuffer vBuffer = yuv420888planes[2].getBuffer();
+      // Get the first V value from the V buffer, since the U buffer does not contain it.
+      vBuffer.get(out, imageSize, 1);
+      // Copy the first U value and the remaining VU values from the U buffer.
+      uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1);
+    } else {
+      // Fallback to copying the UV values one by one, which is slower but also works.
+      // Unpack Y.
+      unpackPlane(yuv420888planes[0], width, height, out, 0, 1);
+      // Unpack U.
+      unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2);
+      // Unpack V.
+      unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2);
+    }
+
+    return ByteBuffer.wrap(out);
+  }
+
+  /** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */
+  @RequiresApi(VERSION_CODES.KITKAT)
+  private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) {
+    int imageSize = width * height;
+
+    ByteBuffer uBuffer = planes[1].getBuffer();
+    ByteBuffer vBuffer = planes[2].getBuffer();
+
+    // Backup buffer properties.
+    int vBufferPosition = vBuffer.position();
+    int uBufferLimit = uBuffer.limit();
+
+    // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
+    vBuffer.position(vBufferPosition + 1);
+    // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
+    uBuffer.limit(uBufferLimit - 1);
+
+    // Check that the buffers are equal and have the expected number of elements.
+    boolean areNV21 =
+            (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);
+
+    // Restore buffers to their initial state.
+    vBuffer.position(vBufferPosition);
+    uBuffer.limit(uBufferLimit);
+
+    return areNV21;
+  }
+
+  /**
+   * Unpack an image plane into a byte array.
+   *
+   * <p>The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
+   * spaced by 'pixelStride'. Note that there is no row padding on the output.
+   */
+  @TargetApi(VERSION_CODES.KITKAT)
+  private static void unpackPlane(
+          Plane plane, int width, int height, byte[] out, int offset, int pixelStride) {
+    ByteBuffer buffer = plane.getBuffer();
+    buffer.rewind();
+
+    // Compute the size of the current plane.
+    // We assume that it has the aspect ratio as the original image.
+    int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride();
+    if (numRow == 0) {
+      return;
+    }
+    int scaleFactor = height / numRow;
+    int numCol = width / scaleFactor;
+
+    // Extract the data in the output buffer.
+    int outputPos = offset;
+    int rowStart = 0;
+    for (int row = 0; row < numRow; row++) {
+      int inputPos = rowStart;
+      for (int col = 0; col < numCol; col++) {
+        out[outputPos] = buffer.get(inputPos);
+        outputPos += pixelStride;
+        inputPos += plane.getPixelStride();
+      }
+      rowStart += plane.getRowStride();
+    }
+  }
+
+  public static int calculateInSampleSize(BitmapFactory.Options options,
+                                          int reqWidth, int reqHeight) {
+    // 源图片的高度和宽度
+    final int height = options.outHeight;
+    final int width = options.outWidth;
+    //压缩当前图片占用内存不超过应用可用内存的3/4
+    //ARGB_8888  一个像素占用4个字节
+    //1兆字节(mb)=1048576字节(b)
+    long FREE_MEMORY = ((int) Runtime.getRuntime().freeMemory())/1024/1024;
+    while(reqHeight*reqWidth*4> FREE_MEMORY*1048576/4*3){
+      reqHeight-=50;
+      reqWidth-=50;
+    }
+    int inSampleSize = 1;
+    if (height > reqHeight || width > reqWidth) {
+      // 计算出实际宽高和目标宽高的比率
+      final int heightRatio = Math.round((float) height / (float) reqHeight);
+      final int widthRatio = Math.round((float) width / (float) reqWidth);
+      // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
+      // 一定都会大于等于目标的宽和高。
+      inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+    }
+    if(inSampleSize==0) return 1;
+    Log.e("hongliang","inSampleSize=" + inSampleSize);
+    return inSampleSize;
+  }
+
+  public static Bitmap decodeBitmapFromPath(String photo_path, int reqWidth, int reqHeight, boolean isFix) {
+    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
+    final BitmapFactory.Options options = new BitmapFactory.Options();
+    options.inJustDecodeBounds = true;
+    if(isFix) {
+      Bitmap scanBitmap = BitmapFactory.decodeFile(photo_path, options);
+    }
+    // 调用上面定义的方法计算inSampleSize值
+    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+    // 使用获取到的inSampleSize值再次解析图片
+    options.inJustDecodeBounds = false;
+    return BitmapFactory.decodeFile(photo_path, options);
+  }
+}

+ 36 - 0
barcode/src/main/java/com/kathline/barcode/CameraImageGraphic.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+
+/** Draw camera image to background. */
+public class CameraImageGraphic extends GraphicOverlay.Graphic {
+
+  private final Bitmap bitmap;
+
+  public CameraImageGraphic(GraphicOverlay overlay, Bitmap bitmap) {
+    super(overlay);
+    this.bitmap = bitmap;
+  }
+
+  @Override
+  public void draw(Canvas canvas) {
+    canvas.drawBitmap(bitmap, getTransformationMatrix(), null);
+  }
+}

+ 789 - 0
barcode/src/main/java/com/kathline/barcode/CameraSource.java

@@ -0,0 +1,789 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+
+import com.google.android.gms.common.images.Size;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/**
+ * Manages the camera and allows UI updates on top of it (e.g. overlaying extra Graphics or
+ * displaying extra information). This receives preview frames from the camera at a specified rate,
+ * sending those frames to child classes' detectors / classifiers as fast as it is able to process.
+ */
+public class CameraSource {
+  @SuppressLint("InlinedApi")
+  public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK;
+
+  @SuppressLint("InlinedApi")
+  public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT;
+
+  public static final int IMAGE_FORMAT = ImageFormat.NV21;
+  public static final int DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH = 1920;
+  public static final int DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT = 1080;
+
+  private static final String TAG = "MIDemoApp:CameraSource";
+
+  /**
+   * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL context,
+   * we can choose any ID we want here. The dummy surface texture is not a crazy hack - it is
+   * actually how the camera team recommends using the camera without a preview.
+   */
+  private static final int DUMMY_TEXTURE_NAME = 100;
+
+  /**
+   * If the absolute difference between a preview size aspect ratio and a picture size aspect ratio
+   * is less than this tolerance, they are considered to be the same aspect ratio.
+   */
+  private static final float ASPECT_RATIO_TOLERANCE = 0.01f;
+
+  protected Activity activity;
+
+  private Camera camera;
+
+  private int facing = CAMERA_FACING_BACK;
+
+  /** Rotation of the device, and thus the associated preview images captured from the device. */
+  private int rotationDegrees;
+
+  private Size previewSize;
+
+  private static final float REQUESTED_FPS = 30.0f;
+  private static final boolean REQUESTED_AUTO_FOCUS = true;
+
+  // This instance needs to be held onto to avoid GC of its underlying resources. Even though it
+  // isn't used outside of the method that creates it, it still must have hard references maintained
+  // to it.
+  private SurfaceTexture dummySurfaceTexture;
+
+  private final GraphicOverlay graphicOverlay;
+
+  /**
+   * Dedicated thread and associated runnable for calling into the detector with frames, as the
+   * frames become available from the camera.
+   */
+  private Thread processingThread;
+
+  private final FrameProcessingRunnable processingRunnable;
+  private final Object processorLock = new Object();
+
+  private VisionImageProcessor frameProcessor;
+
+  /**
+   * Map to convert between a byte array, received from the camera, and its associated byte buffer.
+   * We use byte buffers internally because this is a more efficient way to call into native code
+   * later (avoids a potential copy).
+   *
+   * <p><b>Note:</b> uses IdentityHashMap here instead of HashMap because the behavior of an array's
+   * equals, hashCode and toString methods is both useless and unexpected. IdentityHashMap enforces
+   * identity ('==') check on the keys.
+   */
+  private final IdentityHashMap<byte[], ByteBuffer> bytesToByteBuffer = new IdentityHashMap<>();
+
+  public CameraSource(Activity activity, GraphicOverlay overlay) {
+    this.activity = activity;
+    graphicOverlay = overlay;
+    graphicOverlay.clear();
+    processingRunnable = new FrameProcessingRunnable();
+  }
+
+  // ==============================================================================================
+  // Public
+  // ==============================================================================================
+
+  /** Stops the camera and releases the resources of the camera and underlying detector. */
+  public void release() {
+    synchronized (processorLock) {
+      stop();
+      cleanScreen();
+
+      if (frameProcessor != null) {
+        frameProcessor.stop();
+      }
+    }
+  }
+
+  /**
+   * 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 synchronized CameraSource start() throws IOException {
+    if (camera != null) {
+      return this;
+    }
+
+    camera = createCamera();
+    dummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME);
+    camera.setPreviewTexture(dummySurfaceTexture);
+    camera.startPreview();
+
+    processingThread = new Thread(processingRunnable);
+    processingRunnable.setActive(true);
+    processingThread.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 synchronized CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
+    if (camera != null) {
+      return this;
+    }
+
+    camera = createCamera();
+    camera.setPreviewDisplay(surfaceHolder);
+    camera.startPreview();
+
+    processingThread = new Thread(processingRunnable);
+    processingRunnable.setActive(true);
+    processingThread.start();
+    return this;
+  }
+
+  /**
+   * Closes the camera and stops sending frames to the underlying frame detector.
+   *
+   * <p>This camera source may be restarted again by calling {@link #start()} or {@link
+   * #start(SurfaceHolder)}.
+   *
+   * <p>Call {@link #release()} instead to completely shut down this camera source and release the
+   * resources of the underlying detector.
+   */
+  public synchronized void stop() {
+    processingRunnable.setActive(false);
+    if (processingThread != 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).
+        processingThread.join();
+      } catch (InterruptedException e) {
+        Log.d(TAG, "Frame processing thread interrupted on release.");
+      }
+      processingThread = null;
+    }
+
+    if (camera != null) {
+      camera.stopPreview();
+      camera.setPreviewCallbackWithBuffer(null);
+      try {
+        camera.setPreviewTexture(null);
+        dummySurfaceTexture = null;
+        camera.setPreviewDisplay(null);
+      } catch (Exception e) {
+        Log.e(TAG, "Failed to clear camera preview: " + e);
+      }
+      camera.release();
+      camera = null;
+    }
+
+    // Release the reference to any image buffers, since these will no longer be in use.
+    bytesToByteBuffer.clear();
+  }
+
+  /** Changes the facing of the camera. */
+  public synchronized void setFacing(int facing) {
+    if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) {
+      throw new IllegalArgumentException("Invalid camera: " + facing);
+    }
+    this.facing = facing;
+  }
+
+  /** Returns the preview size that is currently in use by the underlying camera. */
+  public Size getPreviewSize() {
+    return previewSize;
+  }
+
+  /**
+   * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or {@link
+   * #CAMERA_FACING_FRONT}.
+   */
+  public int getCameraFacing() {
+    return facing;
+  }
+
+  /**
+   * Opens the camera and applies the user settings.
+   *
+   * @throws IOException if camera cannot be found or preview cannot be processed
+   */
+  @SuppressLint("InlinedApi")
+  private Camera createCamera() throws IOException {
+    int requestedCameraId = getIdForRequestedCamera(facing);
+    if (requestedCameraId == -1) {
+      int numCameras = Camera.getNumberOfCameras();
+      int cameraId = 0;
+      while (cameraId < numCameras) {
+        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, cameraInfo);
+        if (cameraInfo.facing == CameraSource.CAMERA_FACING_FRONT) {
+          break;
+        }
+        cameraId++;
+      }
+      if (cameraId == numCameras) {
+        Log.i(TAG, "No camera facing " + CameraSource.CAMERA_FACING_BACK + "; returning camera #0");
+        cameraId = 0;
+      }
+      requestedCameraId = cameraId;
+    }
+    Camera camera = Camera.open(requestedCameraId);
+    if(onCameraListener != null) {
+      onCameraListener.open(camera);
+    }
+
+    SizePair sizePair = PreferenceUtils.getCameraPreviewSizePair(activity, requestedCameraId);
+    if (sizePair == null) {
+      sizePair =
+          selectSizePair(
+              camera,
+              DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH,
+              DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT);
+    }
+
+    if (sizePair == null) {
+      throw new IOException("Could not find suitable preview size.");
+    }
+
+    previewSize = sizePair.preview;
+    Log.v(TAG, "Camera preview size: " + previewSize);
+
+    int[] previewFpsRange = selectPreviewFpsRange(camera, REQUESTED_FPS);
+//    if (previewFpsRange == null) {
+//      throw new IOException("Could not find suitable preview frames per second range.");
+//    }
+
+    Camera.Parameters parameters = camera.getParameters();
+
+    Size pictureSize = sizePair.picture;
+    if (pictureSize != null) {
+      Log.v(TAG, "Camera picture size: " + pictureSize);
+      parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
+    }
+    parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
+    if(previewFpsRange != null) {
+      parameters.setPreviewFpsRange(
+              previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+              previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+    }
+    // Use YV12 so that we can exercise YV12->NV21 auto-conversion logic for OCR detection
+    parameters.setPreviewFormat(IMAGE_FORMAT);
+
+    setRotation(camera, parameters, requestedCameraId);
+
+    if (REQUESTED_AUTO_FOCUS) {
+      if (parameters
+          .getSupportedFocusModes()
+          .contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+      } else {
+        Log.i(TAG, "Camera auto focus is not supported on this device.");
+      }
+    }
+
+    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
+    //
+    // Through trial and error it appears that two free buffers, in addition to the two buffers
+    // used in this code, are needed for the camera to work properly.  Perhaps the camera has
+    // one thread for acquiring images, and another thread for calling into user code.  If only
+    // three buffers are used, then the camera will spew thousands of warning messages when
+    // detection takes a non-trivial amount of time.
+    camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback());
+    camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+    camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+    camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+    camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+
+    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.
+   *
+   * <p>Even though we only need to find 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
+   */
+  public static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) {
+    List<SizePair> 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.preview;
+      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.
+   */
+  public static class SizePair {
+    public final Size preview;
+    @Nullable public final Size picture;
+
+    SizePair(Camera.Size previewSize, @Nullable Camera.Size pictureSize) {
+      preview = new Size(previewSize.width, previewSize.height);
+      picture = pictureSize != null ? new Size(pictureSize.width, pictureSize.height) : null;
+    }
+
+    public SizePair(Size previewSize, @Nullable Size pictureSize) {
+      preview = previewSize;
+      picture = pictureSize;
+    }
+  }
+
+  /**
+   * 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.
+   *
+   * <p>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.
+   */
+  public static List<SizePair> generateValidPreviewSizeList(Camera camera) {
+    Camera.Parameters parameters = camera.getParameters();
+    List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
+    List<Camera.Size> supportedPictureSizes = parameters.getSupportedPictureSizes();
+    List<SizePair> validPreviewSizes = new ArrayList<>();
+    for (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 (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 (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
+   */
+  @SuppressLint("InlinedApi")
+  private static 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);
+
+    // Selects a range with whose upper bound is as close as possible to the desired fps while its
+    // lower bound is as small as possible to properly expose frames in low light conditions. Note
+    // that this may select a range that the desired value is outside of. For example, if the
+    // desired frame rate is 30.5, the range (30, 30) is probably more desirable than (30, 40).
+    int[] selectedFpsRange = null;
+    int minUpperBoundDiff = Integer.MAX_VALUE;
+    int minLowerBound = Integer.MAX_VALUE;
+    String str = camera.getParameters().get("preview-fps-range-value");
+    if(!TextUtils.isEmpty(str)) {
+      List<int[]> previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange();
+      for (int[] range : previewFpsRangeList) {
+        int upperBoundDiff =
+                Math.abs(desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+        int lowerBound = range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+        if (upperBoundDiff <= minUpperBoundDiff && lowerBound <= minLowerBound) {
+          selectedFpsRange = range;
+          minUpperBoundDiff = upperBoundDiff;
+          minLowerBound = lowerBound;
+        }
+      }
+    }
+    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) activity.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 displayAngle;
+    if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
+      this.rotationDegrees = (cameraInfo.orientation + degrees) % 360;
+      displayAngle = (360 - this.rotationDegrees) % 360; // compensate for it being mirrored
+    } else { // back-facing
+      this.rotationDegrees = (cameraInfo.orientation - degrees + 360) % 360;
+      displayAngle = this.rotationDegrees;
+    }
+    Log.d(TAG, "Display rotation is: " + rotation);
+    Log.d(TAG, "Camera face is: " + cameraInfo.facing);
+    Log.d(TAG, "Camera rotation is: " + cameraInfo.orientation);
+    // This value should be one of the degrees that ImageMetadata accepts: 0, 90, 180 or 270.
+    Log.d(TAG, "RotationDegrees is: " + this.rotationDegrees);
+
+    camera.setDisplayOrientation(displayAngle);
+    parameters.setRotation(this.rotationDegrees);
+  }
+
+  /**
+   * 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
+   */
+  @SuppressLint("InlinedApi")
+  private byte[] createPreviewBuffer(Size previewSize) {
+    int bitsPerPixel = ImageFormat.getBitsPerPixel(IMAGE_FORMAT);
+    long sizeInBits = (long) previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel;
+    int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1;
+
+    // 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.");
+    }
+
+    bytesToByteBuffer.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) {
+      processingRunnable.setNextFrame(data, camera);
+    }
+  }
+
+  public void setMachineLearningFrameProcessor(VisionImageProcessor processor) {
+    synchronized (processorLock) {
+      cleanScreen();
+      if (frameProcessor != null) {
+        frameProcessor.stop();
+      }
+      frameProcessor = processor;
+    }
+  }
+
+  /**
+   * 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).
+   *
+   * <p>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 is done for the previous frame, detection on the mostly recently received
+   * frame will immediately start on the same thread.
+   */
+  private class FrameProcessingRunnable implements Runnable {
+
+    // This lock guards all of the member variables below.
+    private final Object lock = new Object();
+    private boolean active = true;
+
+    // These pending variables hold the state associated with the new frame awaiting processing.
+    private ByteBuffer pendingFrameData;
+
+    FrameProcessingRunnable() {}
+
+    /** Marks the runnable as active/not active. Signals any blocked threads to continue. */
+    void setActive(boolean active) {
+      synchronized (lock) {
+        this.active = active;
+        lock.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.
+     */
+    @SuppressWarnings("ByteBufferBackingArray")
+    void setNextFrame(byte[] data, Camera camera) {
+      synchronized (lock) {
+        if (pendingFrameData != null) {
+          camera.addCallbackBuffer(pendingFrameData.array());
+          pendingFrameData = null;
+        }
+
+        if (!bytesToByteBuffer.containsKey(data)) {
+          Log.d(
+              TAG,
+              "Skipping frame. Could not find ByteBuffer associated with the image "
+                  + "data from the camera.");
+          return;
+        }
+
+        pendingFrameData = bytesToByteBuffer.get(data);
+
+        // Notify the processor thread if it is waiting on the next frame (see below).
+        lock.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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     */
+    @SuppressLint("InlinedApi")
+    @SuppressWarnings({"GuardedBy", "ByteBufferBackingArray"})
+    @Override
+    public void run() {
+      ByteBuffer data;
+
+      while (true) {
+        synchronized (lock) {
+          while (active && (pendingFrameData == null)) {
+            try {
+              // Wait for the next frame to be received from the camera, since we
+              // don't have it yet.
+              lock.wait();
+            } catch (InterruptedException e) {
+              Log.d(TAG, "Frame processing loop terminated.", e);
+              return;
+            }
+          }
+
+          if (!active) {
+            // 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;
+          }
+
+          // Hold onto the frame data locally, so that we can use this for detection
+          // below.  We need to clear pendingFrameData to ensure that this buffer isn't
+          // recycled back to the camera before we are done using that data.
+          data = pendingFrameData;
+          pendingFrameData = 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 {
+          synchronized (processorLock) {
+            frameProcessor.processByteBuffer(
+                data,
+                new FrameMetadata.Builder()
+                    .setWidth(previewSize.getWidth())
+                    .setHeight(previewSize.getHeight())
+                    .setRotation(rotationDegrees)
+                    .build(),
+                graphicOverlay);
+          }
+        } catch (Exception t) {
+          Log.e(TAG, "Exception thrown from receiver.", t);
+        } finally {
+          camera.addCallbackBuffer(data.array());
+        }
+      }
+    }
+  }
+
+  /** Cleans up graphicOverlay and child classes can do their cleanups as well . */
+  private void cleanScreen() {
+    graphicOverlay.clear();
+  }
+
+  public synchronized void setTorch(boolean on) {
+    Camera.Parameters parameters = camera.getParameters();
+    List<String> supportedFlashModes = parameters.getSupportedFlashModes();
+    String flashMode;
+    if (on) {
+      flashMode = findSettableValue("flash mode",
+              supportedFlashModes,
+              Camera.Parameters.FLASH_MODE_TORCH,
+              Camera.Parameters.FLASH_MODE_ON);
+    } else {
+      flashMode = findSettableValue("flash mode",
+              supportedFlashModes,
+              Camera.Parameters.FLASH_MODE_OFF);
+    }
+    if (flashMode != null) {
+      if (flashMode.equals(parameters.getFlashMode())) {
+        Log.i(TAG, "Flash mode already set to " + flashMode);
+      } else {
+        Log.i(TAG, "Setting flash mode to " + flashMode);
+        parameters.setFlashMode(flashMode);
+      }
+      camera.setParameters(parameters);
+    }
+  }
+
+  private static String findSettableValue(String name,
+                                          Collection<String> supportedValues,
+                                          String... desiredValues) {
+    Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues));
+    Log.i(TAG, "Supported " + name + " values: " + supportedValues);
+    if (supportedValues != null) {
+      for (String desiredValue : desiredValues) {
+        if (supportedValues.contains(desiredValue)) {
+          Log.i(TAG, "Can set " + name + " to: " + desiredValue);
+          return desiredValue;
+        }
+      }
+    }
+    Log.i(TAG, "No supported values match");
+    return null;
+  }
+
+  public Camera getCamera() {
+    return camera;
+  }
+
+  public interface OnCameraListener {
+    void open(Camera camera);
+  }
+
+  private OnCameraListener onCameraListener;
+
+  public void setOnCameraListener(OnCameraListener listener) {
+    onCameraListener = listener;
+  }
+}

+ 177 - 0
barcode/src/main/java/com/kathline/barcode/CameraSourcePreview.java

@@ -0,0 +1,177 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.content.Context;
+import android.content.res.Configuration;
+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;
+
+/** Preview the camera image in the screen. */
+public class CameraSourcePreview extends ViewGroup {
+  private static final String TAG = "MIDemoApp:Preview";
+
+  private final Context context;
+  private final SurfaceView surfaceView;
+  private boolean startRequested;
+  private boolean surfaceAvailable;
+  private CameraSource cameraSource;
+
+  private GraphicOverlay overlay;
+
+  public CameraSourcePreview(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    this.context = context;
+    startRequested = false;
+    surfaceAvailable = false;
+
+    surfaceView = new SurfaceView(context);
+    surfaceView.getHolder().addCallback(new SurfaceCallback());
+    addView(surfaceView);
+  }
+
+  private void start(CameraSource cameraSource) throws IOException {
+    this.cameraSource = cameraSource;
+
+    if (this.cameraSource != null) {
+      startRequested = true;
+      startIfReady();
+    }
+  }
+
+  public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException {
+    this.overlay = overlay;
+    start(cameraSource);
+  }
+
+  public void stop() {
+    if (cameraSource != null) {
+      cameraSource.stop();
+    }
+  }
+
+  public void release() {
+    if (cameraSource != null) {
+      cameraSource.release();
+      cameraSource = null;
+    }
+    surfaceView.getHolder().getSurface().release();
+  }
+
+  private void startIfReady() throws IOException, SecurityException {
+    if (startRequested && surfaceAvailable) {
+      if (PreferenceUtils.isCameraLiveViewportEnabled(context)) {
+        cameraSource.start(surfaceView.getHolder());
+      } else {
+        cameraSource.start();
+      }
+      requestLayout();
+
+      if (overlay != null) {
+        Size size = cameraSource.getPreviewSize();
+        int min = Math.min(size.getWidth(), size.getHeight());
+        int max = Math.max(size.getWidth(), size.getHeight());
+        boolean isImageFlipped = cameraSource.getCameraFacing() == CameraSource.CAMERA_FACING_FRONT;
+        if (isPortraitMode()) {
+          // Swap width and height sizes when in portrait, since it will be rotated by 90 degrees.
+          // The camera preview and the image being processed have the same size.
+          overlay.setImageSourceInfo(min, max, isImageFlipped);
+        } else {
+          overlay.setImageSourceInfo(max, min, isImageFlipped);
+        }
+        overlay.clear();
+      }
+      startRequested = false;
+    }
+  }
+
+  private class SurfaceCallback implements SurfaceHolder.Callback {
+    @Override
+    public void surfaceCreated(SurfaceHolder surface) {
+      surfaceAvailable = true;
+      try {
+        startIfReady();
+      } catch (IOException e) {
+        Log.e(TAG, "Could not start camera source.", e);
+      }
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder surface) {
+      surfaceAvailable = 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 (cameraSource != null) {
+      Size size = cameraSource.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;
+      width = height;
+      height = tmp;
+    }
+
+    float previewAspectRatio = (float) width / height;
+    int layoutWidth = right - left;
+    int layoutHeight = bottom - top;
+    float layoutAspectRatio = (float) layoutWidth / layoutHeight;
+    if (previewAspectRatio > layoutAspectRatio) {
+      // The preview input is wider than the layout area. Fit the layout height and crop
+      // the preview input horizontally while keep the center.
+      int horizontalOffset = (int) (previewAspectRatio * layoutHeight - layoutWidth) / 2;
+      surfaceView.layout(-horizontalOffset, 0, layoutWidth + horizontalOffset, layoutHeight);
+    } else {
+      // The preview input is taller than the layout area. Fit the layout width and crop the preview
+      // input vertically while keep the center.
+      int verticalOffset = (int) (layoutWidth / previewAspectRatio - layoutHeight) / 2;
+      surfaceView.layout(0, -verticalOffset, layoutWidth, layoutHeight + verticalOffset);
+    }
+  }
+
+  private boolean isPortraitMode() {
+    int orientation = context.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;
+  }
+}

+ 70 - 0
barcode/src/main/java/com/kathline/barcode/FrameMetadata.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+/** Describing a frame info. */
+public class FrameMetadata {
+
+  private final int width;
+  private final int height;
+  private final int rotation;
+
+  public int getWidth() {
+    return width;
+  }
+
+  public int getHeight() {
+    return height;
+  }
+
+  public int getRotation() {
+    return rotation;
+  }
+
+  private FrameMetadata(int width, int height, int rotation) {
+    this.width = width;
+    this.height = height;
+    this.rotation = rotation;
+  }
+
+  /** Builder of {@link FrameMetadata}. */
+  public static class Builder {
+
+    private int width;
+    private int height;
+    private int rotation;
+
+    public Builder setWidth(int width) {
+      this.width = width;
+      return this;
+    }
+
+    public Builder setHeight(int height) {
+      this.height = height;
+      return this;
+    }
+
+    public Builder setRotation(int rotation) {
+      this.rotation = rotation;
+      return this;
+    }
+
+    public FrameMetadata build() {
+      return new FrameMetadata(width, height, rotation);
+    }
+  }
+}

+ 146 - 0
barcode/src/main/java/com/kathline/barcode/GestureDetectorUtil.java

@@ -0,0 +1,146 @@
+package com.kathline.barcode;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GestureDetectorUtil {
+
+    private final String TAG = "GestureDetectorUtil";
+    private View surfaceView;
+    private float oldDist = 1f;
+
+    @SuppressLint("ClickableViewAccessibility")
+    public GestureDetectorUtil(View surfaceView, final Camera camera) {
+        this.surfaceView = surfaceView;
+        surfaceView.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                if (event.getPointerCount() == 1) {
+//                    handleFocusMetering(event, camera);
+                } else {
+                    switch (event.getAction() & MotionEvent.ACTION_MASK) {
+                        case MotionEvent.ACTION_POINTER_DOWN:
+                            oldDist = getFingerSpacing(event);
+                            break;
+                        case MotionEvent.ACTION_MOVE:
+                            float newDist = getFingerSpacing(event);
+                            if (newDist > oldDist) {
+//                                Log.e(TAG, "进入放大手势");
+                                handleZoom(true, camera);
+                            } else if (newDist < oldDist) {
+//                                Log.e(TAG, "进入缩小手势");
+                                handleZoom(false, camera);
+                            }
+                            oldDist = newDist;
+                            break;
+                    }
+                }
+                return true;
+            }
+        });
+    }
+
+    private void handleZoom(boolean isZoomIn, Camera camera) {
+//        Log.e(TAG, "进入缩小放大方法");
+        Camera.Parameters params = camera.getParameters();
+        if (params.isZoomSupported()) {
+            int maxZoom = params.getMaxZoom();
+            int zoom = params.getZoom();
+            if (isZoomIn && zoom < maxZoom) {
+//                Log.e(TAG, "进入放大方法zoom=" + zoom);
+                zoom++;
+            } else if (zoom > 0) {
+//                Log.e(TAG, "进入缩小方法zoom=" + zoom);
+                zoom--;
+            }
+            params.setZoom(zoom);
+            camera.setParameters(params);
+        } else {
+            Log.i(TAG, "zoom not supported");
+        }
+    }
+
+    private void handleFocusMetering(MotionEvent event, Camera camera) {
+        Log.e("Camera", "进入handleFocusMetering");
+        Camera.Parameters params = camera.getParameters();
+        Camera.Size previewSize = params.getPreviewSize();
+        Rect focusRect = calculateTapArea(event.getX(), event.getY(), 1f, previewSize);
+        Rect meteringRect = calculateTapArea(event.getX(), event.getY(), 1.5f, previewSize);
+        final String currentFocusMode = params.getFocusMode();
+        if (params
+                .getSupportedFocusModes()
+                .contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+            params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+        } else {
+            Log.i(TAG, "Camera auto focus is not supported on this device.");
+            params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); // 设置对焦模式
+            if (params.getMaxNumFocusAreas() > 0) {
+                List<Camera.Area> focusAreas = new ArrayList<>();
+                focusAreas.add(new Camera.Area(focusRect, 800));
+                params.setFocusAreas(focusAreas);
+            } else {
+                Log.i(TAG, "focus areas not supported");
+            }
+            if (params.getMaxNumMeteringAreas() > 0) {
+                List<Camera.Area> meteringAreas = new ArrayList<>();
+                meteringAreas.add(new Camera.Area(meteringRect, 800));
+                params.setMeteringAreas(meteringAreas);
+            } else {
+                Log.i(TAG, "metering areas not supported");
+            }
+        }
+        try {
+            camera.cancelAutoFocus();// 每次对焦前,需要先取消对焦
+            //本人使用的小米手机在设置聚焦区域的时候经常会出异常,看日志发现是框架层的字符串转int的时候出错了,
+            //目测是小米修改了框架层代码导致,在此try掉,对实际聚焦效果没影响
+            camera.setParameters(params);
+            camera.autoFocus(new Camera.AutoFocusCallback() {
+                @Override
+                public void onAutoFocus(boolean success, Camera camera) {
+                    Camera.Parameters params = camera.getParameters();
+                    params.setFocusMode(currentFocusMode);
+                    camera.setParameters(params);
+                }
+            });
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static float getFingerSpacing(MotionEvent event) {
+        float x = event.getX(0) - event.getX(1);
+        float y = event.getY(0) - event.getY(1);
+        Log.e("Camera", "getFingerSpacing ,计算距离 = " + (float) Math.sqrt(x * x + y * y));
+        return (float) Math.sqrt(x * x + y * y);
+    }
+
+    private static Rect calculateTapArea(float x, float y, float coefficient, Camera.Size previewSize) {
+        float focusAreaSize = 300;
+        int areaSize = Float.valueOf(focusAreaSize * coefficient).intValue();
+        int centerX = (int) (x / previewSize.width - 1000);
+        int centerY = (int) (y / previewSize.height - 1000);
+        int left = clamp(centerX - areaSize / 2, -1000, 1000);
+        int top = clamp(centerY - areaSize / 2, -1000, 1000);
+        RectF rectF = new RectF(left, top, left + areaSize, top + areaSize);
+        return new Rect(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom));
+    }
+
+    private static int clamp(int x, int min, int max) {
+        if (x > max) {
+            return max;
+        }
+        if (x < min) {
+            return min;
+        }
+        return x;
+    }
+
+}

+ 265 - 0
barcode/src/main/java/com/kathline/barcode/GraphicOverlay.java

@@ -0,0 +1,265 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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.
+ *
+ * <p>Supports scaling and mirroring of the graphics relative the camera's preview properties. The
+ * idea is that detection items are expressed in terms of an image size, but need to be scaled up
+ * to the full view size, and also mirrored in the case of the front-facing camera.
+ *
+ * <p>Associated {@link Graphic} items should use the following methods to convert to view
+ * coordinates for the graphics that are drawn:
+ *
+ * <ol>
+ *   <li>{@link Graphic#scale(float)} adjusts the size of the supplied value from the image scale
+ *       to the view scale.
+ *   <li>{@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the
+ *       coordinate from the image's coordinate system to the view coordinate system.
+ * </ol>
+ */
+public class GraphicOverlay extends View {
+  private final Object lock = new Object();
+  private final List<Graphic> graphics = new ArrayList<>();
+  // Matrix for transforming from image coordinates to overlay view coordinates.
+  private final Matrix transformationMatrix = new Matrix();
+
+  private int imageWidth;
+  private int imageHeight;
+  // The factor of overlay View size to image size. Anything in the image coordinates need to be
+  // scaled by this amount to fit with the area of overlay View.
+  private float scaleFactor = 1.0f;
+  // The number of horizontal pixels needed to be cropped on each side to fit the image with the
+  // area of overlay View after scaling.
+  private float postScaleWidthOffset;
+  // The number of vertical pixels needed to be cropped on each side to fit the image with the
+  // area of overlay View after scaling.
+  private float postScaleHeightOffset;
+  private boolean isImageFlipped;
+  private boolean needUpdateTransformation = true;
+
+  /**
+   * 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 abstract static class Graphic {
+    private GraphicOverlay overlay;
+
+    public Graphic(GraphicOverlay overlay) {
+      this.overlay = 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:
+     *
+     * <ol>
+     *   <li>{@link Graphic#scale(float)} adjusts the size of the supplied value from the image
+     *       scale to the view scale.
+     *   <li>{@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the
+     *       coordinate from the image's coordinate system to the view coordinate system.
+     * </ol>
+     *
+     * @param canvas drawing canvas
+     */
+    public abstract void draw(Canvas canvas);
+
+    public boolean onTouchEvent(MotionEvent event) {
+      return false;
+    }
+
+    public void OnDestroy() {
+
+    }
+
+    /** Adjusts the supplied value from the image scale to the view scale. */
+    public float scale(float imagePixel) {
+      return imagePixel * overlay.scaleFactor;
+    }
+
+    /** Returns the application context of the app. */
+    public Context getApplicationContext() {
+      return overlay.getContext().getApplicationContext();
+    }
+
+    public boolean isImageFlipped() {
+      return overlay.isImageFlipped;
+    }
+
+    /**
+     * Adjusts the x coordinate from the image's coordinate system to the view coordinate system.
+     */
+    public float translateX(float x) {
+      if (overlay.isImageFlipped) {
+        return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset);
+      } else {
+        return scale(x) - overlay.postScaleWidthOffset;
+      }
+    }
+
+    /**
+     * Adjusts the y coordinate from the image's coordinate system to the view coordinate system.
+     */
+    public float translateY(float y) {
+      return scale(y) - overlay.postScaleHeightOffset;
+    }
+
+    /**
+     * Returns a {@link Matrix} for transforming from image coordinates to overlay view coordinates.
+     */
+    public Matrix getTransformationMatrix() {
+      return overlay.transformationMatrix;
+    }
+
+    public void postInvalidate() {
+      overlay.postInvalidate();
+    }
+  }
+
+  public GraphicOverlay(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    addOnLayoutChangeListener(
+        (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+            needUpdateTransformation = true);
+  }
+
+  /** Removes all graphics from the overlay. */
+  public void clear() {
+    synchronized (lock) {
+      for (Graphic graphic : graphics) {
+        graphic.OnDestroy();
+      }
+      graphics.clear();
+    }
+    postInvalidate();
+  }
+
+  /** Adds a graphic to the overlay. */
+  public void add(Graphic graphic) {
+    synchronized (lock) {
+      graphics.add(graphic);
+    }
+  }
+
+  /** Removes a graphic from the overlay. */
+  public void remove(Graphic graphic) {
+    synchronized (lock) {
+      graphics.remove(graphic);
+    }
+    postInvalidate();
+  }
+
+  /**
+   * Sets the source information of the image being processed by detectors, including size and
+   * whether it is flipped, which informs how to transform image coordinates later.
+   *
+   * @param imageWidth the width of the image sent to ML Kit detectors
+   * @param imageHeight the height of the image sent to ML Kit detectors
+   * @param isFlipped whether the image is flipped. Should set it to true when the image is from the
+   *     front camera.
+   */
+  public void setImageSourceInfo(int imageWidth, int imageHeight, boolean isFlipped) {
+    Preconditions.checkState(imageWidth > 0, "image width must be positive");
+    Preconditions.checkState(imageHeight > 0, "image height must be positive");
+    synchronized (lock) {
+      this.imageWidth = imageWidth;
+      this.imageHeight = imageHeight;
+      this.isImageFlipped = isFlipped;
+      needUpdateTransformation = true;
+    }
+    postInvalidate();
+  }
+
+  public int getImageWidth() {
+    return imageWidth;
+  }
+
+  public int getImageHeight() {
+    return imageHeight;
+  }
+
+  private void updateTransformationIfNeeded() {
+    if (!needUpdateTransformation || imageWidth <= 0 || imageHeight <= 0) {
+      return;
+    }
+    float viewAspectRatio = (float) getWidth() / getHeight();
+    float imageAspectRatio = (float) imageWidth / imageHeight;
+    postScaleWidthOffset = 0;
+    postScaleHeightOffset = 0;
+    if (viewAspectRatio > imageAspectRatio) {
+      // The image needs to be vertically cropped to be displayed in this view.
+      scaleFactor = (float) getWidth() / imageWidth;
+      postScaleHeightOffset = ((float) getWidth() / imageAspectRatio - getHeight()) / 2;
+    } else {
+      // The image needs to be horizontally cropped to be displayed in this view.
+      scaleFactor = (float) getHeight() / imageHeight;
+      postScaleWidthOffset = ((float) getHeight() * imageAspectRatio - getWidth()) / 2;
+    }
+
+    transformationMatrix.reset();
+    transformationMatrix.setScale(scaleFactor, scaleFactor);
+    transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset);
+
+    if (isImageFlipped) {
+      transformationMatrix.postScale(-1f, 1f, getWidth() / 2f, getHeight() / 2f);
+    }
+
+    needUpdateTransformation = false;
+  }
+
+  /** Draws the overlay with its associated graphic objects. */
+  @Override
+  protected void onDraw(Canvas canvas) {
+    super.onDraw(canvas);
+
+    synchronized (lock) {
+      updateTransformationIfNeeded();
+
+      for (Graphic graphic : graphics) {
+        graphic.draw(canvas);
+      }
+    }
+  }
+
+  @Override
+  public boolean onTouchEvent(MotionEvent event) {
+    synchronized (lock) {
+      updateTransformationIfNeeded();
+
+      for (Graphic graphic : graphics) {
+        graphic.onTouchEvent(event);
+      }
+    }
+    return super.onTouchEvent(event);
+  }
+}

+ 82 - 0
barcode/src/main/java/com/kathline/barcode/InferenceInfoGraphic.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import androidx.annotation.Nullable;
+
+/** Graphic instance for rendering inference info (latency, FPS, resolution) in an overlay view. */
+public class InferenceInfoGraphic extends GraphicOverlay.Graphic {
+
+  private static final int TEXT_COLOR = Color.WHITE;
+  private static final float TEXT_SIZE = 60.0f;
+
+  private final Paint textPaint;
+  private final GraphicOverlay overlay;
+  private final long frameLatency;
+  private final long detectorLatency;
+
+  // Only valid when a stream of input images is being processed. Null for single image mode.
+  @Nullable private final Integer framesPerSecond;
+
+  public InferenceInfoGraphic(
+      GraphicOverlay overlay,
+      long frameLatency,
+      long detectorLatency,
+      @Nullable Integer framesPerSecond) {
+    super(overlay);
+    this.overlay = overlay;
+    this.frameLatency = frameLatency;
+    this.detectorLatency = detectorLatency;
+    this.framesPerSecond = framesPerSecond;
+    textPaint = new Paint();
+    textPaint.setColor(TEXT_COLOR);
+    textPaint.setTextSize(TEXT_SIZE);
+    textPaint.setShadowLayer(5.0f, 0f, 0f, Color.BLACK);
+    postInvalidate();
+  }
+
+  @Override
+  public synchronized void draw(Canvas canvas) {
+    float x = TEXT_SIZE * 0.5f;
+    float y = TEXT_SIZE * 1.5f;
+
+    canvas.drawText(
+        "InputImage size: " + overlay.getImageHeight() + "x" + overlay.getImageWidth(),
+        x,
+        y,
+        textPaint);
+
+    // Draw FPS (if valid) and inference latency
+    if (framesPerSecond != null) {
+      canvas.drawText(
+          "FPS: " + framesPerSecond + ", Frame latency: " + frameLatency + " ms",
+          x,
+          y + TEXT_SIZE,
+          textPaint);
+      canvas.drawText(
+          "Detector latency: " + detectorLatency + " ms", x, y + TEXT_SIZE * 2, textPaint);
+    } else {
+      canvas.drawText("Frame latency: " + frameLatency + " ms", x, y + TEXT_SIZE, textPaint);
+      canvas.drawText(
+          "Detector latency: " + detectorLatency + " ms", x, y + TEXT_SIZE * 2, textPaint);
+    }
+  }
+}

+ 329 - 0
barcode/src/main/java/com/kathline/barcode/MLKit.java

@@ -0,0 +1,329 @@
+package com.kathline.barcode;
+
+import android.Manifest;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+
+import com.google.android.gms.tasks.OnFailureListener;
+import com.google.android.gms.tasks.OnSuccessListener;
+import com.google.android.gms.tasks.Task;
+import com.google.mlkit.vision.barcode.common.Barcode;
+import com.google.mlkit.vision.barcode.BarcodeScanner;
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
+import com.google.mlkit.vision.barcode.BarcodeScanning;
+import com.google.mlkit.vision.common.InputImage;
+import com.kathline.barcode.barcodescanner.BarcodeGraphic;
+import com.kathline.barcode.barcodescanner.BarcodeScannerProcessor;
+import com.kathline.barcode.hardware.BeepManager;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MLKit implements LifecycleObserver {
+
+    private static final String TAG = "MLKit";
+    private FragmentActivity activity;
+    private CameraSource cameraSource = null;
+    private CameraSourcePreview preview;
+    private GraphicOverlay graphicOverlay;
+    private boolean isAnalyze = true;//是否分析结果
+    private boolean isContinuousScanning = true;//是否连续扫描
+    public BarcodeScannerOptions options;
+
+    private BeepManager beepManager;
+    boolean isOpenLight = false;
+    boolean playBeep = true;
+    boolean vibrate = true;
+    private BarcodeScanner barcodeScanner;
+    private BarcodeScannerProcessor barcodeScannerProcessor;
+
+    public MLKit(FragmentActivity activity, CameraSourcePreview preview, GraphicOverlay graphicOverlay) {
+        this.activity = activity;
+        this.preview = preview;
+        this.graphicOverlay = graphicOverlay;
+        activity.getLifecycle().addObserver(this);
+        onCreate();
+    }
+
+    public void onCreate() {
+        Window window = activity.getWindow();
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        if(beepManager == null) {
+            beepManager = new BeepManager(activity);
+        }
+        createCameraSource();
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_START)
+    public void onStart() {
+        Log.d(TAG, "onStart");
+        createCameraSource();
+        startCameraSource();
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+    public void onStop() {
+        Log.d(TAG, "onStop");
+        preview.stop();
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+    public void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        if (cameraSource != null) {
+            cameraSource.release();
+        }
+    }
+
+    public synchronized void scanningImage(String photoPath) {
+        if (TextUtils.isEmpty(photoPath)) {
+            onScanListener.onFail(2, new Exception("photo url is null!"));
+        }
+        Bitmap bitmap = BitmapUtils.decodeBitmapFromPath(photoPath, 600, 600, false);
+
+        detectInImage(bitmap, graphicOverlay);
+    }
+
+    private void detectInImage(Bitmap bitmap, final GraphicOverlay graphicOverlay) {
+        InputImage image = InputImage.fromBitmap(bitmap, 0);
+        if(options != null) {
+            barcodeScanner = BarcodeScanning.getClient(options);
+        }else {
+            barcodeScanner = BarcodeScanning.getClient();
+        }
+        // Or, to specify the formats to recognize:
+        // BarcodeScanner scanner = BarcodeScanning.getClient(options);
+        // [END get_detector]
+
+        // [START run_detector]
+        Task<List<Barcode>> result = barcodeScanner.process(image)
+                .addOnSuccessListener(new OnSuccessListener<List<Barcode>>() {
+                    @Override
+                    public void onSuccess(List<Barcode> barcodes) {
+                        if (barcodes.isEmpty()) {
+                            Log.v(TAG, "No barcode has been detected");
+                        }
+                        if(isAnalyze()) {
+                            if(onScanListener != null) {
+                                if(!barcodes.isEmpty()) {
+                                    playBeepAndVibrate();
+                                }
+                                onScanListener.onSuccess(barcodes, graphicOverlay,
+                                        InputImage.fromBitmap(bitmap, 0));
+                            }
+                        }
+                    }
+                })
+                .addOnFailureListener(new OnFailureListener() {
+                    @Override
+                    public void onFailure(@NonNull Exception e) {
+                        Log.e(TAG, "Barcode detection failed " + e);
+                        if(onScanListener != null) {
+                            onScanListener.onFail(1, e);
+                        }
+                    }
+                });
+    }
+
+    public interface OnScanListener {
+        void onSuccess(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image);
+        void onFail(int code, Exception e);
+    }
+
+    public OnScanListener onScanListener;
+
+    public void setOnScanListener(OnScanListener listener) {
+        onScanListener = listener;
+    }
+
+    /**
+     * 设置是否分析图像,通过此方法可以动态控制是否分析图像,常用于中断扫码识别。如:连扫时,扫到结果,然后停止分析图像
+     *
+     * 1. 因为分析图像默认为true,如果想支持连扫,设置setAnalyze(false)即可。
+     *
+     * 2. 如果只是想拦截扫码结果回调自己处理逻辑,但并不想继续分析图像(即不想连扫),可通过
+     * 调用setAnalyze(false)来停止分析图像。
+     * @param isAnalyze
+     */
+    public void setAnalyze(boolean isAnalyze) {
+        this.isAnalyze = isAnalyze;
+    }
+
+    public boolean isAnalyze() {
+        return isAnalyze;
+    }
+
+    public void setBarcodeFormats(BarcodeScannerOptions options) {
+        this.options = options;
+    }
+
+    public void switchCamera() {
+        int numberOfCameras = Camera.getNumberOfCameras();// 获取摄像头个数
+        if (numberOfCameras == 1) {
+            return;
+        }
+        if(cameraSource != null) {
+            if(cameraSource.getCameraFacing() == CameraSource.CAMERA_FACING_FRONT) {
+                cameraSource.setFacing(CameraSource.CAMERA_FACING_BACK);
+            }else {
+                cameraSource.setFacing(CameraSource.CAMERA_FACING_FRONT);
+            }
+        }
+        preview.stop();
+        startCameraSource();
+    }
+
+    public void setFacing(int facing) {
+        if(cameraSource != null) {
+            cameraSource.setFacing(facing);
+        }
+    }
+
+    public void setPlayBeepAndVibrate(boolean playBeep, boolean vibrate) {
+        this.playBeep = playBeep;
+        this.vibrate = vibrate;
+    }
+
+    public void playBeepAndVibrate() {
+        if(beepManager != null) {
+            beepManager.playBeepSoundAndVibrate(playBeep, vibrate);
+        }
+    }
+
+    public boolean hasLight() {
+        return activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+    }
+
+    /**
+     * 开关闪关灯
+     */
+    public void switchLight() {
+        if (hasLight()) {
+            if (isOpenLight) {
+                closeTorch();
+            } else {
+                openTorch();
+            }
+            isOpenLight = !isOpenLight;
+        }
+    }
+
+    public void openTorch() {
+        if(cameraSource != null) {
+            cameraSource.setTorch(true);
+        }
+    }
+
+    public void closeTorch() {
+        if(cameraSource != null) {
+            cameraSource.setTorch(false);
+        }
+    }
+
+    private void createCameraSource() {
+        // If there's no existing cameraSource, create one.
+        if (cameraSource == null) {
+            cameraSource = new CameraSource(activity, graphicOverlay);
+        }
+
+        barcodeScannerProcessor = new BarcodeScannerProcessor(activity, this);
+        cameraSource.setMachineLearningFrameProcessor(barcodeScannerProcessor);
+    }
+
+    /**
+     * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet
+     * (e.g., because onResume was called before the camera source was created), this will be called
+     * again when the camera source is created.
+     */
+    private void startCameraSource() {
+        if (cameraSource != null) {
+            requirePermission(new CallBack() {
+                @Override
+                public void call() {
+                    try {
+                        if (preview == null) {
+                            Log.d(TAG, "resume: Preview is null");
+                        }
+                        if (graphicOverlay == null) {
+                            Log.d(TAG, "resume: graphOverlay is null");
+                        }
+                        preview.start(cameraSource, graphicOverlay);
+                        cameraSource.setOnCameraListener(new CameraSource.OnCameraListener() {
+                            @Override
+                            public void open(Camera camera) {
+                                new GestureDetectorUtil(preview, camera);
+                            }
+                        });
+                    } catch (IOException e) {
+                        Log.e(TAG, "Unable to start camera source.", e);
+                        cameraSource.release();
+                        cameraSource = null;
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * 再一次进行扫描识别
+     */
+    public void startProcessor() {
+        barcodeScannerProcessor = new BarcodeScannerProcessor(activity, this);
+        cameraSource.setMachineLearningFrameProcessor(barcodeScannerProcessor);
+    }
+
+    /**
+     * 扫描结果后,停止扫描识别
+     * @see OnScanListener#onSuccess(List, GraphicOverlay, InputImage)
+     */
+    public void stopProcessor() {
+        if (barcodeScannerProcessor != null) {
+            barcodeScannerProcessor.stop();
+        }
+    }
+
+    private interface CallBack {
+        void call();
+    }
+
+    private void requirePermission(CallBack callBack) {
+        PermissionUtil.getInstance().with(activity).requestPermissions(new String[]{Manifest.permission.CAMERA,
+                Manifest.permission.VIBRATE}, new PermissionUtil.PermissionListener() {
+            @Override
+            public void onGranted() {
+                if(callBack != null) {
+                    callBack.call();
+                }
+            }
+
+            @Override
+            public void onDenied(List<String> deniedPermission) {
+                PermissionUtil.getInstance().showDialogTips(activity, deniedPermission, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+//                        activity.finish();
+                    }
+                });
+            }
+
+            @Override
+            public void onShouldShowRationale(List<String> deniedPermission) {
+//                requirePermission(callBack);
+            }
+        });
+    }
+}

+ 511 - 0
barcode/src/main/java/com/kathline/barcode/PermissionUtil.java

@@ -0,0 +1,511 @@
+package com.kathline.barcode;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * PermissionUtil.getInstance().with(this).requestPermissions()
+ */
+public class PermissionUtil {
+
+    private static final String TAG = "PermissionsUtil";
+
+    private PermissionFragment fragment;
+
+    private static PermissionUtil mInstance;
+
+    public static PermissionUtil getInstance() {
+        if(mInstance == null) {
+            synchronized (PermissionUtil.class) {
+                if(mInstance == null) {
+                    mInstance = new PermissionUtil();
+                }
+            }
+        }
+        return mInstance;
+    }
+
+    public PermissionUtil with(@NonNull FragmentActivity activity) {
+        fragment = getPermissionsFragment(activity);
+        return this;
+    }
+
+    public PermissionUtil with(@NonNull Fragment fragmentX) {
+        fragment = getPermissionsFragment(fragmentX);
+        return this;
+    }
+
+    public void showDialogTips(Context context, List<String> permission, DialogInterface.OnClickListener onDenied) {
+        showDialogTips(context, String.format("您拒绝了相关权限,无法正常使用本功能。请前往 设置->应用管理->%s->权限管理中启用 %s 权限",
+                context.getString(R.string.app_name),
+                listToString(permission)
+        ), permission, onDenied);
+    }
+
+    public void showDialogTips(Context context, String msg, List<String> permission, DialogInterface.OnClickListener onDenied) {
+        String message = TextUtils.isEmpty(msg) ? String.format("您拒绝了相关权限,无法正常使用本功能。请前往 设置->应用管理->%s->权限管理中启用 %s 权限", context.getString(R.string.app_name), listToString(permission)) : msg;
+        android.app.AlertDialog alertDialog = new AlertDialog.Builder(context).setTitle("权限被禁用").setMessage(message).setCancelable(false)
+                .setNegativeButton("返回", onDenied)
+                .setPositiveButton("去设置", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int i) {
+                        Intent intent = PermissionUtil.PermissionSettingPage.getSmartPermissionIntent(context, permission);
+                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                        context.startActivity(intent);
+//                        String manufacturer = Build.MANUFACTURER;
+//                        String model = Build.MODEL;//手机型号,如MI 6,MI 9 SE
+//                        if(manufacturer.equalsIgnoreCase("xiaomi")) {
+//                            Intent intent=new Intent();
+//                            //model.toUpperCase().contains("MI 6")
+//                            intent.setAction("miui.intent.action.APP_PERM_EDITOR");
+//                            intent.putExtra("extra_pkgname", context.getPackageName());
+//                            context.startActivity(intent);
+//                        }else {
+//                            //第二个参数为包名
+//                            Uri uri = Uri.fromParts("package", context.getPackageName(), null);
+//                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+//                            intent.setData(uri);
+//                            context.startActivity(intent);
+//                        }
+                    }
+                }).create();
+        alertDialog.show();
+    }
+
+    public static String listToString(List<String> list) {
+        StringBuilder builder = new StringBuilder();
+        int size = list.size();
+        for (int i = 0; i < size; i++) {
+            if (i < size - 1) {
+                builder.append(list.get(i)).append(",");
+            } else {
+                builder.append(list.get(i));
+            }
+        }
+        return builder.toString();
+    }
+
+    private PermissionFragment getPermissionsFragment(FragmentActivity activity) {
+        PermissionFragment fragment = (PermissionFragment) activity.getSupportFragmentManager().findFragmentByTag(TAG);
+        boolean isNewInstance = fragment == null;
+        if (isNewInstance) {
+            fragment = new PermissionFragment();
+            FragmentManager fragmentManager = activity.getSupportFragmentManager();
+            fragmentManager
+                    .beginTransaction()
+                    .add(fragment, TAG)
+                    .commitNow();
+        }
+
+        return fragment;
+    }
+
+    private PermissionFragment getPermissionsFragment(Fragment fragmentX) {
+        PermissionFragment fragment = (PermissionFragment) fragmentX.getChildFragmentManager().findFragmentByTag(TAG);
+        boolean isNewInstance = fragment == null;
+        if (isNewInstance) {
+            fragment = new PermissionFragment();
+            FragmentManager fragmentManager = fragmentX.getChildFragmentManager();
+            fragmentManager
+                    .beginTransaction()
+                    .add(fragment, TAG)
+                    .commitNow();
+        }
+
+        return fragment;
+    }
+
+    /**
+     * 外部调用申请权限
+     * @param permissions 申请的权限
+     * @param listener 监听权限接口
+     */
+    public void requestPermissions(String[] permissions, PermissionListener listener) {
+        fragment.setListener(listener);
+        fragment.requestPermissions(permissions);
+    }
+
+    public interface PermissionListener {
+        void onGranted();
+
+        void onDenied(List<String> deniedPermission);
+
+        void onShouldShowRationale(List<String> deniedPermission);
+    }
+
+    public static class PermissionFragment extends Fragment {
+        /**
+         * 申请权限的requestCode
+         */
+        private static final int PERMISSIONS_REQUEST_CODE = 1;
+
+        /**
+         * 权限监听接口
+         */
+        private PermissionListener listener;
+        public void setListener(PermissionListener listener) {
+            this.listener = listener;
+        }
+
+        @Override
+        public void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setRetainInstance(true);
+        }
+
+        /**
+         * 申请权限
+         * @param permissions 需要申请的权限
+         */
+        public void requestPermissions(@NonNull String[] permissions) {
+            List<String> requestPermissionList = new ArrayList<>();
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                //找出所有未授权的权限
+                for (String permission : permissions) {
+                    if (ContextCompat.checkSelfPermission(getContext(), permission) != PackageManager.PERMISSION_GRANTED) {
+                        requestPermissionList.add(permission);
+                    }
+                }
+                if (requestPermissionList.isEmpty()) {
+                    //已经全部授权
+                    permissionAllGranted();
+                } else {
+                    //申请授权
+                    requestPermissions(requestPermissionList.toArray(new String[requestPermissionList.size()]), PERMISSIONS_REQUEST_CODE);
+                }
+            }else {
+                //已经全部授权
+                permissionAllGranted();
+            }
+
+        }
+
+        /**
+         * fragment回调处理权限的结果
+         * @param requestCode 请求码 要等于申请时候的请求码
+         * @param permissions 申请的权限
+         * @param grantResults 对应权限的处理结果
+         */
+        @Override
+        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+            if (requestCode != PERMISSIONS_REQUEST_CODE) {
+                return;
+            }
+
+            if (grantResults.length > 0) {
+                List<String> deniedPermissionList = new ArrayList<>();
+                for (int i = 0; i < grantResults.length; i++) {
+                    if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
+                        deniedPermissionList.add(permissions[i]);
+                    }
+                }
+
+                if (deniedPermissionList.isEmpty()) {
+                    //已经全部授权
+                    permissionAllGranted();
+                } else {
+
+                    //勾选了对话框中”Don’t ask again”的选项, 返回false
+                    for (String deniedPermission : deniedPermissionList) {
+                        boolean flag = shouldShowRequestPermissionRationale(deniedPermission);
+                        if (flag) {
+                            //拒绝授权
+                            permissionShouldShowRationale(deniedPermissionList);
+                            return;
+                        }
+                    }
+                    //拒绝授权
+                    permissionHasDenied(deniedPermissionList);
+
+                }
+
+
+            }
+
+        }
+
+        /**
+         * 权限全部已经授权
+         */
+        private void permissionAllGranted() {
+            if (listener != null) {
+                listener.onGranted();
+            }
+        }
+
+        /**
+         * 有权限被拒绝
+         *
+         * @param deniedList 被拒绝的权限
+         */
+        private void permissionHasDenied(List<String> deniedList) {
+            if (listener != null) {
+                listener.onDenied(deniedList);
+            }
+        }
+
+        /**
+         * 权限被拒绝并且勾选了不在询问
+         *
+         * @param deniedList 勾选了不在询问的权限
+         */
+        private void permissionShouldShowRationale(List<String> deniedList) {
+            if (listener != null) {
+                listener.onShouldShowRationale(deniedList);
+            }
+        }
+    }
+
+    /**
+     * 是否是 Android 11 及以上版本
+     */
+    static boolean isAndroid11() {
+        return Build.VERSION.SDK_INT >= 11;//Build.VERSION_CODES.R
+    }
+
+    /**
+     * 是否是 Android 10 及以上版本
+     */
+    static boolean isAndroid10() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+    }
+
+    /**
+     * 是否是 Android 9.0 及以上版本
+     */
+    static boolean isAndroid9() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+    }
+
+    /**
+     * 是否是 Android 8.0 及以上版本
+     */
+    static boolean isAndroid8() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+    }
+
+    /**
+     * 是否是 Android 7.0 及以上版本
+     */
+    static boolean isAndroid7() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
+    }
+
+    /**
+     * 是否是 Android 6.0 及以上版本
+     */
+    static boolean isAndroid6() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+    }
+
+    /**
+     * 判断某个权限集合是否包含特殊权限
+     */
+    static boolean containsSpecialPermission(List<String> permissions) {
+        if (permissions == null || permissions.isEmpty()) {
+            return false;
+        }
+
+        for (String permission : permissions) {
+            if (isSpecialPermission(permission)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断某个权限是否是特殊权限
+     */
+    static boolean isSpecialPermission(String permission) {
+        return Permission.MANAGE_EXTERNAL_STORAGE.equals(permission) ||
+                Permission.REQUEST_INSTALL_PACKAGES.equals(permission) ||
+                Permission.SYSTEM_ALERT_WINDOW.equals(permission) ||
+                Permission.NOTIFICATION_SERVICE.equals(permission) ||
+                Permission.WRITE_SETTINGS.equals(permission);
+    }
+
+    public static class Permission {
+        /** 外部存储权限(特殊权限,需要 Android 11 及以上) */
+        public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
+
+        /** 安装应用权限(特殊权限,需要 Android 8.0 及以上) */
+        public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES";
+
+        /** 通知栏权限(特殊权限,需要 Android 6.0 及以上,注意此权限不需要在清单文件中注册也能申请) */
+        public static final String NOTIFICATION_SERVICE = "android.permission.NOTIFICATION_SERVICE";
+
+        /** 悬浮窗权限(特殊权限,需要 Android 6.0 及以上) */
+        public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW";
+
+        /** 系统设置权限(特殊权限,需要 Android 6.0 及以上) */
+        public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
+    }
+
+    public static class PermissionSettingPage {
+        /**
+         * 根据传入的权限自动选择最合适的权限设置页
+         */
+        public static Intent getSmartPermissionIntent(Context context, List<String> deniedPermissions) {
+            // 如果失败的权限里面不包含特殊权限
+            if (deniedPermissions == null || deniedPermissions.isEmpty() || !PermissionUtil.containsSpecialPermission(deniedPermissions)) {
+                return PermissionSettingPage.getApplicationDetailsIntent(context);
+            }
+
+            // 如果当前只有一个权限被拒绝了
+            if (deniedPermissions.size() == 1) {
+
+                String permission = deniedPermissions.get(0);
+                if (Permission.MANAGE_EXTERNAL_STORAGE.equals(permission)) {
+                    return getStoragePermissionIntent(context);
+                }
+
+                if (Permission.REQUEST_INSTALL_PACKAGES.equals(permission)) {
+                    return getInstallPermissionIntent(context);
+                }
+
+                if (Permission.SYSTEM_ALERT_WINDOW.equals(permission)) {
+                    return getWindowPermissionIntent(context);
+                }
+
+                if (Permission.NOTIFICATION_SERVICE.equals(permission)) {
+                    return getNotifyPermissionIntent(context);
+                }
+
+                if (Permission.WRITE_SETTINGS.equals(permission)) {
+                    return getSettingPermissionIntent(context);
+                }
+
+                return getApplicationDetailsIntent(context);
+            }
+
+            if (PermissionUtil.isAndroid11() && deniedPermissions.size() == 3 &&
+                    (deniedPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE) &&
+                            deniedPermissions.contains(Manifest.permission.READ_EXTERNAL_STORAGE) &&
+                            deniedPermissions.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE))) {
+                return getStoragePermissionIntent(context);
+            }
+
+            return PermissionSettingPage.getApplicationDetailsIntent(context);
+        }
+
+        /**
+         * 获取应用详情界面意图
+         */
+        static Intent getApplicationDetailsIntent(Context context) {
+            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+            intent.setData(Uri.parse("package:" + context.getPackageName()));
+            return intent;
+        }
+
+        /**
+         * 获取安装权限设置界面意图
+         */
+        static Intent getInstallPermissionIntent(Context context) {
+            Intent intent = null;
+            if (PermissionUtil.isAndroid8()) {
+                intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
+                intent.setData(Uri.parse("package:" + context.getPackageName()));
+            }
+            if (intent == null || !areActivityIntent(context, intent)) {
+                intent = getApplicationDetailsIntent(context);
+            }
+            return intent;
+        }
+
+        /**
+         * 获取悬浮窗权限设置界面意图
+         */
+        static Intent getWindowPermissionIntent(Context context) {
+            Intent intent = null;
+            if (PermissionUtil.isAndroid6()) {
+                intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
+                // 在 Android 11 上面不能加包名跳转,因为就算加了也没有效果
+                // 还有人反馈在 Android 11 的 TV 模拟器上会出现崩溃的情况
+                // https://developer.android.google.cn/reference/android/provider/Settings#ACTION_MANAGE_OVERLAY_PERMISSION
+                if (!PermissionUtil.isAndroid11()) {
+                    intent.setData(Uri.parse("package:" + context.getPackageName()));
+                }
+            }
+
+            if (intent == null || !areActivityIntent(context, intent)) {
+                intent = getApplicationDetailsIntent(context);
+            }
+            return intent;
+        }
+
+        /**
+         * 获取通知栏权限设置界面意图
+         */
+        static Intent getNotifyPermissionIntent(Context context) {
+            Intent intent = null;
+            if (PermissionUtil.isAndroid8()) {
+                intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+                intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
+                //intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.getApplicationInfo().uid);
+            }
+            if (intent == null || !areActivityIntent(context, intent)) {
+                intent = getApplicationDetailsIntent(context);
+            }
+            return intent;
+        }
+
+        /**
+         * 获取系统设置权限界面意图
+         */
+        static Intent getSettingPermissionIntent(Context context) {
+            Intent intent = null;
+            if (PermissionUtil.isAndroid6()) {
+                intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
+                intent.setData(Uri.parse("package:" + context.getPackageName()));
+            }
+            if (intent == null || !areActivityIntent(context, intent)) {
+                intent = getApplicationDetailsIntent(context);
+            }
+            return intent;
+        }
+
+        /**
+         * 获取存储权限设置界面意图
+         */
+        static Intent getStoragePermissionIntent(Context context) {
+            Intent intent = null;
+            if (PermissionUtil.isAndroid11()) {
+                intent = new Intent("android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION");//Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
+                intent.setData(Uri.parse("package:" + context.getPackageName()));
+            }
+            if (intent == null || !areActivityIntent(context, intent)) {
+                intent = getApplicationDetailsIntent(context);
+            }
+            return intent;
+        }
+
+        /**
+         * 判断这个意图的 Activity 是否存在
+         */
+        private static boolean areActivityIntent(Context context, Intent intent) {
+            return !context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
+        }
+    }
+
+}

+ 124 - 0
barcode/src/main/java/com/kathline/barcode/PreferenceUtils.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.google.android.gms.common.images.Size;
+import com.google.common.base.Preconditions;
+
+/** Utility class to retrieve shared preferences. */
+public class PreferenceUtils {
+
+  private static final int POSE_DETECTOR_PERFORMANCE_MODE_FAST = 1;
+
+  static void saveString(Context context, @StringRes int prefKeyId, @Nullable String value) {
+    PreferenceManager.getDefaultSharedPreferences(context)
+        .edit()
+        .putString(context.getString(prefKeyId), value)
+        .apply();
+  }
+
+  @Nullable
+  public static CameraSource.SizePair getCameraPreviewSizePair(Context context, int cameraId) {
+    Preconditions.checkArgument(
+        cameraId == CameraSource.CAMERA_FACING_BACK
+            || cameraId == CameraSource.CAMERA_FACING_FRONT);
+    String previewSizePrefKey;
+    String pictureSizePrefKey;
+    if (cameraId == CameraSource.CAMERA_FACING_BACK) {
+      previewSizePrefKey = context.getString(R.string.pref_key_rear_camera_preview_size);
+      pictureSizePrefKey = context.getString(R.string.pref_key_rear_camera_picture_size);
+    } else {
+      previewSizePrefKey = context.getString(R.string.pref_key_front_camera_preview_size);
+      pictureSizePrefKey = context.getString(R.string.pref_key_front_camera_picture_size);
+    }
+
+    try {
+      SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+      return new CameraSource.SizePair(
+          Size.parseSize(sharedPreferences.getString(previewSizePrefKey, null)),
+          Size.parseSize(sharedPreferences.getString(pictureSizePrefKey, null)));
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  public static boolean shouldShowPoseDetectionInFrameLikelihoodLivePreview(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey =
+        context.getString(R.string.pref_key_live_preview_pose_detector_show_in_frame_likelihood);
+    return sharedPreferences.getBoolean(prefKey, true);
+  }
+
+  public static boolean shouldShowPoseDetectionInFrameLikelihoodStillImage(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey =
+        context.getString(R.string.pref_key_still_image_pose_detector_show_in_frame_likelihood);
+    return sharedPreferences.getBoolean(prefKey, true);
+  }
+
+  public static boolean shouldPoseDetectionVisualizeZ(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey = context.getString(R.string.pref_key_pose_detector_visualize_z);
+    return sharedPreferences.getBoolean(prefKey, true);
+  }
+
+  public static boolean shouldPoseDetectionRescaleZForVisualization(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey = context.getString(R.string.pref_key_pose_detector_rescale_z);
+    return sharedPreferences.getBoolean(prefKey, true);
+  }
+
+  public static boolean shouldPoseDetectionRunClassification(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey = context.getString(R.string.pref_key_pose_detector_run_classification);
+    return sharedPreferences.getBoolean(prefKey, false);
+  }
+
+  public static boolean shouldSegmentationEnableRawSizeMask(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey =
+        context.getString(R.string.pref_key_segmentation_raw_size_mask);
+    return sharedPreferences.getBoolean(prefKey, false);
+  }
+
+  /**
+   * Mode type preference is backed by {@link android.preference.ListPreference} which only support
+   * storing its entry value as string type, so we need to retrieve as string and then convert to
+   * integer.
+   */
+  private static int getModeTypePreferenceValue(
+          Context context, @StringRes int prefKeyResId, int defaultValue) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey = context.getString(prefKeyResId);
+    return Integer.parseInt(sharedPreferences.getString(prefKey, String.valueOf(defaultValue)));
+  }
+
+  public static boolean isCameraLiveViewportEnabled(Context context) {
+    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String prefKey = context.getString(R.string.pref_key_camera_live_viewport);
+    return sharedPreferences.getBoolean(prefKey, true);
+  }
+
+  private PreferenceUtils() {}
+}

+ 62 - 0
barcode/src/main/java/com/kathline/barcode/ScopedExecutor.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Wraps an existing executor to provide a {@link #shutdown} method that allows subsequent
+ * cancellation of submitted runnables.
+ */
+public class ScopedExecutor implements Executor {
+
+  private final Executor executor;
+  private final AtomicBoolean shutdown = new AtomicBoolean();
+
+  public ScopedExecutor(@NonNull Executor executor) {
+    this.executor = executor;
+  }
+
+  @Override
+  public void execute(@NonNull Runnable command) {
+    // Return early if this object has been shut down.
+    if (shutdown.get()) {
+      return;
+    }
+    executor.execute(
+        () -> {
+          // Check again in case it has been shut down in the mean time.
+          if (shutdown.get()) {
+            return;
+          }
+          command.run();
+        });
+  }
+
+  /**
+   * After this method is called, no runnables that have been submitted or are subsequently
+   * submitted will start to execute, turning this executor into a no-op.
+   *
+   * <p>Runnables that have already started to execute will continue.
+   */
+  public void shutdown() {
+    shutdown.set(true);
+  }
+}

+ 185 - 0
barcode/src/main/java/com/kathline/barcode/UriUtils.java

@@ -0,0 +1,185 @@
+package com.kathline.barcode;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.File;
+import java.util.Locale;
+
+public class UriUtils {
+    private static final String TAG = "UriUtils";
+
+    /**
+     * 转换为可以识别展示的路径
+     * @param context
+     * @param path
+     * @return
+     */
+    public static String getPath(Context context, String path) {
+        Uri uri;
+        if(path.startsWith("content://")) {
+            uri = Uri.parse(path);
+        }else {
+            uri = Uri.parse("file://" + path);
+        }
+        return getPath(context, uri);
+    }
+
+    @SuppressLint("NewApi")
+    public static String getPath(Context context, Uri uri) {
+        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+        // DocumentProvider
+        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+            if (isExternalStorageDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                if ("primary".equalsIgnoreCase(type)) {
+                    if (beforeAndroidTen()) {
+                        return Environment.getExternalStorageDirectory() + "/" + split[1];
+                    } else {
+                        return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "/" + split[1];
+                    }
+                }
+            }
+            // DownloadsProvider
+            else if (isDownloadsDocument(uri)) {
+                final String id = DocumentsContract.getDocumentId(uri);
+                final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+                return getDataColumn(context, contentUri, null, null);
+            }
+            // MediaProvider
+            else if (isMediaDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                Uri contentUri = null;
+                if ("image".equals(type)) {
+                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+                } else if ("video".equals(type)) {
+                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                } else if ("audio".equals(type)) {
+                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                } else {
+                    contentUri = MediaStore.Files.getContentUri("external");
+                }
+                final String selection = "_id=?";
+                final String[] selectionArgs = new String[]{split[1]};
+                return getDataColumn(context, contentUri, selection, selectionArgs);
+            }
+        }
+        // MediaStore (and general)
+        else if ("content".equalsIgnoreCase(uri.getScheme())) {
+            // Return the remote address
+            if (isGooglePhotosUri(uri)) {
+                return uri.getLastPathSegment();
+            }
+            return getDataColumn(context, uri, null, null);
+        }
+        // File
+        else if ("file".equalsIgnoreCase(uri.getScheme())) {
+            return uri.getPath();
+        }
+        return null;
+    }
+
+    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
+        Cursor cursor = null;
+        final String column = "_data";
+        final String[] projection = {column};
+        try {
+            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+                    null);
+            if (cursor != null && cursor.moveToFirst()) {
+                final int column_index = cursor.getColumnIndexOrThrow(column);
+                return cursor.getString(column_index);
+            }
+        } catch (IllegalArgumentException ex) {
+            Log.i(TAG, String.format(Locale.getDefault(), "getDataColumn: _data - [%s]", ex.getMessage()));
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     */
+    private static boolean isExternalStorageDocument(Uri uri) {
+        return "com.android.externalstorage.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     */
+    private static boolean isDownloadsDocument(Uri uri) {
+        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     */
+    private static boolean isMediaDocument(Uri uri) {
+        return "com.android.providers.media.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * 判断是否是Google相册的图片,类似于content://com.google.android.apps.photos.content/...
+     **/
+    public static boolean isGooglePhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+    }
+
+    /**
+     * 判断是否是Google相册的图片,类似于content://com.google.android.apps.photos.contentprovider/0/1/mediakey:/local%3A821abd2f-9f8c-4931-bbe9-a975d1f5fabc/ORIGINAL/NONE/1075342619
+     **/
+    public static boolean isGooglePlayPhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
+    }
+
+    /**
+     * 图片路径转uri
+     */
+    public static Uri getUriByPath(Context context, String path) {
+        ContentResolver contentResolver = context.getContentResolver();
+        Uri contentUri = MediaStore.Files.getContentUri("external");
+        Cursor cursor = contentResolver.query(contentUri, new String[]{MediaStore.Files.FileColumns._ID},
+                MediaStore.Files.FileColumns.DATA + "=? ", new String[]{path},
+                null);
+        if (cursor != null && cursor.moveToFirst()) {
+            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
+            cursor.close();
+            return Uri.withAppendedPath(contentUri, "" + id);
+        } else {
+            if (new File(path).exists()) {
+                ContentValues values = new ContentValues();
+                values.put(MediaStore.Images.Media.DATA, path);
+                return contentResolver.insert(contentUri, values);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    //Android 10 之前的版本
+    public static boolean beforeAndroidTen() {
+        return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
+    }
+}

+ 568 - 0
barcode/src/main/java/com/kathline/barcode/ViewfinderView.java

@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) Jenly, MLKit 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.
+ */
+package com.kathline.barcode;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+public class ViewfinderView extends View {
+
+    /**
+     * 画笔
+     */
+    private Paint paint;
+
+    /**
+     * 文本画笔
+     */
+    private TextPaint textPaint;
+    /**
+     * 扫码框外面遮罩颜色
+     */
+    private int maskColor;
+    /**
+     * 扫描区域边框颜色
+     */
+    private int frameColor;
+    /**
+     * 扫描线颜色
+     */
+    private int laserColor;
+    /**
+     * 扫码框四角颜色
+     */
+    private int cornerColor;
+
+    /**
+     * 提示文本与扫码框的边距
+     */
+    private float labelTextPadding;
+    /**
+     * 提示文本的宽度
+     */
+    private int labelTextWidth;
+    /**
+     * 提示文本的位置
+     */
+    private TextLocation labelTextLocation;
+    /**
+     * 扫描区域提示文本
+     */
+    private String labelText;
+    /**
+     * 扫描区域提示文本颜色
+     */
+    private int labelTextColor;
+    /**
+     * 提示文本字体大小
+     */
+    private float labelTextSize;
+
+    /**
+     * 扫描线开始位置
+     */
+    public int scannerStart = 0;
+    /**
+     * 扫描线结束位置
+     */
+    public int scannerEnd = 0;
+
+    /**
+     * 扫码框宽
+     */
+    private int frameWidth;
+    /**
+     * 扫码框高
+     */
+    private int frameHeight;
+    /**
+     * 扫描激光线风格
+     */
+    private LaserStyle laserStyle;
+
+    /**
+     * 网格列数
+     */
+    private int gridColumn;
+    /**
+     * 网格高度
+     */
+    private int gridHeight;
+
+    /**
+     * 扫码框
+     */
+    private Rect frame;
+
+    /**
+     * 扫描区边角的宽
+     */
+    private int cornerRectWidth;
+    /**
+     * 扫描区边角的高
+     */
+    private int cornerRectHeight;
+    /**
+     * 扫描线每次移动距离
+     */
+    private int scannerLineMoveDistance;
+    /**
+     * 扫描线高度
+     */
+    private int scannerLineHeight;
+
+    /**
+     * 边框线宽度
+     */
+    private int frameLineWidth;
+
+    /**
+     * 扫描动画延迟间隔时间 默认20毫秒
+     */
+    private int scannerAnimationDelay;
+
+    /**
+     * 扫码框占比
+     */
+    private float frameRatio;
+
+    /**
+     * 扫码框内间距
+     */
+    private float framePaddingLeft;
+    private float framePaddingTop;
+    private float framePaddingRight;
+    private float framePaddingBottom;
+    /**
+     * 扫码框对齐方式
+     */
+    private FrameGravity frameGravity;
+
+
+    private Point point;
+    private int pointColor;
+    private int pointStrokeColor;
+
+    private float pointRadius;
+    private float pointStrokeRatio = 1.2f;
+
+
+    public enum LaserStyle{
+        NONE(0),LINE(1),GRID(2);
+        private int mValue;
+        LaserStyle(int value){
+            mValue = value;
+        }
+
+        private static LaserStyle getFromInt(int value){
+            for(LaserStyle style : LaserStyle.values()){
+                if(style.mValue == value){
+                    return style;
+                }
+            }
+            return LaserStyle.LINE;
+        }
+    }
+
+    public enum TextLocation {
+        TOP(0),BOTTOM(1);
+
+        private int mValue;
+
+        TextLocation(int value){
+            mValue = value;
+        }
+
+        private static TextLocation getFromInt(int value){
+            for(TextLocation location : TextLocation.values()){
+                if(location.mValue == value){
+                    return location;
+                }
+            }
+            return TextLocation.TOP;
+        }
+    }
+
+
+    public enum FrameGravity {
+        CENTER(0), LEFT(1), TOP(2), RIGHT(3), BOTTOM(4);
+
+        private int mValue;
+
+        FrameGravity(int value) {
+            mValue = value;
+        }
+
+        private static FrameGravity getFromInt(int value) {
+            for (FrameGravity gravity : values()) {
+                if (gravity.mValue == value) {
+                    return gravity;
+                }
+            }
+            return CENTER;
+        }
+    }
+
+    public ViewfinderView(Context context) {
+        this(context,null);
+    }
+
+    public ViewfinderView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs,0);
+    }
+
+    public ViewfinderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context,attrs);
+    }
+
+
+    private void init(Context context, AttributeSet attrs) {
+        //初始化自定义属性信息
+        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ViewfinderView);
+        maskColor = array.getColor(R.styleable.ViewfinderView_maskColor, ContextCompat.getColor(context,R.color.viewfinder_mask));
+        frameColor = array.getColor(R.styleable.ViewfinderView_frameColor, ContextCompat.getColor(context,R.color.viewfinder_frame));
+        cornerColor = array.getColor(R.styleable.ViewfinderView_cornerColor, ContextCompat.getColor(context,R.color.viewfinder_corner));
+        laserColor = array.getColor(R.styleable.ViewfinderView_laserColor, ContextCompat.getColor(context,R.color.viewfinder_laser));
+
+        labelText = array.getString(R.styleable.ViewfinderView_labelText);
+        labelTextColor = array.getColor(R.styleable.ViewfinderView_labelTextColor, ContextCompat.getColor(context,R.color.viewfinder_text_color));
+        labelTextSize = array.getDimension(R.styleable.ViewfinderView_labelTextSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,14f,getResources().getDisplayMetrics()));
+        labelTextPadding = array.getDimension(R.styleable.ViewfinderView_labelTextPadding, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,24,getResources().getDisplayMetrics()));
+        labelTextWidth = array.getDimensionPixelSize(R.styleable.ViewfinderView_labelTextWidth,0);
+        labelTextLocation = TextLocation.getFromInt(array.getInt(R.styleable.ViewfinderView_labelTextLocation,0));
+
+        frameWidth = array.getDimensionPixelSize(R.styleable.ViewfinderView_frameWidth,0);
+        frameHeight = array.getDimensionPixelSize(R.styleable.ViewfinderView_frameHeight,0);
+
+        laserStyle = LaserStyle.getFromInt(array.getInt(R.styleable.ViewfinderView_laserStyle, LaserStyle.LINE.mValue));
+        gridColumn = array.getInt(R.styleable.ViewfinderView_gridColumn,20);
+        gridHeight = (int)array.getDimension(R.styleable.ViewfinderView_gridHeight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,40,getResources().getDisplayMetrics()));
+
+        cornerRectWidth = (int)array.getDimension(R.styleable.ViewfinderView_cornerRectWidth, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,4,getResources().getDisplayMetrics()));
+        cornerRectHeight = (int)array.getDimension(R.styleable.ViewfinderView_cornerRectHeight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,16,getResources().getDisplayMetrics()));
+        scannerLineMoveDistance = (int)array.getDimension(R.styleable.ViewfinderView_scannerLineMoveDistance, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,2,getResources().getDisplayMetrics()));
+        scannerLineHeight = (int)array.getDimension(R.styleable.ViewfinderView_scannerLineHeight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics()));
+        frameLineWidth = (int)array.getDimension(R.styleable.ViewfinderView_frameLineWidth, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,getResources().getDisplayMetrics()));
+        scannerAnimationDelay = array.getInteger(R.styleable.ViewfinderView_scannerAnimationDelay,20);
+        frameRatio = array.getFloat(R.styleable.ViewfinderView_frameRatio,0.625f);
+        framePaddingLeft = array.getDimension(R.styleable.ViewfinderView_framePaddingLeft,0);
+        framePaddingTop = array.getDimension(R.styleable.ViewfinderView_framePaddingTop,0);
+        framePaddingRight = array.getDimension(R.styleable.ViewfinderView_framePaddingRight,0);
+        framePaddingBottom = array.getDimension(R.styleable.ViewfinderView_framePaddingBottom,0);
+        frameGravity = FrameGravity.getFromInt(array.getInt(R.styleable.ViewfinderView_frameGravity, FrameGravity.CENTER.mValue));
+        array.recycle();
+
+        pointColor = laserColor;
+        pointStrokeColor = Color.WHITE;
+
+        pointRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,getResources().getDisplayMetrics());
+        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+
+    }
+
+    private DisplayMetrics getDisplayMetrics(){
+        return getResources().getDisplayMetrics();
+    }
+
+    public void setLabelText(String labelText) {
+        this.labelText = labelText;
+    }
+
+    public void setLabelTextColor(@ColorInt int color) {
+        this.labelTextColor = color;
+    }
+
+    public void setLabelTextColorResource(@ColorRes int id){
+        this.labelTextColor = ContextCompat.getColor(getContext(),id);
+    }
+
+    public void setLabelTextSize(float textSize) {
+        this.labelTextSize = textSize;
+    }
+
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        initFrame(w,h);
+    }
+
+    private void initFrame(int width,int height){
+
+        int size = (int)(Math.min(width,height) * frameRatio);
+
+        if(frameWidth <= 0 || frameWidth > width){
+            frameWidth = size;
+        }
+
+        if(frameHeight <= 0 || frameHeight > height){
+            frameHeight = size;
+        }
+
+        if(labelTextWidth <= 0){
+            labelTextWidth = width - getPaddingLeft() - getPaddingRight();
+        }
+
+        float leftOffsets = (width - frameWidth) / 2 + framePaddingLeft - framePaddingRight;
+        float topOffsets = (height - frameHeight) / 2 + framePaddingTop - framePaddingBottom;
+        switch (frameGravity){
+            case LEFT:
+                leftOffsets = framePaddingLeft;
+                break;
+            case TOP:
+                topOffsets = framePaddingTop;
+                break;
+            case RIGHT:
+                leftOffsets = width - frameWidth + framePaddingRight;
+                break;
+            case BOTTOM:
+                topOffsets =  height - frameHeight + framePaddingBottom;
+                break;
+        }
+
+        frame = new Rect((int)leftOffsets, (int)topOffsets, (int)leftOffsets + frameWidth, (int)topOffsets + frameHeight);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+
+        if (frame == null) {
+            return;
+        }
+
+        if(scannerStart == 0 || scannerEnd == 0) {
+            scannerStart = frame.top;
+            scannerEnd = frame.bottom - scannerLineHeight;
+        }
+
+        int width = canvas.getWidth();
+        int height = canvas.getHeight();
+
+        // 绘制模糊区域
+        drawExterior(canvas,frame,width,height);
+        // 绘制扫描动画
+        drawLaserScanner(canvas,frame);
+        // 绘制取景区域框
+        drawFrame(canvas, frame);
+        // 绘制取景区域边角
+        drawCorner(canvas, frame);
+        //绘制提示信息
+        drawTextInfo(canvas, frame);
+        // 间隔更新取景区域
+        postInvalidateDelayed(scannerAnimationDelay, frame.left, frame.top, frame.right, frame.bottom);
+    }
+
+    /**
+     * 绘制文本
+     * @param canvas
+     * @param frame
+     */
+    private void drawTextInfo(Canvas canvas, Rect frame) {
+        if(!TextUtils.isEmpty(labelText)){
+            textPaint.setColor(labelTextColor);
+            textPaint.setTextSize(labelTextSize);
+            textPaint.setTextAlign(Paint.Align.CENTER);
+            StaticLayout staticLayout = new StaticLayout(labelText,textPaint,labelTextWidth, Layout.Alignment.ALIGN_NORMAL,1.2f,0.0f,true);
+            if(labelTextLocation == TextLocation.BOTTOM){
+                canvas.translate(frame.left + frame.width() / 2,frame.bottom + labelTextPadding);
+            }else{
+                canvas.translate(frame.left + frame.width() / 2,frame.top - labelTextPadding - staticLayout.getHeight());
+            }
+            staticLayout.draw(canvas);
+        }
+
+    }
+
+    /**
+     * 绘制边角
+     * @param canvas
+     * @param frame
+     */
+    private void drawCorner(Canvas canvas, Rect frame) {
+        paint.setColor(cornerColor);
+        //左上
+        canvas.drawRect(frame.left, frame.top, frame.left + cornerRectWidth, frame.top + cornerRectHeight, paint);
+        canvas.drawRect(frame.left, frame.top, frame.left + cornerRectHeight, frame.top + cornerRectWidth, paint);
+        //右上
+        canvas.drawRect(frame.right - cornerRectWidth, frame.top, frame.right, frame.top + cornerRectHeight, paint);
+        canvas.drawRect(frame.right - cornerRectHeight, frame.top, frame.right, frame.top + cornerRectWidth, paint);
+        //左下
+        canvas.drawRect(frame.left, frame.bottom - cornerRectWidth, frame.left + cornerRectHeight, frame.bottom, paint);
+        canvas.drawRect(frame.left, frame.bottom - cornerRectHeight, frame.left + cornerRectWidth, frame.bottom, paint);
+        //右下
+        canvas.drawRect(frame.right - cornerRectWidth, frame.bottom - cornerRectHeight, frame.right, frame.bottom, paint);
+        canvas.drawRect(frame.right - cornerRectHeight, frame.bottom - cornerRectWidth, frame.right, frame.bottom, paint);
+    }
+
+    /**
+     * 绘制激光扫描线
+     * @param canvas
+     * @param frame
+     */
+    private void drawLaserScanner(Canvas canvas, Rect frame) {
+        if(laserStyle != null){
+            paint.setColor(laserColor);
+            switch (laserStyle){
+                case LINE://线
+                    drawLineScanner(canvas,frame);
+                    break;
+                case GRID://网格
+                    drawGridScanner(canvas,frame);
+                    break;
+            }
+            paint.setShader(null);
+        }
+    }
+
+    /**
+     * 绘制线性式扫描
+     * @param canvas
+     * @param frame
+     */
+    private void drawLineScanner(Canvas canvas, Rect frame){
+        //线性渐变
+        LinearGradient linearGradient = new LinearGradient(
+                frame.left, scannerStart,
+                frame.left, scannerStart + scannerLineHeight,
+                shadeColor(laserColor),
+                laserColor,
+                Shader.TileMode.MIRROR);
+
+        paint.setShader(linearGradient);
+        if(scannerStart <= scannerEnd) {
+            //椭圆
+            RectF rectF = new RectF(frame.left + 2 * scannerLineHeight, scannerStart, frame.right - 2 * scannerLineHeight, scannerStart + scannerLineHeight);
+            canvas.drawOval(rectF, paint);
+            scannerStart += scannerLineMoveDistance;
+        } else {
+            scannerStart = frame.top;
+        }
+    }
+
+    /**
+     * 绘制网格式扫描
+     * @param canvas
+     * @param frame
+     */
+    private void drawGridScanner(Canvas canvas, Rect frame){
+        int stroke = 2;
+        paint.setStrokeWidth(stroke);
+        //计算Y轴开始位置
+        int startY = gridHeight > 0 && scannerStart - frame.top > gridHeight ? scannerStart - gridHeight : frame.top;
+
+        LinearGradient linearGradient = new LinearGradient(frame.left + frame.width()/2, startY, frame.left + frame.width()/2, scannerStart, new int[]{shadeColor(laserColor), laserColor}, new float[]{0,1f}, LinearGradient.TileMode.CLAMP);
+        //给画笔设置着色器
+        paint.setShader(linearGradient);
+
+        float wUnit = frame.width() * 1.0f/ gridColumn;
+        float hUnit = wUnit;
+        //遍历绘制网格纵线
+        for (int i = 1; i < gridColumn; i++) {
+            canvas.drawLine(frame.left + i * wUnit, startY,frame.left + i * wUnit, scannerStart,paint);
+        }
+
+        int height = gridHeight > 0 && scannerStart - frame.top > gridHeight ? gridHeight : scannerStart - frame.top;
+
+        //遍历绘制网格横线
+        for (int i = 0; i <= height/hUnit; i++) {
+            canvas.drawLine(frame.left, scannerStart - i * hUnit,frame.right, scannerStart - i * hUnit,paint);
+        }
+
+        if(scannerStart<scannerEnd){
+            scannerStart += scannerLineMoveDistance;
+        } else {
+            scannerStart = frame.top;
+        }
+
+    }
+
+    /**
+     * 处理颜色模糊
+     * @param color
+     * @return
+     */
+    public int shadeColor(int color) {
+        String hax = Integer.toHexString(color);
+        String result = "01"+hax.substring(2);
+        return Integer.valueOf(result, 16);
+    }
+
+    /**
+     * 绘制扫描区边框
+     * @param canvas
+     * @param frame
+     */
+    private void drawFrame(Canvas canvas, Rect frame) {
+        paint.setColor(frameColor);
+        canvas.drawRect(frame.left, frame.top, frame.right, frame.top + frameLineWidth, paint);
+        canvas.drawRect(frame.left, frame.top, frame.left + frameLineWidth, frame.bottom, paint);
+        canvas.drawRect(frame.right - frameLineWidth, frame.top, frame.right, frame.bottom, paint);
+        canvas.drawRect(frame.left, frame.bottom - frameLineWidth, frame.right, frame.bottom, paint);
+    }
+
+    /**
+     * 绘制模糊区域
+     * @param canvas
+     * @param frame
+     * @param width
+     * @param height
+     */
+    private void drawExterior(Canvas canvas, Rect frame, int width, int height) {
+        if(maskColor != 0){
+            paint.setColor(maskColor);
+            canvas.drawRect(0, 0, width, frame.top, paint);
+            canvas.drawRect(0, frame.top, frame.left, frame.bottom, paint);
+            canvas.drawRect(frame.right, frame.top, width, frame.bottom, paint);
+            canvas.drawRect(0, frame.bottom, width, height, paint);
+        }
+    }
+
+
+    public void drawViewfinder() {
+        invalidate();
+    }
+
+    public void setLaserStyle(LaserStyle laserStyle) {
+        this.laserStyle = laserStyle;
+    }
+
+
+
+
+}

+ 38 - 0
barcode/src/main/java/com/kathline/barcode/VisionImageProcessor.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.graphics.Bitmap;
+
+import com.google.mlkit.common.MlKitException;
+
+import java.nio.ByteBuffer;
+
+/** An interface to process the images with different vision detectors and custom image models. */
+public interface VisionImageProcessor {
+
+  /** Processes a bitmap image. */
+  void processBitmap(Bitmap bitmap, GraphicOverlay graphicOverlay);
+
+  /** Processes ByteBuffer image data, e.g. used for Camera1 live preview case. */
+  void processByteBuffer(
+          ByteBuffer data, FrameMetadata frameMetadata, GraphicOverlay graphicOverlay)
+      throws MlKitException;
+
+  /** Stops the underlying machine learning model and release resources. */
+  void stop();
+}

+ 270 - 0
barcode/src/main/java/com/kathline/barcode/VisionProcessorBase.java

@@ -0,0 +1,270 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.SystemClock;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.TaskExecutors;
+import com.google.mlkit.vision.common.InputImage;
+
+import java.nio.ByteBuffer;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/**
+ * Abstract base class for vision frame processors. Subclasses need to implement {@link
+ * #onSuccess(Object, GraphicOverlay, InputImage)} to define what they want to with the detection results and
+ * {@link #detectInImage(InputImage)} to specify the detector object.
+ *
+ * @param <T> The type of the detected feature.
+ */
+public abstract class VisionProcessorBase<T> implements VisionImageProcessor {
+
+    protected static final String MANUAL_TESTING_LOG = "LogTagForTest";
+    private static final String TAG = "VisionProcessorBase";
+
+    private final ActivityManager activityManager;
+    private final Timer fpsTimer = new Timer();
+    private final ScopedExecutor executor;
+
+    // Whether this processor is already shut down
+    private boolean isShutdown;
+
+    // Used to calculate latency, running in the same thread, no sync needed.
+    private int numRuns = 0;
+    private long totalFrameMs = 0;
+    private long maxFrameMs = 0;
+    private long minFrameMs = Long.MAX_VALUE;
+    private long totalDetectorMs = 0;
+    private long maxDetectorMs = 0;
+    private long minDetectorMs = Long.MAX_VALUE;
+
+    // Frame count that have been processed so far in an one second interval to calculate FPS.
+    private int frameProcessedInOneSecondInterval = 0;
+    private int framesPerSecond = 0;
+
+    private boolean isShowInfo;
+
+    // To keep the latest images and its metadata.
+    @GuardedBy("this")
+    private ByteBuffer latestImage;
+
+    @GuardedBy("this")
+    private FrameMetadata latestImageMetaData;
+    // To keep the images and metadata in process.
+    @GuardedBy("this")
+    private ByteBuffer processingImage;
+
+    @GuardedBy("this")
+    private FrameMetadata processingMetaData;
+
+    protected VisionProcessorBase(Context context) {
+        activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        executor = new ScopedExecutor(TaskExecutors.MAIN_THREAD);
+        fpsTimer.scheduleAtFixedRate(
+                new TimerTask() {
+                    @Override
+                    public void run() {
+                        framesPerSecond = frameProcessedInOneSecondInterval;
+                        frameProcessedInOneSecondInterval = 0;
+                    }
+                },
+                /* delay= */ 0,
+                /* period= */ 1000);
+    }
+
+    // -----------------Code for processing single still image----------------------------------------
+    @Override
+    public void processBitmap(Bitmap bitmap, final GraphicOverlay graphicOverlay) {
+        long frameStartMs = SystemClock.elapsedRealtime();
+        requestDetectInImage(
+                InputImage.fromBitmap(bitmap, 0),
+                graphicOverlay,
+                /* originalCameraImage= */ null,
+                /* shouldShowFps= */ false,
+                frameStartMs);
+    }
+
+    // -----------------Code for processing live preview frame from Camera1 API-----------------------
+    @Override
+    public synchronized void processByteBuffer(
+            ByteBuffer data, final FrameMetadata frameMetadata, final GraphicOverlay graphicOverlay) {
+        latestImage = data;
+        latestImageMetaData = frameMetadata;
+        if (processingImage == null && processingMetaData == null) {
+            processLatestImage(graphicOverlay);
+        }
+    }
+
+    private synchronized void processLatestImage(final GraphicOverlay graphicOverlay) {
+        processingImage = latestImage;
+        processingMetaData = latestImageMetaData;
+        latestImage = null;
+        latestImageMetaData = null;
+        if (processingImage != null && processingMetaData != null && !isShutdown) {
+            processImage(processingImage, processingMetaData, graphicOverlay);
+        }
+    }
+
+    private void processImage(
+            ByteBuffer data, final FrameMetadata frameMetadata, final GraphicOverlay graphicOverlay) {
+        long frameStartMs = SystemClock.elapsedRealtime();
+
+        // If live viewport is on (that is the underneath surface view takes care of the camera preview
+        // drawing), skip the unnecessary bitmap creation that used for the manual preview drawing.
+        Bitmap bitmap =
+                PreferenceUtils.isCameraLiveViewportEnabled(graphicOverlay.getContext())
+                        ? null
+                        : BitmapUtils.getBitmap(data, frameMetadata);
+
+        requestDetectInImage(
+                InputImage.fromByteBuffer(
+                        data,
+                        frameMetadata.getWidth(),
+                        frameMetadata.getHeight(),
+                        frameMetadata.getRotation(),
+                        InputImage.IMAGE_FORMAT_NV21),
+                graphicOverlay,
+                bitmap,
+                /* shouldShowFps= */ true,
+                frameStartMs)
+                .addOnSuccessListener(executor, results -> processLatestImage(graphicOverlay));
+    }
+
+    // -----------------Common processing logic-------------------------------------------------------
+    private Task<T> requestDetectInImage(
+            final InputImage image,
+            final GraphicOverlay graphicOverlay,
+            @Nullable final Bitmap originalCameraImage,
+            boolean shouldShowFps,
+            long frameStartMs) {
+        final long detectorStartMs = SystemClock.elapsedRealtime();
+        return detectInImage(image)
+                .addOnSuccessListener(
+                        executor,
+                        results -> {
+                            long endMs = SystemClock.elapsedRealtime();
+                            long currentFrameLatencyMs = endMs - frameStartMs;
+                            long currentDetectorLatencyMs = endMs - detectorStartMs;
+                            if (numRuns >= 500) {
+                                resetLatencyStats();
+                            }
+                            numRuns++;
+                            frameProcessedInOneSecondInterval++;
+                            totalFrameMs += currentFrameLatencyMs;
+                            maxFrameMs = max(currentFrameLatencyMs, maxFrameMs);
+                            minFrameMs = min(currentFrameLatencyMs, minFrameMs);
+                            totalDetectorMs += currentDetectorLatencyMs;
+                            maxDetectorMs = max(currentDetectorLatencyMs, maxDetectorMs);
+                            minDetectorMs = min(currentDetectorLatencyMs, minDetectorMs);
+
+                            // Only log inference info once per second. When frameProcessedInOneSecondInterval is
+                            // equal to 1, it means this is the first frame processed during the current second.
+                            if (frameProcessedInOneSecondInterval == 1) {
+                                Log.d(TAG, "Num of Runs: " + numRuns);
+                                Log.d(
+                                        TAG,
+                                        "Frame latency: max="
+                                                + maxFrameMs
+                                                + ", min="
+                                                + minFrameMs
+                                                + ", avg="
+                                                + totalFrameMs / numRuns);
+                                Log.d(
+                                        TAG,
+                                        "Detector latency: max="
+                                                + maxDetectorMs
+                                                + ", min="
+                                                + minDetectorMs
+                                                + ", avg="
+                                                + totalDetectorMs / numRuns);
+                                MemoryInfo mi = new MemoryInfo();
+                                activityManager.getMemoryInfo(mi);
+                                long availableMegs = mi.availMem / 0x100000L;
+                                Log.d(TAG, "Memory available in system: " + availableMegs + " MB");
+                            }
+
+                            graphicOverlay.clear();
+                            if (originalCameraImage != null) {
+                                graphicOverlay.add(new CameraImageGraphic(graphicOverlay, originalCameraImage));
+                            }
+                            if (isShowInfo) {
+                                graphicOverlay.add(
+                                        new InferenceInfoGraphic(
+                                                graphicOverlay,
+                                                currentFrameLatencyMs,
+                                                currentDetectorLatencyMs,
+                                                shouldShowFps ? framesPerSecond : null));
+                            }
+                            VisionProcessorBase.this.onSuccess(results, graphicOverlay, image);
+                            graphicOverlay.postInvalidate();
+                        })
+                .addOnFailureListener(
+                        executor,
+                        e -> {
+                            graphicOverlay.clear();
+                            graphicOverlay.postInvalidate();
+                            String error = "Failed to process. Error: " + e.getLocalizedMessage();
+                            Toast.makeText(
+                                    graphicOverlay.getContext(),
+                                    error + "\nCause: " + e.getCause(),
+                                    Toast.LENGTH_SHORT)
+                                    .show();
+                            Log.d(TAG, error);
+                            e.printStackTrace();
+                            VisionProcessorBase.this.onFailure(e);
+                        });
+    }
+
+    @Override
+    public void stop() {
+        executor.shutdown();
+        isShutdown = true;
+        resetLatencyStats();
+        fpsTimer.cancel();
+    }
+
+    private void resetLatencyStats() {
+        numRuns = 0;
+        totalFrameMs = 0;
+        maxFrameMs = 0;
+        minFrameMs = Long.MAX_VALUE;
+        totalDetectorMs = 0;
+        maxDetectorMs = 0;
+        minDetectorMs = Long.MAX_VALUE;
+    }
+
+    protected abstract Task<T> detectInImage(InputImage image);
+
+    protected abstract void onSuccess(@NonNull T results, @NonNull GraphicOverlay graphicOverlay, InputImage image);
+
+    protected abstract void onFailure(@NonNull Exception e);
+}

+ 98 - 0
barcode/src/main/java/com/kathline/barcode/barcodescanner/BarcodeGraphic.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode.barcodescanner;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.google.mlkit.vision.barcode.common.Barcode;
+import com.kathline.barcode.GraphicOverlay;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/** Graphic instance for rendering Barcode position and content information in an overlay view. */
+public class BarcodeGraphic extends GraphicOverlay.Graphic {
+
+  private static final int TEXT_COLOR = Color.BLACK;
+  private static final int MARKER_COLOR = Color.WHITE;
+  private static final float TEXT_SIZE = 54.0f;
+  private static final float STROKE_WIDTH = 4.0f;
+
+  private final Paint rectPaint;
+  private final Paint barcodePaint;
+  private final Barcode barcode;
+  private final Paint labelPaint;
+
+  public BarcodeGraphic(GraphicOverlay overlay, Barcode barcode) {
+    super(overlay);
+
+    this.barcode = barcode;
+
+    rectPaint = new Paint();
+    rectPaint.setColor(MARKER_COLOR);
+    rectPaint.setStyle(Paint.Style.STROKE);
+    rectPaint.setStrokeWidth(STROKE_WIDTH);
+
+    barcodePaint = new Paint();
+    barcodePaint.setColor(TEXT_COLOR);
+    barcodePaint.setTextSize(TEXT_SIZE);
+
+    labelPaint = new Paint();
+    labelPaint.setColor(MARKER_COLOR);
+    labelPaint.setStyle(Paint.Style.FILL);
+  }
+
+  /**
+   * Draws the barcode block annotations for position, size, and raw value on the supplied canvas.
+   */
+  @Override
+  public void draw(Canvas canvas) {
+    if (barcode == null) {
+      throw new IllegalStateException("Attempting to draw a null barcode.");
+    }
+
+    // Draws the bounding box around the BarcodeBlock.
+    RectF rect = new RectF(barcode.getBoundingBox());
+    // If the image is flipped, the left will be translated to right, and the right to left.
+    float x0 = translateX(rect.left);
+    float x1 = translateX(rect.right);
+    rect.left = min(x0, x1);
+    rect.right = max(x0, x1);
+    rect.top = translateY(rect.top);
+    rect.bottom = translateY(rect.bottom);
+    canvas.drawRect(rect, rectPaint);
+
+    // Draws other object info.
+    float lineHeight = TEXT_SIZE + (2 * STROKE_WIDTH);
+    Rect boundingBox = barcode.getBoundingBox();
+    String rawValue = String.format("(%d,%d)", boundingBox.left, boundingBox.top);//barcode.getRawValue();
+    float textWidth = barcodePaint.measureText(rawValue);
+    canvas.drawRect(
+        rect.left - STROKE_WIDTH,
+        rect.top - lineHeight,
+        rect.left + textWidth + (2 * STROKE_WIDTH),
+        rect.top,
+        labelPaint);
+    // Renders the barcode at the bottom of the box.
+//    canvas.drawText(barcode.getRawValue(), rect.left, rect.top - STROKE_WIDTH, barcodePaint);
+    canvas.drawText(rawValue, rect.left, rect.top - STROKE_WIDTH, barcodePaint);
+  }
+}

+ 143 - 0
barcode/src/main/java/com/kathline/barcode/barcodescanner/BarcodeScannerProcessor.java

@@ -0,0 +1,143 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode.barcodescanner;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.gms.tasks.Task;
+import com.google.mlkit.vision.barcode.common.Barcode;
+import com.google.mlkit.vision.barcode.BarcodeScanner;
+import com.google.mlkit.vision.barcode.BarcodeScanning;
+import com.google.mlkit.vision.common.InputImage;
+import com.kathline.barcode.GraphicOverlay;
+import com.kathline.barcode.MLKit;
+import com.kathline.barcode.VisionProcessorBase;
+
+import java.util.List;
+
+/** Barcode Detector Demo. */
+public class BarcodeScannerProcessor extends VisionProcessorBase<List<Barcode>> {
+
+  private static final String TAG = "BarcodeProcessor";
+
+  private final BarcodeScanner barcodeScanner;
+  private MLKit mlKit;
+
+  public BarcodeScannerProcessor(Context context) {
+    super(context);
+    // Note that if you know which format of barcode your app is dealing with, detection will be
+    // faster to specify the supported barcode formats one by one, e.g.
+    // new BarcodeScannerOptions.Builder()
+    //     .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
+    //     .build();
+    barcodeScanner = BarcodeScanning.getClient();
+  }
+
+  public BarcodeScannerProcessor(Context context, MLKit mlKit) {
+    super(context);
+    if(mlKit.options != null) {
+      barcodeScanner = BarcodeScanning.getClient(mlKit.options);
+    }else {
+      barcodeScanner = BarcodeScanning.getClient();
+    }
+    this.mlKit = mlKit;
+  }
+
+  @Override
+  public void stop() {
+    super.stop();
+    barcodeScanner.close();
+  }
+
+  @Override
+  protected Task<List<Barcode>> detectInImage(InputImage image) {
+    return barcodeScanner.process(image);
+  }
+
+  @Override
+  protected void onSuccess(
+      @NonNull List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
+    if (barcodes.isEmpty()) {
+      Log.v(MANUAL_TESTING_LOG, "No barcode has been detected");
+    }
+    if(mlKit == null) {
+      for (int i = 0; i < barcodes.size(); ++i) {
+        Barcode barcode = barcodes.get(i);
+        graphicOverlay.add(new BarcodeGraphic(graphicOverlay, barcode));
+        logExtrasForTesting(barcode);
+      }
+    }else if(mlKit.isAnalyze()) {
+      if(mlKit.onScanListener != null) {
+        if(!barcodes.isEmpty()) {
+          mlKit.playBeepAndVibrate();
+        }
+        mlKit.onScanListener.onSuccess(barcodes, graphicOverlay, image);
+      }
+    }
+  }
+
+  private static void logExtrasForTesting(Barcode barcode) {
+    if (barcode != null) {
+      Log.v(
+          MANUAL_TESTING_LOG,
+          String.format(
+              "Detected barcode's bounding box: %s", barcode.getBoundingBox().flattenToString()));
+      Log.v(
+          MANUAL_TESTING_LOG,
+          String.format(
+              "Expected corner point size is 4, get %d", barcode.getCornerPoints().length));
+      for (Point point : barcode.getCornerPoints()) {
+        Log.v(
+            MANUAL_TESTING_LOG,
+            String.format("Corner point is located at: x = %d, y = %d", point.x, point.y));
+      }
+      Log.v(MANUAL_TESTING_LOG, "barcode display value: " + barcode.getDisplayValue());
+      Log.v(MANUAL_TESTING_LOG, "barcode raw value: " + barcode.getRawValue());
+      Barcode.DriverLicense dl = barcode.getDriverLicense();
+      if (dl != null) {
+        Log.v(MANUAL_TESTING_LOG, "driver license city: " + dl.getAddressCity());
+        Log.v(MANUAL_TESTING_LOG, "driver license state: " + dl.getAddressState());
+        Log.v(MANUAL_TESTING_LOG, "driver license street: " + dl.getAddressStreet());
+        Log.v(MANUAL_TESTING_LOG, "driver license zip code: " + dl.getAddressZip());
+        Log.v(MANUAL_TESTING_LOG, "driver license birthday: " + dl.getBirthDate());
+        Log.v(MANUAL_TESTING_LOG, "driver license document type: " + dl.getDocumentType());
+        Log.v(MANUAL_TESTING_LOG, "driver license expiry date: " + dl.getExpiryDate());
+        Log.v(MANUAL_TESTING_LOG, "driver license first name: " + dl.getFirstName());
+        Log.v(MANUAL_TESTING_LOG, "driver license middle name: " + dl.getMiddleName());
+        Log.v(MANUAL_TESTING_LOG, "driver license last name: " + dl.getLastName());
+        Log.v(MANUAL_TESTING_LOG, "driver license gender: " + dl.getGender());
+        Log.v(MANUAL_TESTING_LOG, "driver license issue date: " + dl.getIssueDate());
+        Log.v(MANUAL_TESTING_LOG, "driver license issue country: " + dl.getIssuingCountry());
+        Log.v(MANUAL_TESTING_LOG, "driver license number: " + dl.getLicenseNumber());
+      }
+    }
+  }
+
+  @Override
+  protected void onFailure(@NonNull Exception e) {
+    Log.e(TAG, "Barcode detection failed " + e);
+    if(mlKit != null) {
+      if(mlKit.onScanListener != null) {
+        mlKit.onScanListener.onFail(1, e);
+      }
+    }
+  }
+}

+ 235 - 0
barcode/src/main/java/com/kathline/barcode/barcodescanner/WxGraphic.java

@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * 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.
+ */
+
+package com.kathline.barcode.barcodescanner;
+
+import android.animation.ValueAnimator;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.MotionEvent;
+
+import androidx.annotation.ColorInt;
+
+import com.google.mlkit.vision.barcode.common.Barcode;
+import com.kathline.barcode.GraphicOverlay;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/**
+ * Graphic instance for rendering Barcode position and content information in an overlay view.
+ * 仿微信扫码
+ */
+public class WxGraphic extends GraphicOverlay.Graphic {
+
+    private static final int TEXT_COLOR = Color.BLACK;
+    private static final int MARKER_COLOR = Color.WHITE;
+    private static final float TEXT_SIZE = 54.0f;
+    private static final float STROKE_WIDTH = 4.0f;
+
+    private final Paint rectPaint;
+    private final Paint barcodePaint;
+    private final Barcode barcode;
+    private final Paint labelPaint;
+    private Paint paint;
+    private float radius = 40f;
+    private @ColorInt
+    int color = Color.parseColor("#6200EE");
+    Region circleRegion;
+    Path circlePath;
+    private Bitmap bitmap;
+    private boolean isHideColor;
+
+    public WxGraphic(GraphicOverlay overlay, Barcode barcode) {
+        super(overlay);
+
+        this.barcode = barcode;
+
+        rectPaint = new Paint();
+        rectPaint.setColor(MARKER_COLOR);
+        rectPaint.setStyle(Paint.Style.STROKE);
+        rectPaint.setStrokeWidth(STROKE_WIDTH);
+
+        barcodePaint = new Paint();
+        barcodePaint.setColor(TEXT_COLOR);
+        barcodePaint.setTextSize(TEXT_SIZE);
+
+        labelPaint = new Paint();
+        labelPaint.setColor(MARKER_COLOR);
+        labelPaint.setStyle(Paint.Style.FILL);
+
+        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(color);
+        paint.setStrokeWidth(6);
+
+        circlePath = new Path();
+        circleRegion = new Region();
+    }
+
+    public void setRadius(float radius) {
+        this.radius = radius;
+        postInvalidate();
+    }
+
+    public void setColor(@ColorInt int color) {
+        this.color = color;
+        paint.setColor(color);
+        postInvalidate();
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        setBitmap(bitmap, true);
+    }
+
+    /**
+     * 当设置图片后,是否显示后面的圆
+     * @param bitmap
+     * @param isHideColor
+     */
+    public void setBitmap(Bitmap bitmap, boolean isHideColor) {
+        this.bitmap = bitmap;
+        this.isHideColor = isHideColor;
+        postInvalidate();
+    }
+
+    /**
+     * Draws the barcode block annotations for position, size, and raw value on the supplied canvas.
+     */
+    @Override
+    public void draw(Canvas canvas) {
+        if (barcode == null) {
+            throw new IllegalStateException("Attempting to draw a null barcode.");
+        }
+
+        // Draws the bounding box around the BarcodeBlock.
+        RectF rect = new RectF(barcode.getBoundingBox());
+        // If the image is flipped, the left will be translated to right, and the right to left.
+        float x0 = translateX(rect.left);
+        float x1 = translateX(rect.right);
+        rect.left = min(x0, x1);
+        rect.right = max(x0, x1);
+        rect.top = translateY(rect.top);
+        rect.bottom = translateY(rect.bottom);
+
+        circlePath.reset();
+        // ▼在屏幕中间添加一个圆
+        circlePath.addCircle(rect.left + (rect.right - rect.left) / 2f, rect.top + (rect.bottom - rect.top) / 2f, radius * mProgress, Path.Direction.CW);
+        // ▼将剪裁边界设置为视图大小
+        Region globalRegion = new Region((int) rect.left, (int) rect.top, (int) rect.right, (int) rect.bottom);
+        // ▼将 Path 添加到 Region 中
+        circleRegion.setPath(circlePath, globalRegion);
+        Path circle = circlePath;
+        if (bitmap != null) {
+            if(isHideColor) {
+                paint.setAlpha(0);
+            }
+            // 绘制圆
+            canvas.drawPath(circle, paint);
+            float x = rect.left + (rect.right - rect.left) / 2f;
+            float y = rect.top + (rect.bottom - rect.top) / 2f;
+            int width = bitmap.getWidth();
+            int height = bitmap.getHeight();
+            float scaleX = (radius) * mProgress / (width / 2f);
+            float scaleY = (radius) * mProgress / (height / 2f);
+            Matrix mMatrix = new Matrix();
+            mMatrix.postScale(scaleX, scaleY, (width / 2f), (height / 2f));
+            mMatrix.postTranslate(x - width / 2f, y - height / 2f);
+            canvas.drawBitmap(bitmap, mMatrix, null);
+        } else {
+            // 绘制圆
+            canvas.drawPath(circle, paint);
+        }
+        if (valueAnimator == null && !isDestroy) {
+            startAnim();
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                int x = (int) event.getX();
+                int y = (int) event.getY();
+
+                // ▼点击区域判断
+                if (circleRegion.contains(x, y)) {
+                    if (onClickListener != null) {
+                        onClickListener.onClick(barcode);
+                    }
+                }
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    public void OnDestroy() {
+        super.OnDestroy();
+        isDestroy = true;
+        handler.removeCallbacks(resetRunnable);
+        valueAnimator.cancel();
+        valueAnimator = null;
+    }
+
+    public interface OnClickListener {
+        void onClick(Barcode barcode);
+    }
+
+    private OnClickListener onClickListener;
+
+    public void setOnClickListener(OnClickListener listener) {
+        onClickListener = listener;
+    }
+
+    private float mProgress = 1;
+    private ValueAnimator valueAnimator;
+    private Handler handler = new Handler(Looper.getMainLooper());
+    private Runnable resetRunnable;
+    private boolean isDestroy;
+
+    private void startAnim() {
+        valueAnimator = ValueAnimator.ofFloat(1, 0.8f, 1, 0.8f, 1);
+        valueAnimator.setDuration(2000);
+        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mProgress = (float) animation.getAnimatedValue();
+                if (animation.getCurrentPlayTime() >= animation.getDuration()) {
+                    if (handler != null && !isDestroy) {
+                        handler.postDelayed(resetRunnable = new Runnable() {
+                            @Override
+                            public void run() {
+                                startAnim();
+                            }
+                        }, 1000);
+                    }
+                }
+                postInvalidate();
+            }
+        });
+        valueAnimator.start();
+    }
+
+}

+ 123 - 0
barcode/src/main/java/com/kathline/barcode/hardware/BeepManager.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * 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.
+ */
+
+package com.kathline.barcode.hardware;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Vibrator;
+import android.util.Log;
+
+import com.kathline.barcode.R;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Manages beeps and vibrations
+ */
+public final class BeepManager implements MediaPlayer.OnErrorListener, Closeable {
+
+  private static final String TAG = BeepManager.class.getSimpleName();
+
+  private static final float BEEP_VOLUME = 0.10f;
+  private static final long VIBRATE_DURATION = 200L;
+
+  private final Activity activity;
+  private MediaPlayer mediaPlayer;
+  private boolean playBeep;
+  private boolean vibrate;
+
+  public BeepManager(Activity activity) {
+    this.activity = activity;
+    this.mediaPlayer = null;
+    updatePrefs();
+  }
+
+  public synchronized void updatePrefs() {
+    if (mediaPlayer == null) {
+      // The volume on STREAM_SYSTEM is not adjustable, and users found it too loud,
+      // so we now play on the music stream.
+      activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
+      mediaPlayer = buildMediaPlayer(activity);
+    }
+  }
+
+  public synchronized void playBeepSoundAndVibrate() {
+    if (playBeep && mediaPlayer != null) {
+      mediaPlayer.start();
+    }
+    if (vibrate) {
+      Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
+      vibrator.vibrate(VIBRATE_DURATION);
+    }
+  }
+
+  public synchronized void playBeepSoundAndVibrate(boolean playBeep, boolean vibrate) {
+    this.playBeep = playBeep;
+    this.vibrate = vibrate;
+    if (playBeep && mediaPlayer != null) {
+      mediaPlayer.start();
+    }
+    if (vibrate) {
+      Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
+      vibrator.vibrate(VIBRATE_DURATION);
+    }
+  }
+
+  private MediaPlayer buildMediaPlayer(Context activity) {
+    MediaPlayer mediaPlayer = new MediaPlayer();
+    try {
+      AssetFileDescriptor file = activity.getResources().openRawResourceFd(R.raw.beep);
+      mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
+      mediaPlayer.setOnErrorListener(this);
+      mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+      mediaPlayer.setLooping(false);
+      mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
+      mediaPlayer.prepare();
+      return mediaPlayer;
+    } catch (IOException ioe) {
+      Log.w(TAG, ioe);
+      mediaPlayer.release();
+      return null;
+    }
+  }
+
+  @Override
+  public synchronized boolean onError(MediaPlayer mp, int what, int extra) {
+    if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
+      // we are finished, so put up an appropriate error toast if required and finish
+      activity.finish();
+    } else {
+      // possibly media player error, so release and recreate
+      close();
+      updatePrefs();
+    }
+    return true;
+  }
+
+  @Override
+  public synchronized void close() {
+    if (mediaPlayer != null) {
+      mediaPlayer.release();
+      mediaPlayer = null;
+    }
+  }
+
+}

+ 35 - 0
barcode/src/main/java/com/kathline/barcode/hardware/FrontLightMode.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 ZXing authors
+ *
+ * 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.
+ */
+
+package com.kathline.barcode.hardware;
+
+/**
+ * Enumerates settings of the preference controlling the front light.
+ */
+public enum FrontLightMode {
+
+  /** Always on. */
+  ON,
+  /** On only when ambient light is low. */
+  AUTO,
+  /** Always off. */
+  OFF;
+
+  private static FrontLightMode parse(String modeString) {
+    return modeString == null ? OFF : valueOf(modeString);
+  }
+
+}

+ 9 - 0
barcode/src/main/res/drawable/ic_switch_camera_white_48dp.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This is an example InsetDrawable. It should be manually reviewed. -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@mipmap/ic_switch_camera_white_48dp_inset"
+    android:insetTop="3.5dp"
+    android:insetLeft="3.5dp"
+    android:insetBottom="7.5dp"
+    android:insetRight="3.5dp"
+    android:visible="true" />

+ 9 - 0
barcode/src/main/res/drawable/toggle_style.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+      android:drawable="@drawable/ic_switch_camera_white_48dp"
+      android:state_checked="true"/>
+  <item
+      android:drawable="@drawable/ic_switch_camera_white_48dp"
+      android:state_checked="false"/>
+</selector>

+ 8 - 0
barcode/src/main/res/layout/settings_style.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:padding="12dp"
+    android:contentDescription="@string/menu_item_settings"
+    android:src="@mipmap/ic_settings_white_24dp"/>

BIN
barcode/src/main/res/mipmap-xhdpi/ic_settings_white_24dp.png


BIN
barcode/src/main/res/mipmap-xhdpi/ic_switch_camera_white_48dp_inset.png


BIN
barcode/src/main/res/mipmap-xhdpi/ico_wechat.png


BIN
barcode/src/main/res/raw/beep.ogg


+ 45 - 0
barcode/src/main/res/values/attrs.xml

@@ -0,0 +1,45 @@
+<resources>
+    <declare-styleable name="ViewfinderView">
+        <attr name="maskColor" format="color" />
+        <attr name="frameColor" format="color" />
+        <attr name="cornerColor" format="color"/>
+        <attr name="laserColor" format="color"/>
+        <attr name="labelText" format="string"/>
+        <attr name="labelTextColor" format="color"/>
+        <attr name="labelTextSize" format="dimension"/>
+        <attr name="labelTextPadding" format="dimension"/>
+        <attr name="labelTextWidth" format="dimension"/>
+        <attr name="labelTextLocation" format="enum">
+            <enum name="top" value="0"/>
+            <enum name="bottom" value="1"/>
+        </attr>
+        <attr name="frameWidth" format="dimension"/>
+        <attr name="frameHeight" format="dimension"/>
+        <attr name="gridColumn" format="integer"/>
+        <attr name="gridHeight" format="dimension"/>
+        <attr name="laserStyle" format="enum">
+            <enum name="none" value="0"/>
+            <enum name="line" value="1"/>
+            <enum name="grid" value="2"/>
+        </attr>
+        <attr name="cornerRectWidth" format="dimension"/>
+        <attr name="cornerRectHeight" format="dimension"/>
+        <attr name="scannerLineMoveDistance" format="dimension"/>
+        <attr name="scannerLineHeight" format="dimension"/>
+        <attr name="frameLineWidth" format="dimension"/>
+        <attr name="scannerAnimationDelay" format="integer"/>
+        <attr name="frameRatio" format="float"/>
+        <attr name="framePaddingLeft" format="dimension"/>
+        <attr name="framePaddingTop" format="dimension"/>
+        <attr name="framePaddingRight" format="dimension"/>
+        <attr name="framePaddingBottom" format="dimension"/>
+        <attr name="frameGravity" format="enum">
+            <enum name="center" value="0"/>
+            <enum name="left" value="1"/>
+            <enum name="top" value="2"/>
+            <enum name="right" value="3"/>
+            <enum name="bottom" value="4"/>
+        </attr>
+    </declare-styleable>
+
+</resources>

+ 10 - 0
barcode/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <color name="viewfinder_mask">#60000000</color>
+    <color name="viewfinder_frame">#7F1FB3E2</color>
+    <color name="viewfinder_corner">#FF1FB3E2</color>
+    <color name="viewfinder_laser">#FF1FB3E2</color>
+    <color name="viewfinder_text_color">#FFC0C0C0</color>
+
+</resources>

+ 131 - 0
barcode/src/main/res/values/strings.xml

@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">MLKit-Vision</string>
+    <string name="java_entry_title">Run the ML Kit quickstart written in Java</string>
+    <string name="kotlin_entry_title">Run the ML Kit quickstart written in Kotlin</string>
+    <string name="ok">OK</string>
+    <string name="permission_camera_rationale">Access to the camera is needed for detection</string>
+    <string name="no_camera_permission">This application cannot run because it does not have the camera permission.  The application will now exit.</string>
+    <string name="low_storage_error">Face detector dependencies cannot be downloaded due to low device storage</string>
+    <string name="toggle_turn_on">Front</string>
+    <string name="toggle_turn_off">Back</string>
+    <string name="desc_camera_source_activity">Vision detectors demo with live camera preview</string>
+    <string name="desc_still_image_activity">Vision detectors demo with a still image</string>
+    <string name="desc_camerax_live_preview_activity">Vision detectors demo with live preview using CameraX. Note that CameraX is only supported on API 21+</string>
+    <string name="download_error">Download error</string>
+    <string name="start_over">Start over</string>
+    <string name="menu_item_settings">Settings</string>
+    <string name="select_image">Select image</string>
+
+    <!-- Settings related strings. -->
+    <string name="pref_screen_title_live_preview">Live preview settings</string>
+    <string name="pref_screen_title_still_image">Still image settings</string>
+    <string name="pref_screen_title_camerax_live_preview">CameraX live preview settings</string>
+    <string name="pref_category_face_detection">Face Detection</string>
+    <string name="pref_category_object_detection">Object Detection / Custom Object Detection</string>
+    <string name="pref_category_automl">AutoML Image Labeling</string>
+    <string name="pref_category_pose_detection">Pose Detection</string>
+    <string name="pref_category_segmentation">Selfie Segmentation</string>
+
+    <!-- Strings for camera settings. -->
+    <string name="pref_category_key_camera" translatable="false">pckc</string>
+    <string name="pref_category_title_camera">Camera</string>
+    <string name="pref_key_rear_camera_preview_size" translatable="false">rcpvs</string>
+    <string name="pref_key_rear_camera_picture_size" translatable="false">rcpts</string>
+    <string name="pref_key_front_camera_preview_size" translatable="false">fcpvs</string>
+    <string name="pref_key_front_camera_picture_size" translatable="false">fcpts</string>
+    <string name="pref_key_camerax_rear_camera_target_resolution" translatable="false">crctas</string>
+    <string name="pref_key_camerax_front_camera_target_resolution" translatable="false">cfctas</string>
+    <string name="pref_key_camera_live_viewport" translatable="false">clv</string>
+    <string name="pref_title_rear_camera_preview_size">Rear camera preview size</string>
+    <string name="pref_title_front_camera_preview_size">Front camera preview size</string>
+    <string name="pref_title_camerax_rear_camera_target_resolution">CameraX rear camera target resolution</string>
+    <string name="pref_title_camerax_front_camera_target_resolution">CameraX front camera target resolution</string>
+    <string name="pref_title_camera_live_viewport">Enable live viewport</string>
+    <string name="pref_summary_camera_live_viewport">Do not block camera preview drawing on detection</string>
+
+    <!-- Strings for object detector enable multiple objects preference. -->
+    <string name="pref_title_object_detector_enable_multiple_objects">Enable multiple objects</string>
+    <string name="pref_key_live_preview_object_detector_enable_multiple_objects" translatable="false">lpodemo</string>
+    <string name="pref_key_still_image_object_detector_enable_multiple_objects" translatable="false">siodemo</string>
+
+    <!-- Strings for object detector enable classification preference. -->
+    <string name="pref_title_object_detector_enable_classification">Enable classification</string>
+    <string name="pref_key_live_preview_object_detector_enable_classification" translatable="false">lpodec</string>
+    <string name="pref_key_still_image_object_detector_enable_classification" translatable="false">siodec</string>
+
+    <!-- Strings for face detector landmark mode preference. -->
+    <string name="pref_title_face_detector_landmark_mode">Landmark mode</string>
+    <string name="pref_key_live_preview_face_detection_landmark_mode" translatable="false">lpfdlm</string>
+    <string name="pref_entries_face_detector_landmark_mode_no_landmarks">No landmarks</string>
+    <string name="pref_entries_face_detector_landmark_mode_all_landmarks">All landmarks</string>
+    <!-- The following entry values must match the ones in FaceDetectorOptions#LandmarkMode -->
+    <string name="pref_entry_values_face_detector_landmark_mode_no_landmarks" translatable="false">1</string>
+    <string name="pref_entry_values_face_detector_landmark_mode_all_landmarks" translatable="false">2</string>
+
+    <!-- Strings for face detector contour mode preference. -->
+    <string name="pref_title_face_detector_contour_mode">Contour mode</string>
+    <string name="pref_key_live_preview_face_detection_contour_mode" translatable="false">lpfdcm</string>
+    <string name="pref_entries_face_detector_contour_mode_no_contours">No contours</string>
+    <string name="pref_entries_face_detector_contour_mode_all_contours">All contours</string>
+    <!-- The following entry values must match the ones in FaceDetectorOptions#ContourMode -->
+    <string name="pref_entry_values_face_detector_contour_mode_no_contours" translatable="false">1</string>
+    <string name="pref_entry_values_face_detector_contour_mode_all_contours" translatable="false">2</string>
+
+    <!-- Strings for face detector classification mode preference. -->
+    <string name="pref_title_face_detector_classification_mode">Classification mode</string>
+    <string name="pref_key_live_preview_face_detection_classification_mode" translatable="false">lpfdcfm</string>
+    <string name="pref_entries_face_detector_classification_mode_no_classifications">No classifications</string>
+    <string name="pref_entries_face_detector_classification_mode_all_classifications">All classifications</string>
+    <!-- The following entry values must match the ones in FaceDetectorOptions#ClassificationMode -->
+    <string name="pref_entry_values_face_detector_classification_mode_no_classifications" translatable="false">1</string>
+    <string name="pref_entry_values_face_detector_classification_mode_all_classifications" translatable="false">2</string>
+
+    <!-- Strings for face detector performance mode preference. -->
+    <string name="pref_title_face_detector_performance_mode">Performance mode</string>
+    <string name="pref_key_live_preview_face_detection_performance_mode" translatable="false">lpfdpm</string>
+    <string name="pref_entries_face_detector_performance_mode_fast">Fast</string>
+    <string name="pref_entries_face_detector_performance_mode_accurate">Accurate</string>
+    <!-- The following entry values must match the ones in FaceDetectorOptions#PerformanceMode -->
+    <string name="pref_entry_values_face_detector_performance_mode_fast" translatable="false">1</string>
+    <string name="pref_entry_values_face_detector_performance_mode_accurate" translatable="false">2</string>
+
+    <!-- Strings for face detector face tracking preference. -->
+    <string name="pref_title_face_detector_face_tracking">Face tracking</string>
+    <string name="pref_key_live_preview_face_detection_face_tracking" translatable="false">lpfdft</string>
+
+    <!-- Strings for face detector min face size preference. -->
+    <string name="pref_title_face_detector_min_face_size">Minimum face size</string>
+    <string name="pref_key_live_preview_face_detection_min_face_size" translatable="false">lpfdmfs</string>
+    <string name="pref_dialog_message_face_detector_min_face_size">Proportion of the head width to the image width, and the valid value range is [0.0, 1.0]</string>
+    <string name="pref_toast_invalid_min_face_size">Minimum face size must be a float value and in the range [0.0, 1.0]</string>
+
+    <!-- Strings for pose detector performance mode preference. -->
+    <string name="pref_title_pose_detector_performance_mode">Performance mode</string>
+    <string name="pref_key_live_preview_pose_detection_performance_mode" translatable="false">lppdpm</string>
+    <string name="pref_key_still_image_pose_detection_performance_mode" translatable="false">sipdpm</string>
+    <string name="pref_entries_pose_detector_performance_mode_fast">Fast</string>
+    <string name="pref_entries_pose_detector_performance_mode_accurate">Accurate</string>
+    <string name="pref_entry_values_pose_detector_performance_mode_fast" translatable="false">1</string>
+    <string name="pref_entry_values_pose_detector_performance_mode_accurate" translatable="false">2</string>
+
+    <!-- Strings for pose detector showInFrameLikelihood preference. -->
+    <string name="pref_title_pose_detector_show_in_frame_likelihood">Show in-frame likelihood</string>
+    <string name="pref_key_live_preview_pose_detector_show_in_frame_likelihood" translatable="false">lppdsifl</string>
+    <string name="pref_key_still_image_pose_detector_show_in_frame_likelihood" translatable="false">sipdsifl</string>
+
+    <!-- Strings for pose detector z value visualization preference. -->
+    <string name="pref_title_pose_detector_visualize_z">Visualize z value</string>
+    <string name="pref_key_pose_detector_visualize_z" translatable="false">pdvz</string>
+    <string name="pref_title_pose_detector_rescale_z">Rescale z value for visualization</string>
+    <string name="pref_key_pose_detector_rescale_z" translatable="false">pdrz</string>
+
+    <!-- Strings for pose classification preference. -->
+    <string name="pref_title_pose_detector_run_classification">Run Classification</string>
+    <string name="pref_key_pose_detector_run_classification" translatable="false">pdrc</string>
+    <string name="pref_summary_pose_detector_run_classification">Classify squat and pushup poses. Count reps in streaming mode. To get the best classification results based on the current sample data, face the camera side way and make sure your full body is in the frame.</string>
+
+    <!-- Strings for segmentation preference. -->
+    <string name="pref_title_segmentation_raw_size_mask">Enable raw size mask</string>
+    <string name="pref_key_segmentation_raw_size_mask" translatable="false">srsm</string>
+</resources>