詹子聪 hace 5 años
commit
95401ecc8f
Se han modificado 76 ficheros con 4997 adiciones y 0 borrados
  1. 191 0
      LICENSE
  2. 77 0
      README.md
  3. 129 0
      app/build.gradle
  4. 97 0
      app/src/main/AndroidManifest.xml
  5. 5 0
      app/src/main/assets/automl/dict.txt
  6. 5 0
      app/src/main/assets/automl/manifest.json
  7. BIN
      app/src/main/assets/automl/model.tflite
  8. BIN
      app/src/main/assets/custom_models/bird_classifier.tflite
  9. 268 0
      app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java
  10. 37 0
      app/src/main/java/com/google/mlkit/vision/demo/CameraImageGraphic.java
  11. 706 0
      app/src/main/java/com/google/mlkit/vision/demo/CameraSource.java
  12. 182 0
      app/src/main/java/com/google/mlkit/vision/demo/CameraSourcePreview.java
  13. 38 0
      app/src/main/java/com/google/mlkit/vision/demo/EntryChoiceActivity.kt
  14. 70 0
      app/src/main/java/com/google/mlkit/vision/demo/FrameMetadata.java
  15. 239 0
      app/src/main/java/com/google/mlkit/vision/demo/GraphicOverlay.java
  16. 80 0
      app/src/main/java/com/google/mlkit/vision/demo/InferenceInfoGraphic.java
  17. 61 0
      app/src/main/java/com/google/mlkit/vision/demo/ScopedExecutor.java
  18. 36 0
      app/src/main/java/com/google/mlkit/vision/demo/VisionImageProcessor.java
  19. 171 0
      app/src/main/java/com/google/mlkit/vision/demo/java/ChooserActivity.java
  20. 240 0
      app/src/main/java/com/google/mlkit/vision/demo/java/LivePreviewActivity.java
  21. 361 0
      app/src/main/java/com/google/mlkit/vision/demo/java/StillImageActivity.java
  22. 263 0
      app/src/main/java/com/google/mlkit/vision/demo/java/VisionProcessorBase.java
  23. 94 0
      app/src/main/java/com/google/mlkit/vision/demo/java/barcodescanner/BarcodeGraphic.java
  24. 131 0
      app/src/main/java/com/google/mlkit/vision/demo/java/barcodescanner/BarcodeScannerProcessor.java
  25. 157 0
      app/src/main/java/com/google/mlkit/vision/demo/preference/LivePreviewPreferenceFragment.java
  26. 106 0
      app/src/main/java/com/google/mlkit/vision/demo/preference/PreferenceUtils.java
  27. 74 0
      app/src/main/java/com/google/mlkit/vision/demo/preference/SettingsActivity.java
  28. 31 0
      app/src/main/java/com/google/mlkit/vision/demo/preference/StillImagePreferenceFragment.java
  29. BIN
      app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png
  30. 9 0
      app/src/main/res/drawable-hdpi/ic_switch_camera_white_48dp.xml
  31. BIN
      app/src/main/res/drawable-hdpi/ic_switch_camera_white_48dp_inset.png
  32. BIN
      app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png
  33. 9 0
      app/src/main/res/drawable-mdpi/ic_switch_camera_white_48dp.xml
  34. BIN
      app/src/main/res/drawable-mdpi/ic_switch_camera_white_48dp_inset.png
  35. BIN
      app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png
  36. 9 0
      app/src/main/res/drawable-xhdpi/ic_switch_camera_white_48dp.xml
  37. BIN
      app/src/main/res/drawable-xhdpi/ic_switch_camera_white_48dp_inset.png
  38. BIN
      app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png
  39. 9 0
      app/src/main/res/drawable-xxhdpi/ic_switch_camera_white_48dp.xml
  40. BIN
      app/src/main/res/drawable-xxhdpi/ic_switch_camera_white_48dp_inset.png
  41. BIN
      app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png
  42. 9 0
      app/src/main/res/drawable-xxxhdpi/ic_switch_camera_white_48dp.xml
  43. BIN
      app/src/main/res/drawable-xxxhdpi/ic_switch_camera_white_48dp_inset.png
  44. 9 0
      app/src/main/res/drawable/list_item_background.xml
  45. 42 0
      app/src/main/res/drawable/logo_mlkit.xml
  46. 57 0
      app/src/main/res/layout-land/activity_vision_camerax_live_preview.xml
  47. 56 0
      app/src/main/res/layout-land/activity_vision_live_preview.xml
  48. 35 0
      app/src/main/res/layout/activity_chooser.xml
  49. 8 0
      app/src/main/res/layout/activity_settings.xml
  50. 77 0
      app/src/main/res/layout/activity_still_image.xml
  51. 60 0
      app/src/main/res/layout/activity_vision_camerax_live_preview.xml
  52. 31 0
      app/src/main/res/layout/activity_vision_entry_choice.xml
  53. 18 0
      app/src/main/res/layout/activity_vision_live_preview.xml
  54. 8 0
      app/src/main/res/layout/settings_style.xml
  55. 10 0
      app/src/main/res/layout/spinner_style.xml
  56. 9 0
      app/src/main/res/layout/toggle_style.xml
  57. 12 0
      app/src/main/res/menu/camera_button_menu.xml
  58. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  59. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  60. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  61. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  62. 53 0
      app/src/main/res/values/arrays.xml
  63. 14 0
      app/src/main/res/values/colors.xml
  64. 6 0
      app/src/main/res/values/dimens.xml
  65. 116 0
      app/src/main/res/values/strings.xml
  66. 3 0
      app/src/main/res/values/styles.xml
  67. 121 0
      app/src/main/res/xml/preference_live_preview_quickstart.xml
  68. 37 0
      app/src/main/res/xml/preference_still_image.xml
  69. 29 0
      build.gradle
  70. 20 0
      gradle.properties
  71. BIN
      gradle/wrapper/gradle-wrapper.jar
  72. 6 0
      gradle/wrapper/gradle-wrapper.properties
  73. 172 0
      gradlew
  74. 84 0
      gradlew.bat
  75. 8 0
      local.properties
  76. 2 0
      settings.gradle

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+   Copyright 2020 Google LLC
+
+   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
+
+       https://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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS

+ 77 - 0
README.md

@@ -0,0 +1,77 @@
+# ML Kit Vision Quickstart Sample App
+
+## Introduction
+
+This ML Kit Quickstart app demonstrates how to use and integrate various vision based ML Kit features into your app.
+
+## Feature List
+
+Features that are included in this Quickstart app:
+* [Object Detection](https://developers.google.com/ml-kit/vision/object-detection/android) - Detect, track, and classify objects in real time and static images
+* [Face Detection](https://developers.google.com/ml-kit/vision/face-detection/android) - Detect faces in real time and static images
+* [Text Recognition](https://developers.google.com/ml-kit/vision/text-recognition/android) - Recognize text in real time and static images
+* [Barcode Scanning](https://developers.google.com/ml-kit/vision/barcode-scanning/android)  - Scan barcodes in real time and static images
+* [Image Labeling](https://developers.google.com/ml-kit/vision/image-labeling/android) - Label images in real time and static images
+* [Custom Image Labeling - Birds](https://developers.google.com/ml-kit/vision/image-labeling/custom-models/android) - Label images of birds with a custom TensorFlow Lite model.
+* [Pose Detection](https://developers.google.com/ml-kit/vision/pose-detection/android) - Detect the position of the human body in real time.
+
+<img src="../screenshots/quickstart-picker.png" width="256"/> <img src="../screenshots/quickstart-image-labeling.png" width="256"/> <img src="../screenshots/quickstart-object-detection.png" width="256"/>
+
+## Getting Started
+
+* Run the sample code on your Android device or emulator
+* Try extending the code to add new features and functionality
+
+## How to use the app
+
+This app supports three usage scenarios: Live Camera, Static Image, and CameraX enabled live camera.
+
+### Live Camera scenario
+It uses the camera preview as input and contains these API workflows: Object detection & tracking, Face Detection, Text Recognition, Barcode Scanning, Image Labeling, and Pose Detection. There's also a settings page that allows you to configure several options:
+* Camera
+    * Preview size - Specify the preview size of rear/front camera manually (Default size is chosen appropriately based on screen size)
+    * Enable live viewport - Toggle between blocking camera preview by API processing and result rendering or not
+* Object detection / Custom Object Detection
+    * Enable multiple objects -- Enable multiple objects to be detected at once
+    * Enable classification -- Enable classification for each detected object
+* Face Detection
+    * Landmark mode -- Toggle between showing no or all facial landmarks
+    * Contour mode -- Toggle between showing no or all contours
+    * Classification mode -- Toggle between showing no or all classifications (smiling, eyes open/closed)
+    * Performance mode -- Toggle between two operating modes (Fast or Accurate)
+    * Face tracking -- Enable or disable face tracking
+    * Minimum face size -- Choose the proportion of the head width to the image width
+* Pose Detection
+    * Performance mode -- Allows you to switch between "Fast" and "Accurate" operation mode
+    * Show in-frame likelihood -- Displays InFrameLikelihood score for each landmark
+
+### Static Image scenario
+The static image scenario is identical to the live camera scenario, but instead relies on images fed into the app through the gallery.
+
+### CameraX Live Preview scenario
+The CameraX live preview scenario is very similar to the native live camera scenario, but instead relies on CameraX live preview. Note: CameraX is only supported on API level 21+.
+
+## Support
+
+* [Documentation](https://developers.google.com/ml-kit/guides)
+* [API Reference](https://developers.google.com/ml-kit/reference/android)
+* [Stack Overflow](https://stackoverflow.com/questions/tagged/google-mlkit)
+
+## License
+
+Copyright 2020 Google, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements.  See the NOTICE file distributed with this work for
+additional information regarding copyright ownership.  The ASF licenses this
+file to you 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.

+ 129 - 0
app/build.gradle

@@ -0,0 +1,129 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+    compileSdkVersion 29
+
+    defaultConfig {
+        applicationId "com.google.mlkit.vision.demo"
+        minSdkVersion 16
+        multiDexEnabled true
+        targetSdkVersion 29
+
+        versionCode 11
+        versionName "1.11"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        vectorDrawables.useSupportLibrary = true
+        setProperty("archivesBaseName", "vision-quickstart")
+    }
+    buildTypes {
+        proguard {
+            debuggable false
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard.cfg'
+            testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro', 'proguard.cfg'
+        }
+        testBuildType "proguard"
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+        debug {
+            minifyEnabled false
+            proguardFiles 'proguard-rules.pro'
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    // Do NOT compress tflite model files (need to call out to developers!)
+    aaptOptions {
+        noCompress "tflite"
+    }
+}
+
+repositories {
+    // Depending on AndroidX Snapshot Builds to get the latest CameraX libs.
+    maven { url 'https://androidx.dev/snapshots/builds/6787662/artifacts/repository/' }
+}
+
+dependencies {
+    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72'
+    implementation 'androidx.multidex:multidex:2.0.1'
+
+    // Barcode model
+    implementation 'com.google.mlkit:barcode-scanning:16.1.0'
+    // Or comment the dependency above and uncomment the dependency below to
+    // use unbundled model that depends on Google Play Services
+    // implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:16.1.3'
+
+    // Object detection feature with bundled default classifier
+    //implementation 'com.google.mlkit:object-detection:16.2.2'
+
+    // Object detection feature with custom classifier support
+    //implementation 'com.google.mlkit:object-detection-custom:16.3.0'
+
+    // Face features
+    //implementation 'com.google.mlkit:face-detection:16.0.3'
+    // Or comment the dependency above and uncomment the dependency below to
+    // use unbundled model that depends on Google Play Services
+    // implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.2'
+
+    // Text features
+    //implementation 'com.google.android.gms:play-services-mlkit-text-recognition:16.1.2'
+
+    // Image labeling
+    //implementation 'com.google.mlkit:image-labeling:17.0.1'
+    // Or comment the dependency above and uncomment the dependency below to
+    // use unbundled model that depends on Google Play Services
+    // implementation 'com.google.android.gms:play-services-mlkit-image-labeling:16.0.1'
+
+    // Image labeling custom
+    //implementation 'com.google.mlkit:image-labeling-custom:16.3.0'
+
+    // Pose detection with default models
+    //implementation 'com.google.mlkit:pose-detection:17.0.1-beta1'
+    // Pose detection with accurate models
+    //implementation 'com.google.mlkit:pose-detection-accurate:17.0.1-beta1'
+
+    // -------------------------------------------------------
+
+    //implementation 'com.google.code.gson:gson:2.8.5'
+    implementation 'com.google.guava:guava:27.1-android'
+
+    // For how to setup gradle dependencies in Android X, see:
+    // https://developer.android.com/training/testing/set-up-project#gradle-dependencies
+    // Core library
+    androidTestImplementation 'androidx.test:core:1.3.0'
+
+    // AndroidJUnitRunner and JUnit Rules
+    androidTestImplementation 'androidx.test:runner:1.3.0'
+    androidTestImplementation 'androidx.test:rules:1.3.0'
+
+    // Assertions
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+
+    // ViewModel and LiveData
+//    implementation "androidx.lifecycle:lifecycle-livedata:2.2.0"
+//    implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
+
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    implementation 'androidx.annotation:annotation:1.1.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+
+    // CameraX
+//    implementation "androidx.camera:camera-camera2:1.0.0-SNAPSHOT"
+//    implementation "androidx.camera:camera-lifecycle:1.0.0-SNAPSHOT"
+//    implementation "androidx.camera:camera-view:1.0.0-SNAPSHOT"
+}
+
+configurations {
+    // Resolves dependency conflict caused by some dependencies use
+    // com.google.guava:guava and com.google.guava:listenablefuture together.
+    all*.exclude group: 'com.google.guava', module: 'listenablefuture'
+}

+ 97 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.google.mlkit.vision.demo"
+    android:installLocation="auto">
+
+    <!-- CameraX libraries require minSdkVersion 21, while this quickstart app
+    supports low to 16. Needs to use overrideLibrary to make the merger tool
+    ignore this conflict and import the libraries while keeping the app's lower
+    minSdkVersion value. In code, will check SDK version, before calling CameraX
+    APIs. -->
+    <uses-sdk
+        tools:overrideLibrary="
+          androidx.camera.camera2, androidx.camera.core,
+          androidx.camera.view, androidx.camera.lifecycle" />
+
+    <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"/>
+
+    <application
+        android:name="androidx.multidex.MultiDexApplication"
+        android:icon="@drawable/logo_mlkit"
+        android:label="@string/app_name"
+        android:theme="@style/Theme.AppCompat">
+
+        <meta-data
+            android:name="com.google.android.gms.version"
+            android:value="@integer/google_play_services_version"/>
+
+        <!-- Optional: Add it to automatically download ML model to device after
+          your app is installed.-->
+        <meta-data
+            android:name="com.google.mlkit.vision.DEPENDENCIES"
+            android:value="barcode,face,ocr,ica"/>
+
+        <activity
+            android:name=".EntryChoiceActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".java.ChooserActivity"
+            android:exported="true">
+        </activity>
+
+        <activity
+            android:name=".java.LivePreviewActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+        </activity>
+
+        <activity
+            android:name=".java.StillImageActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+        </activity>
+
+        <activity
+            android:name=".kotlin.ChooserActivity"
+            android:exported="true">
+        </activity>
+
+        <activity
+            android:name=".kotlin.LivePreviewActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+        </activity>
+
+        <activity
+            android:name=".kotlin.CameraXLivePreviewActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+        </activity>
+
+        <activity
+            android:name=".kotlin.StillImageActivity"
+            android:exported="true"
+            android:theme="@style/AppTheme">
+        </activity>
+
+        <activity
+            android:name=".preference.SettingsActivity"
+            android:exported="false"/>
+
+    </application>
+
+</manifest>

+ 5 - 0
app/src/main/assets/automl/dict.txt

@@ -0,0 +1,5 @@
+daisy
+dandelion
+roses
+sunflowers
+tulips

+ 5 - 0
app/src/main/assets/automl/manifest.json

@@ -0,0 +1,5 @@
+{
+  "modelType": "IMAGE_LABELING",
+  "modelFile": "model.tflite",
+  "labelsFile": "dict.txt"
+}

BIN
app/src/main/assets/automl/model.tflite


BIN
app/src/main/assets/custom_models/bird_classifier.tflite


+ 268 - 0
app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java

@@ -0,0 +1,268 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+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 androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import android.util.Log;
+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();
+    }
+  }
+}

+ 37 - 0
app/src/main/java/com/google/mlkit/vision/demo/CameraImageGraphic.java

@@ -0,0 +1,37 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import com.google.mlkit.vision.demo.GraphicOverlay.Graphic;
+
+/** Draw camera image to background. */
+public class CameraImageGraphic extends 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);
+  }
+}

+ 706 - 0
app/src/main/java/com/google/mlkit/vision/demo/CameraSource.java

@@ -0,0 +1,706 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+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 androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+import com.google.android.gms.common.images.Size;
+import com.google.mlkit.vision.demo.preference.PreferenceUtils;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+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 = 1280;
+  public static final int DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT = 720;
+
+  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) {
+      throw new IOException("Could not find requested camera.");
+    }
+    Camera camera = Camera.open(requestedCameraId);
+
+    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());
+    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;
+    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();
+  }
+}

+ 182 - 0
app/src/main/java/com/google/mlkit/vision/demo/CameraSourcePreview.java

@@ -0,0 +1,182 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+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 com.google.mlkit.vision.demo.preference.PreferenceUtils;
+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);
+    }
+
+    try {
+      startIfReady();
+    } catch (IOException e) {
+      Log.e(TAG, "Could not start camera source.", e);
+    }
+  }
+
+  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;
+  }
+}

+ 38 - 0
app/src/main/java/com/google/mlkit/vision/demo/EntryChoiceActivity.kt

@@ -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.google.mlkit.vision.demo
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import android.widget.TextView
+import com.google.mlkit.vision.demo.java.ChooserActivity
+
+class EntryChoiceActivity : AppCompatActivity() {
+
+  override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+    setContentView(R.layout.activity_vision_entry_choice)
+
+    findViewById<TextView>(R.id.java_entry_point).setOnClickListener {
+      val intent = Intent(this@EntryChoiceActivity, ChooserActivity::class.java)
+      startActivity(intent)
+    }
+
+
+  }
+}

+ 70 - 0
app/src/main/java/com/google/mlkit/vision/demo/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.google.mlkit.vision.demo;
+
+/** 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);
+    }
+  }
+}

+ 239 - 0
app/src/main/java/com/google/mlkit/vision/demo/GraphicOverlay.java

@@ -0,0 +1,239 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+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);
+
+    /** 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) {
+      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);
+      }
+    }
+  }
+}

+ 80 - 0
app/src/main/java/com/google/mlkit/vision/demo/InferenceInfoGraphic.java

@@ -0,0 +1,80 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+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);
+    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.getImageWidth() + "x" + overlay.getImageHeight(),
+        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);
+    }*/
+  }
+}

+ 61 - 0
app/src/main/java/com/google/mlkit/vision/demo/ScopedExecutor.java

@@ -0,0 +1,61 @@
+/*
+ * 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.google.mlkit.vision.demo;
+
+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);
+  }
+}

+ 36 - 0
app/src/main/java/com/google/mlkit/vision/demo/VisionImageProcessor.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.google.mlkit.vision.demo;
+
+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();
+}

+ 171 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/ChooserActivity.java

@@ -0,0 +1,171 @@
+/*
+ * 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.google.mlkit.vision.demo.java;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.StrictMode;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback;
+import androidx.core.content.ContextCompat;
+import com.google.mlkit.vision.demo.BuildConfig;
+import com.google.mlkit.vision.demo.R;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Demo app chooser which takes care of runtime permission requesting and allow you pick from all
+ * available testing Activities.
+ */
+public final class ChooserActivity extends AppCompatActivity
+    implements OnRequestPermissionsResultCallback, AdapterView.OnItemClickListener {
+  private static final String TAG = "ChooserActivity";
+  private static final int PERMISSION_REQUESTS = 1;
+
+  // , StillImageActivity.class, CameraXLivePreviewActivity.class,LivePreviewActivity
+  private static final Class<?>[] CLASSES = new Class<?>[] {LivePreviewActivity.class};
+
+  private static final int[] DESCRIPTION_IDS =
+      new int[] {R.string.desc_camera_source_activity};
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    if (BuildConfig.DEBUG) {
+      StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
+      StrictMode.setVmPolicy(
+          new StrictMode.VmPolicy.Builder()
+              .detectLeakedSqlLiteObjects()
+              .detectLeakedClosableObjects()
+              .penaltyLog()
+              .build());
+    }
+    super.onCreate(savedInstanceState);
+    Log.d(TAG, "onCreate");
+
+    setContentView(R.layout.activity_chooser);
+
+    // Set up ListView and Adapter
+    ListView listView = findViewById(R.id.test_activity_list_view);
+
+    MyArrayAdapter adapter = new MyArrayAdapter(this, android.R.layout.simple_list_item_2, CLASSES);
+    adapter.setDescriptionIds(DESCRIPTION_IDS);
+
+    listView.setAdapter(adapter);
+    listView.setOnItemClickListener(this);
+
+    if (!allPermissionsGranted()) {
+      getRuntimePermissions();
+    }
+  }
+
+  @Override
+  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+    Class<?> clicked = CLASSES[position];
+    startActivity(new Intent(this, clicked));
+  }
+
+  private String[] getRequiredPermissions() {
+    try {
+      PackageInfo info = this.getPackageManager().getPackageInfo(this.getPackageName(), PackageManager.GET_PERMISSIONS);
+      String[] ps = info.requestedPermissions;
+      if (ps != null && ps.length > 0) {
+        return ps;
+      } else {
+        return new String[0];
+      }
+    } catch (Exception e) {
+      return new String[0];
+    }
+  }
+
+  private boolean allPermissionsGranted() {
+    for (String permission : getRequiredPermissions()) {
+      if (!isPermissionGranted(this, permission)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private void getRuntimePermissions() {
+    List<String> allNeededPermissions = new ArrayList<>();
+    for (String permission : getRequiredPermissions()) {
+      if (!isPermissionGranted(this, permission)) {
+        allNeededPermissions.add(permission);
+      }
+    }
+
+    if (!allNeededPermissions.isEmpty()) {
+      ActivityCompat.requestPermissions(this, allNeededPermissions.toArray(new String[0]), PERMISSION_REQUESTS);
+    }
+  }
+
+  private static boolean isPermissionGranted(Context context, String permission) {
+    if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
+      Log.i(TAG, "Permission granted: " + permission);
+      return true;
+    }
+    Log.i(TAG, "Permission NOT granted: " + permission);
+    return false;
+  }
+
+  private static class MyArrayAdapter extends ArrayAdapter<Class<?>> {
+
+    private final Context context;
+    private final Class<?>[] classes;
+    private int[] descriptionIds;
+
+    MyArrayAdapter(Context context, int resource, Class<?>[] objects) {
+      super(context, resource, objects);
+
+      this.context = context;
+      classes = objects;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      View view = convertView;
+
+      if (convertView == null) {
+        LayoutInflater inflater =
+            (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
+        view = inflater.inflate(android.R.layout.simple_list_item_2, null);
+      }
+
+      ((TextView) view.findViewById(android.R.id.text1)).setText(classes[position].getSimpleName());
+      ((TextView) view.findViewById(android.R.id.text2)).setText(descriptionIds[position]);
+
+      return view;
+    }
+
+    void setDescriptionIds(int[] descriptionIds) {
+      this.descriptionIds = descriptionIds;
+    }
+  }
+}

+ 240 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/LivePreviewActivity.java

@@ -0,0 +1,240 @@
+/*
+ * 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.google.mlkit.vision.demo.java;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback;
+import androidx.core.content.ContextCompat;
+import com.google.android.gms.common.annotation.KeepName;
+import com.google.mlkit.common.model.LocalModel;
+import com.google.mlkit.vision.demo.CameraSource;
+import com.google.mlkit.vision.demo.CameraSourcePreview;
+import com.google.mlkit.vision.demo.GraphicOverlay;
+import com.google.mlkit.vision.demo.R;
+import com.google.mlkit.vision.demo.java.barcodescanner.BarcodeScannerProcessor;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Live preview demo for ML Kit APIs. */
+@KeepName
+public final class LivePreviewActivity extends AppCompatActivity
+    implements OnRequestPermissionsResultCallback,
+        OnItemSelectedListener {
+  private static final String OBJECT_DETECTION = "Object Detection";
+  private static final String OBJECT_DETECTION_CUSTOM = "Custom Object Detection (Bird)";
+  private static final String CUSTOM_AUTOML_OBJECT_DETECTION =
+      "Custom AutoML Object Detection (Flower)";
+  private static final String FACE_DETECTION = "Face Detection";
+  private static final String TEXT_RECOGNITION = "Text Recognition";
+  private static final String BARCODE_SCANNING = "Barcode Scanning";
+  private static final String IMAGE_LABELING = "Image Labeling";
+  private static final String IMAGE_LABELING_CUSTOM = "Custom Image Labeling (Bird)";
+  private static final String CUSTOM_AUTOML_LABELING = "Custom AutoML Image Labeling (Flower)";
+  private static final String POSE_DETECTION = "Pose Detection";
+
+  private static final String TAG = "LivePreviewActivity";
+  private static final int PERMISSION_REQUESTS = 1;
+
+  private CameraSource cameraSource = null;
+  private CameraSourcePreview preview;
+  private GraphicOverlay graphicOverlay;
+  private String selectedModel = OBJECT_DETECTION;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    Log.d(TAG, "onCreate");
+
+    setContentView(R.layout.activity_vision_live_preview);
+
+    preview = findViewById(R.id.preview_view);
+    if (preview == null) {
+      Log.d(TAG, "Preview is null");
+    }
+    graphicOverlay = findViewById(R.id.graphic_overlay);
+    if (graphicOverlay == null) {
+      Log.d(TAG, "graphicOverlay is null");
+    }
+
+    if (allPermissionsGranted()) {
+      createCameraSource(selectedModel);
+    } else {
+      getRuntimePermissions();
+    }
+  }
+
+  @Override
+  public synchronized void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+    // An item was selected. You can retrieve the selected item using
+    // parent.getItemAtPosition(pos)
+    selectedModel = parent.getItemAtPosition(pos).toString();
+    Log.d(TAG, "Selected model: " + selectedModel);
+    preview.stop();
+    if (allPermissionsGranted()) {
+      createCameraSource(selectedModel);
+      startCameraSource();
+    } else {
+      getRuntimePermissions();
+    }
+  }
+
+  @Override
+  public void onNothingSelected(AdapterView<?> parent) {
+    // Do nothing.
+  }
+
+  private void createCameraSource(String model) {
+    // If there's no existing cameraSource, create one.
+    if (cameraSource == null) {
+      cameraSource = new CameraSource(this, graphicOverlay);
+    }
+
+    try {
+      Log.i(TAG, "Using Barcode Detector Processor");
+      cameraSource.setMachineLearningFrameProcessor(new BarcodeScannerProcessor(this));
+    } catch (RuntimeException e) {
+      Log.e(TAG, "Can not create image processor: " + model, e);
+      Toast.makeText(
+              getApplicationContext(),
+              "Can not create image processor: " + e.getMessage(),
+              Toast.LENGTH_LONG)
+          .show();
+    }
+  }
+
+  /**
+   * 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) {
+      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);
+      } catch (IOException e) {
+        Log.e(TAG, "Unable to start camera source.", e);
+        cameraSource.release();
+        cameraSource = null;
+      }
+    }
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    Log.d(TAG, "onResume");
+    createCameraSource(selectedModel);
+    startCameraSource();
+  }
+
+  /** Stops the camera. */
+  @Override
+  protected void onPause() {
+    super.onPause();
+    preview.stop();
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    if (cameraSource != null) {
+      cameraSource.release();
+    }
+  }
+
+  private String[] getRequiredPermissions() {
+    try {
+      PackageInfo info =
+          this.getPackageManager()
+              .getPackageInfo(this.getPackageName(), PackageManager.GET_PERMISSIONS);
+      String[] ps = info.requestedPermissions;
+      if (ps != null && ps.length > 0) {
+        return ps;
+      } else {
+        return new String[0];
+      }
+    } catch (Exception e) {
+      return new String[0];
+    }
+  }
+
+  private boolean allPermissionsGranted() {
+    for (String permission : getRequiredPermissions()) {
+      if (!isPermissionGranted(this, permission)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private void getRuntimePermissions() {
+    List<String> allNeededPermissions = new ArrayList<>();
+    for (String permission : getRequiredPermissions()) {
+      if (!isPermissionGranted(this, permission)) {
+        allNeededPermissions.add(permission);
+      }
+    }
+
+    if (!allNeededPermissions.isEmpty()) {
+      ActivityCompat.requestPermissions(
+          this, allNeededPermissions.toArray(new String[0]), PERMISSION_REQUESTS);
+    }
+  }
+
+  @Override
+  public void onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {
+    Log.i(TAG, "Permission granted!");
+    if (allPermissionsGranted()) {
+      createCameraSource(selectedModel);
+    }
+    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+  }
+
+  private static boolean isPermissionGranted(Context context, String permission) {
+    if (ContextCompat.checkSelfPermission(context, permission)
+        == PackageManager.PERMISSION_GRANTED) {
+      Log.i(TAG, "Permission granted: " + permission);
+      return true;
+    }
+    Log.i(TAG, "Permission NOT granted: " + permission);
+    return false;
+  }
+}

+ 361 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/StillImageActivity.java

@@ -0,0 +1,361 @@
+/*
+ * 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.google.mlkit.vision.demo.java;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
+import android.util.Pair;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.Spinner;
+import android.widget.Toast;
+import com.google.android.gms.common.annotation.KeepName;
+import com.google.mlkit.common.model.LocalModel;
+import com.google.mlkit.vision.demo.BitmapUtils;
+import com.google.mlkit.vision.demo.GraphicOverlay;
+import com.google.mlkit.vision.demo.R;
+import com.google.mlkit.vision.demo.VisionImageProcessor;
+import com.google.mlkit.vision.demo.java.barcodescanner.BarcodeScannerProcessor;
+import com.google.mlkit.vision.demo.preference.SettingsActivity;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Activity demonstrating different image detector features with a still image from camera. */
+@KeepName
+public final class StillImageActivity extends AppCompatActivity {
+
+  private static final String TAG = "StillImageActivity";
+
+  private static final String OBJECT_DETECTION = "Object Detection";
+  private static final String OBJECT_DETECTION_CUSTOM = "Custom Object Detection (Bird)";
+  private static final String CUSTOM_AUTOML_OBJECT_DETECTION =
+      "Custom AutoML Object Detection (Flower)";
+  private static final String FACE_DETECTION = "Face Detection";
+  private static final String BARCODE_SCANNING = "Barcode Scanning";
+  private static final String TEXT_RECOGNITION = "Text Recognition";
+  private static final String IMAGE_LABELING = "Image Labeling";
+  private static final String IMAGE_LABELING_CUSTOM = "Custom Image Labeling (Bird)";
+  private static final String CUSTOM_AUTOML_LABELING = "Custom AutoML Image Labeling (Flower)";
+  private static final String POSE_DETECTION = "Pose Detection";
+
+  private static final String SIZE_SCREEN = "w:screen"; // Match screen width
+  private static final String SIZE_1024_768 = "w:1024"; // ~1024*768 in a normal ratio
+  private static final String SIZE_640_480 = "w:640"; // ~640*480 in a normal ratio
+
+  private static final String KEY_IMAGE_URI = "com.google.mlkit.vision.demo.KEY_IMAGE_URI";
+  private static final String KEY_SELECTED_SIZE = "com.google.mlkit.vision.demo.KEY_SELECTED_SIZE";
+
+  private static final int REQUEST_IMAGE_CAPTURE = 1001;
+  private static final int REQUEST_CHOOSE_IMAGE = 1002;
+
+  private ImageView preview;
+  private GraphicOverlay graphicOverlay;
+  private String selectedMode = OBJECT_DETECTION;
+  private String selectedSize = SIZE_SCREEN;
+
+  boolean isLandScape;
+
+  private Uri imageUri;
+  private int imageMaxWidth;
+  private int imageMaxHeight;
+  private VisionImageProcessor imageProcessor;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    setContentView(R.layout.activity_still_image);
+
+    findViewById(R.id.select_image_button)
+        .setOnClickListener(
+            view -> {
+              // Menu for selecting either: a) take new photo b) select from existing
+              PopupMenu popup = new PopupMenu(StillImageActivity.this, view);
+              popup.setOnMenuItemClickListener(
+                  menuItem -> {
+                    int itemId = menuItem.getItemId();
+                    if (itemId == R.id.select_images_from_local) {
+                      startChooseImageIntentForResult();
+                      return true;
+                    } else if (itemId == R.id.take_photo_using_camera) {
+                      startCameraIntentForResult();
+                      return true;
+                    }
+                    return false;
+                  });
+              MenuInflater inflater = popup.getMenuInflater();
+              inflater.inflate(R.menu.camera_button_menu, popup.getMenu());
+              popup.show();
+            });
+    preview = findViewById(R.id.preview);
+    graphicOverlay = findViewById(R.id.graphic_overlay);
+
+    populateFeatureSelector();
+    populateSizeSelector();
+
+    isLandScape =
+        (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
+
+    if (savedInstanceState != null) {
+      imageUri = savedInstanceState.getParcelable(KEY_IMAGE_URI);
+      selectedSize = savedInstanceState.getString(KEY_SELECTED_SIZE);
+    }
+
+    View rootView = findViewById(R.id.root);
+    rootView
+        .getViewTreeObserver()
+        .addOnGlobalLayoutListener(
+            new OnGlobalLayoutListener() {
+              @Override
+              public void onGlobalLayout() {
+                rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                imageMaxWidth = rootView.getWidth();
+                imageMaxHeight = rootView.getHeight() - findViewById(R.id.control).getHeight();
+                if (SIZE_SCREEN.equals(selectedSize)) {
+                  tryReloadAndDetectInImage();
+                }
+              }
+            });
+
+    ImageView settingsButton = findViewById(R.id.settings_button);
+    settingsButton.setOnClickListener(
+        v -> {
+          Intent intent = new Intent(getApplicationContext(), SettingsActivity.class);
+          intent.putExtra(
+              SettingsActivity.EXTRA_LAUNCH_SOURCE, SettingsActivity.LaunchSource.STILL_IMAGE);
+          startActivity(intent);
+        });
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    Log.d(TAG, "onResume");
+    createImageProcessor();
+    tryReloadAndDetectInImage();
+  }
+
+  private void populateFeatureSelector() {
+    Spinner featureSpinner = findViewById(R.id.feature_selector);
+    List<String> options = new ArrayList<>();
+    options.add(OBJECT_DETECTION);
+    options.add(OBJECT_DETECTION_CUSTOM);
+    options.add(CUSTOM_AUTOML_OBJECT_DETECTION);
+    options.add(FACE_DETECTION);
+    options.add(BARCODE_SCANNING);
+    options.add(TEXT_RECOGNITION);
+    options.add(IMAGE_LABELING);
+    options.add(IMAGE_LABELING_CUSTOM);
+    options.add(CUSTOM_AUTOML_LABELING);
+    options.add(POSE_DETECTION);
+
+    // Creating adapter for featureSpinner
+    ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(this, R.layout.spinner_style, options);
+    // Drop down layout style - list view with radio button
+    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+    // attaching data adapter to spinner
+    featureSpinner.setAdapter(dataAdapter);
+    featureSpinner.setOnItemSelectedListener(
+        new OnItemSelectedListener() {
+
+          @Override
+          public void onItemSelected(
+              AdapterView<?> parentView, View selectedItemView, int pos, long id) {
+            selectedMode = parentView.getItemAtPosition(pos).toString();
+            createImageProcessor();
+            tryReloadAndDetectInImage();
+          }
+
+          @Override
+          public void onNothingSelected(AdapterView<?> arg0) {}
+        });
+  }
+
+  private void populateSizeSelector() {
+    Spinner sizeSpinner = findViewById(R.id.size_selector);
+    List<String> options = new ArrayList<>();
+    options.add(SIZE_SCREEN);
+    options.add(SIZE_1024_768);
+    options.add(SIZE_640_480);
+
+    // Creating adapter for featureSpinner
+    ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(this, R.layout.spinner_style, options);
+    // Drop down layout style - list view with radio button
+    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+    // attaching data adapter to spinner
+    sizeSpinner.setAdapter(dataAdapter);
+    sizeSpinner.setOnItemSelectedListener(
+        new OnItemSelectedListener() {
+
+          @Override
+          public void onItemSelected(
+              AdapterView<?> parentView, View selectedItemView, int pos, long id) {
+            selectedSize = parentView.getItemAtPosition(pos).toString();
+            createImageProcessor();
+            tryReloadAndDetectInImage();
+          }
+
+          @Override
+          public void onNothingSelected(AdapterView<?> arg0) {}
+        });
+  }
+
+  @Override
+  public void onSaveInstanceState(Bundle outState) {
+    super.onSaveInstanceState(outState);
+    outState.putParcelable(KEY_IMAGE_URI, imageUri);
+    outState.putString(KEY_SELECTED_SIZE, selectedSize);
+  }
+
+  private void startCameraIntentForResult() {
+    // Clean up last time's image
+    imageUri = null;
+    preview.setImageBitmap(null);
+
+    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
+      ContentValues values = new ContentValues();
+      values.put(MediaStore.Images.Media.TITLE, "New Picture");
+      values.put(MediaStore.Images.Media.DESCRIPTION, "From Camera");
+      imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+      takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
+      startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
+    }
+  }
+
+  private void startChooseImageIntentForResult() {
+    Intent intent = new Intent();
+    intent.setType("image/*");
+    intent.setAction(Intent.ACTION_GET_CONTENT);
+    startActivityForResult(Intent.createChooser(intent, "Select Picture"), REQUEST_CHOOSE_IMAGE);
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
+      tryReloadAndDetectInImage();
+    } else if (requestCode == REQUEST_CHOOSE_IMAGE && resultCode == RESULT_OK) {
+      // In this case, imageUri is returned by the chooser, save it.
+      imageUri = data.getData();
+      tryReloadAndDetectInImage();
+    } else {
+      super.onActivityResult(requestCode, resultCode, data);
+    }
+  }
+
+  private void tryReloadAndDetectInImage() {
+    Log.d(TAG, "Try reload and detect image");
+    try {
+      if (imageUri == null) {
+        return;
+      }
+
+      if (SIZE_SCREEN.equals(selectedSize) && imageMaxWidth == 0) {
+        // UI layout has not finished yet, will reload once it's ready.
+        return;
+      }
+
+      Bitmap imageBitmap = BitmapUtils.getBitmapFromContentUri(getContentResolver(), imageUri);
+      if (imageBitmap == null) {
+        return;
+      }
+
+      // Clear the overlay first
+      graphicOverlay.clear();
+
+      // Get the dimensions of the image view
+      Pair<Integer, Integer> targetedSize = getTargetedWidthHeight();
+
+      // Determine how much to scale down the image
+      float scaleFactor =
+          Math.max(
+              (float) imageBitmap.getWidth() / (float) targetedSize.first,
+              (float) imageBitmap.getHeight() / (float) targetedSize.second);
+
+      Bitmap resizedBitmap =
+          Bitmap.createScaledBitmap(
+              imageBitmap,
+              (int) (imageBitmap.getWidth() / scaleFactor),
+              (int) (imageBitmap.getHeight() / scaleFactor),
+              true);
+
+      preview.setImageBitmap(resizedBitmap);
+
+      if (imageProcessor != null) {
+        graphicOverlay.setImageSourceInfo(
+            resizedBitmap.getWidth(), resizedBitmap.getHeight(), /* isFlipped= */ false);
+        imageProcessor.processBitmap(resizedBitmap, graphicOverlay);
+      } else {
+        Log.e(TAG, "Null imageProcessor, please check adb logs for imageProcessor creation error");
+      }
+    } catch (IOException e) {
+      Log.e(TAG, "Error retrieving saved image");
+      imageUri = null;
+    }
+  }
+
+  private Pair<Integer, Integer> getTargetedWidthHeight() {
+    int targetWidth;
+    int targetHeight;
+
+    switch (selectedSize) {
+      case SIZE_SCREEN:
+        targetWidth = imageMaxWidth;
+        targetHeight = imageMaxHeight;
+        break;
+      case SIZE_640_480:
+        targetWidth = isLandScape ? 640 : 480;
+        targetHeight = isLandScape ? 480 : 640;
+        break;
+      case SIZE_1024_768:
+        targetWidth = isLandScape ? 1024 : 768;
+        targetHeight = isLandScape ? 768 : 1024;
+        break;
+      default:
+        throw new IllegalStateException("Unknown size");
+    }
+
+    return new Pair<>(targetWidth, targetHeight);
+  }
+
+  private void createImageProcessor() {
+    try {
+      imageProcessor = new BarcodeScannerProcessor(this);
+    } catch (Exception e) {
+      Log.e(TAG, "Can not create image processor: " + selectedMode, e);
+      Toast.makeText(
+              getApplicationContext(),
+              "Can not create image processor: " + e.getMessage(),
+              Toast.LENGTH_LONG)
+          .show();
+    }
+  }
+}

+ 263 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/VisionProcessorBase.java

@@ -0,0 +1,263 @@
+/*
+ * 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.google.mlkit.vision.demo.java;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build.VERSION_CODES;
+import android.os.SystemClock;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import android.util.Log;
+import android.widget.Toast;
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.TaskExecutors;
+import com.google.mlkit.vision.common.InputImage;
+import com.google.mlkit.vision.demo.BitmapUtils;
+import com.google.mlkit.vision.demo.CameraImageGraphic;
+import com.google.mlkit.vision.demo.FrameMetadata;
+import com.google.mlkit.vision.demo.GraphicOverlay;
+import com.google.mlkit.vision.demo.InferenceInfoGraphic;
+import com.google.mlkit.vision.demo.ScopedExecutor;
+import com.google.mlkit.vision.demo.VisionImageProcessor;
+import com.google.mlkit.vision.demo.preference.PreferenceUtils;
+import java.nio.ByteBuffer;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Abstract base class for vision frame processors. Subclasses need to implement {@link
+ * #onSuccess(Object, GraphicOverlay)} 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;
+
+  // 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;
+              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));
+              }
+              graphicOverlay.add(
+                  new InferenceInfoGraphic(
+                      graphicOverlay,
+                      currentFrameLatencyMs,
+                      currentDetectorLatencyMs,
+                      shouldShowFps ? framesPerSecond : null));
+              VisionProcessorBase.this.onSuccess(results, graphicOverlay);
+              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;
+    numRuns = 0;
+    totalFrameMs = 0;
+    totalDetectorMs = 0;
+    fpsTimer.cancel();
+  }
+
+  protected abstract Task<T> detectInImage(InputImage image);
+
+  protected abstract void onSuccess(@NonNull T results, @NonNull GraphicOverlay graphicOverlay);
+
+  protected abstract void onFailure(@NonNull Exception e);
+}

+ 94 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/barcodescanner/BarcodeGraphic.java

@@ -0,0 +1,94 @@
+/*
+ * 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.google.mlkit.vision.demo.java.barcodescanner;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import com.google.mlkit.vision.barcode.Barcode;
+import com.google.mlkit.vision.demo.GraphicOverlay;
+import com.google.mlkit.vision.demo.GraphicOverlay.Graphic;
+
+/** Graphic instance for rendering Barcode position and content information in an overlay view. */
+public class BarcodeGraphic extends 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;
+
+  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);
+    float textWidth = barcodePaint.measureText(barcode.getRawValue());
+    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);*/
+  }
+}

+ 131 - 0
app/src/main/java/com/google/mlkit/vision/demo/java/barcodescanner/BarcodeScannerProcessor.java

@@ -0,0 +1,131 @@
+/*
+ * 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.google.mlkit.vision.demo.java.barcodescanner;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Point;
+import androidx.annotation.NonNull;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.tasks.Task;
+import com.google.mlkit.vision.barcode.Barcode;
+import com.google.mlkit.vision.barcode.BarcodeScanner;
+import com.google.mlkit.vision.barcode.BarcodeScanning;
+import com.google.mlkit.vision.common.InputImage;
+import com.google.mlkit.vision.demo.GraphicOverlay;
+import com.google.mlkit.vision.demo.java.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 Activity mActivity;
+  public BarcodeScannerProcessor(Activity 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();
+    mActivity = context;
+    barcodeScanner = BarcodeScanning.getClient();
+  }
+
+  @Override
+  public void stop() {
+    super.stop();
+    barcodeScanner.close();
+  }
+
+  @Override
+  protected Task<List<Barcode>> detectInImage(InputImage image) {
+    return barcodeScanner.process(image);
+  }
+
+  private boolean mResultGot = false;
+  @Override
+  protected void onSuccess(@NonNull List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay) {
+    if (barcodes.isEmpty()) {
+      Log.v(MANUAL_TESTING_LOG, "No barcode has been detected");
+    }
+
+    if (mResultGot) {
+      return;
+    }
+    for (int i = 0; i < barcodes.size(); ++i) {
+      Barcode barcode = barcodes.get(i);
+      mResultGot = true;
+      //Toast.makeText(mActivity, "Result: " + barcode.getRawValue(), Toast.LENGTH_SHORT).show();
+      //graphicOverlay.add(new BarcodeGraphic(graphicOverlay, barcode));
+      logExtrasForTesting(barcode);
+      break;
+    }
+
+    if (mResultGot) {
+      mActivity.finish();
+      mActivity = null;
+    }
+  }
+
+  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);
+  }
+}

+ 157 - 0
app/src/main/java/com/google/mlkit/vision/demo/preference/LivePreviewPreferenceFragment.java

@@ -0,0 +1,157 @@
+/*
+ * 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.google.mlkit.vision.demo.preference;
+
+import android.hardware.Camera;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.widget.Toast;
+import androidx.annotation.StringRes;
+import com.google.mlkit.vision.demo.CameraSource;
+import com.google.mlkit.vision.demo.CameraSource.SizePair;
+import com.google.mlkit.vision.demo.R;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Configures live preview demo settings. */
+public class LivePreviewPreferenceFragment extends PreferenceFragment {
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    addPreferencesFromResource(R.xml.preference_live_preview_quickstart);
+    setUpCameraPreferences();
+    setUpFaceDetectionPreferences();
+  }
+
+  void setUpCameraPreferences() {
+    PreferenceCategory cameraPreference = (PreferenceCategory) findPreference(getString(R.string.pref_category_key_camera));
+    cameraPreference.removePreference(findPreference(getString(R.string.pref_key_camerax_rear_camera_target_resolution)));
+    cameraPreference.removePreference(findPreference(getString(R.string.pref_key_camerax_front_camera_target_resolution)));
+    setUpCameraPreviewSizePreference(R.string.pref_key_rear_camera_preview_size, R.string.pref_key_rear_camera_picture_size,
+          CameraSource.CAMERA_FACING_BACK);
+
+    setUpCameraPreviewSizePreference(R.string.pref_key_front_camera_preview_size, R.string.pref_key_front_camera_picture_size, CameraSource.CAMERA_FACING_FRONT);
+  }
+
+  private void setUpCameraPreviewSizePreference(
+      @StringRes int previewSizePrefKeyId, @StringRes int pictureSizePrefKeyId, int cameraId) {
+    ListPreference previewSizePreference =
+        (ListPreference) findPreference(getString(previewSizePrefKeyId));
+
+    Camera camera = null;
+    try {
+      camera = Camera.open(cameraId);
+
+      List<SizePair> previewSizeList = CameraSource.generateValidPreviewSizeList(camera);
+      String[] previewSizeStringValues = new String[previewSizeList.size()];
+      Map<String, String> previewToPictureSizeStringMap = new HashMap<>();
+      for (int i = 0; i < previewSizeList.size(); i++) {
+        SizePair sizePair = previewSizeList.get(i);
+        previewSizeStringValues[i] = sizePair.preview.toString();
+        if (sizePair.picture != null) {
+          previewToPictureSizeStringMap.put(
+              sizePair.preview.toString(), sizePair.picture.toString());
+        }
+      }
+      previewSizePreference.setEntries(previewSizeStringValues);
+      previewSizePreference.setEntryValues(previewSizeStringValues);
+
+      if (previewSizePreference.getEntry() == null) {
+        // First time of opening the Settings page.
+        SizePair sizePair =
+            CameraSource.selectSizePair(
+                camera,
+                CameraSource.DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH,
+                CameraSource.DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT);
+        String previewSizeString = sizePair.preview.toString();
+        previewSizePreference.setValue(previewSizeString);
+        previewSizePreference.setSummary(previewSizeString);
+        PreferenceUtils.saveString(
+            getActivity(),
+            pictureSizePrefKeyId,
+            sizePair.picture != null ? sizePair.picture.toString() : null);
+      } else {
+        previewSizePreference.setSummary(previewSizePreference.getEntry());
+      }
+
+      previewSizePreference.setOnPreferenceChangeListener(
+          (preference, newValue) -> {
+            String newPreviewSizeStringValue = (String) newValue;
+            previewSizePreference.setSummary(newPreviewSizeStringValue);
+            PreferenceUtils.saveString(
+                getActivity(),
+                pictureSizePrefKeyId,
+                previewToPictureSizeStringMap.get(newPreviewSizeStringValue));
+            return true;
+          });
+    } catch (RuntimeException e) {
+      // If there's no camera for the given camera id, hide the corresponding preference.
+      ((PreferenceCategory) findPreference(getString(R.string.pref_category_key_camera)))
+          .removePreference(previewSizePreference);
+    } finally {
+      if (camera != null) {
+        camera.release();
+      }
+    }
+  }
+
+  private void setUpFaceDetectionPreferences() {
+    setUpListPreference(R.string.pref_key_live_preview_face_detection_landmark_mode);
+    setUpListPreference(R.string.pref_key_live_preview_face_detection_contour_mode);
+    setUpListPreference(R.string.pref_key_live_preview_face_detection_classification_mode);
+    setUpListPreference(R.string.pref_key_live_preview_face_detection_performance_mode);
+
+    EditTextPreference minFaceSizePreference =
+        (EditTextPreference)
+            findPreference(getString(R.string.pref_key_live_preview_face_detection_min_face_size));
+    minFaceSizePreference.setSummary(minFaceSizePreference.getText());
+    minFaceSizePreference.setOnPreferenceChangeListener(
+        (preference, newValue) -> {
+          try {
+            float minFaceSize = Float.parseFloat((String) newValue);
+            if (minFaceSize >= 0.0f && minFaceSize <= 1.0f) {
+              minFaceSizePreference.setSummary((String) newValue);
+              return true;
+            }
+          } catch (NumberFormatException e) {
+            // Fall through intentionally.
+          }
+
+          Toast.makeText(
+                  getActivity(), R.string.pref_toast_invalid_min_face_size, Toast.LENGTH_LONG)
+              .show();
+          return false;
+        });
+  }
+
+  private void setUpListPreference(@StringRes int listPreferenceKeyId) {
+    ListPreference listPreference = (ListPreference) findPreference(getString(listPreferenceKeyId));
+    listPreference.setSummary(listPreference.getEntry());
+    listPreference.setOnPreferenceChangeListener(
+        (preference, newValue) -> {
+          int index = listPreference.findIndexOfValue((String) newValue);
+          listPreference.setSummary(listPreference.getEntries()[index]);
+          return true;
+        });
+  }
+}

+ 106 - 0
app/src/main/java/com/google/mlkit/vision/demo/preference/PreferenceUtils.java

@@ -0,0 +1,106 @@
+/*
+ * 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.google.mlkit.vision.demo.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StringRes;
+import com.google.android.gms.common.images.Size;
+import com.google.common.base.Preconditions;
+import com.google.mlkit.common.model.LocalModel;
+import com.google.mlkit.vision.demo.CameraSource;
+import com.google.mlkit.vision.demo.CameraSource.SizePair;
+import com.google.mlkit.vision.demo.R;
+
+/** 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 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 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, false);
+  }
+
+  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, 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, false);
+  }
+
+  private PreferenceUtils() {}
+}

+ 74 - 0
app/src/main/java/com/google/mlkit/vision/demo/preference/SettingsActivity.java

@@ -0,0 +1,74 @@
+/*
+ * 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.google.mlkit.vision.demo.preference;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.mlkit.vision.demo.R;
+
+/**
+ * Hosts the preference fragment to configure settings for a demo activity that specified by the
+ * {@link LaunchSource}.
+ */
+public class SettingsActivity extends AppCompatActivity {
+
+  public static final String EXTRA_LAUNCH_SOURCE = "extra_launch_source";
+
+  /** Specifies where this activity is launched from. */
+  public enum LaunchSource {
+    LIVE_PREVIEW(R.string.pref_screen_title_live_preview, LivePreviewPreferenceFragment.class),
+    STILL_IMAGE(R.string.pref_screen_title_still_image, StillImagePreferenceFragment.class),
+    CAMERAX_LIVE_PREVIEW(
+        R.string.pref_screen_title_camerax_live_preview,
+            LivePreviewPreferenceFragment.class);
+
+    private final int titleResId;
+    private final Class<? extends PreferenceFragment> prefFragmentClass;
+
+    LaunchSource(int titleResId, Class<? extends PreferenceFragment> prefFragmentClass) {
+      this.titleResId = titleResId;
+      this.prefFragmentClass = prefFragmentClass;
+    }
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    setContentView(R.layout.activity_settings);
+
+    LaunchSource launchSource =
+        (LaunchSource) getIntent().getSerializableExtra(EXTRA_LAUNCH_SOURCE);
+    ActionBar actionBar = getSupportActionBar();
+    if (actionBar != null) {
+      actionBar.setTitle(launchSource.titleResId);
+    }
+
+    try {
+      getFragmentManager()
+          .beginTransaction()
+          .replace(
+              R.id.settings_container,
+              launchSource.prefFragmentClass.getDeclaredConstructor().newInstance())
+          .commit();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+}

+ 31 - 0
app/src/main/java/com/google/mlkit/vision/demo/preference/StillImagePreferenceFragment.java

@@ -0,0 +1,31 @@
+/*
+ * 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.google.mlkit.vision.demo.preference;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import com.google.mlkit.vision.demo.R;
+
+/** Configures still image demo settings. */
+public class StillImagePreferenceFragment extends PreferenceFragment {
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    addPreferencesFromResource(R.xml.preference_still_image);
+  }
+}

BIN
app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png


+ 9 - 0
app/src/main/res/drawable-hdpi/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="@drawable/ic_switch_camera_white_48dp_inset"
+    android:insetTop="3.333333492dp"
+    android:insetLeft="3.333333492dp"
+    android:insetBottom="7.333333492dp"
+    android:insetRight="3.333333492dp"
+    android:visible="true" />

BIN
app/src/main/res/drawable-hdpi/ic_switch_camera_white_48dp_inset.png


BIN
app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png


+ 9 - 0
app/src/main/res/drawable-mdpi/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="@drawable/ic_switch_camera_white_48dp_inset"
+    android:insetTop="3dp"
+    android:insetLeft="3dp"
+    android:insetBottom="7dp"
+    android:insetRight="3dp"
+    android:visible="true" />

BIN
app/src/main/res/drawable-mdpi/ic_switch_camera_white_48dp_inset.png


BIN
app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png


+ 9 - 0
app/src/main/res/drawable-xhdpi/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="@drawable/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" />

BIN
app/src/main/res/drawable-xhdpi/ic_switch_camera_white_48dp_inset.png


BIN
app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png


+ 9 - 0
app/src/main/res/drawable-xxhdpi/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="@drawable/ic_switch_camera_white_48dp_inset"
+    android:insetTop="3.666666746dp"
+    android:insetLeft="3.666666746dp"
+    android:insetBottom="7.666666985dp"
+    android:insetRight="3.666666746dp"
+    android:visible="true" />

BIN
app/src/main/res/drawable-xxhdpi/ic_switch_camera_white_48dp_inset.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png


+ 9 - 0
app/src/main/res/drawable-xxxhdpi/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="@drawable/ic_switch_camera_white_48dp_inset"
+    android:insetTop="3.75dp"
+    android:insetLeft="3.75dp"
+    android:insetBottom="7.75dp"
+    android:insetRight="3.75dp"
+    android:visible="true" />

BIN
app/src/main/res/drawable-xxxhdpi/ic_switch_camera_white_48dp_inset.png


+ 9 - 0
app/src/main/res/drawable/list_item_background.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <stroke android:width="3dip" android:color="@color/blue"/>
+    <corners android:bottomRightRadius="16dp"
+        android:bottomLeftRadius="16dp"
+        android:topLeftRadius="16dp"
+        android:topRightRadius="16dp" />
+</shape>

+ 42 - 0
app/src/main/res/drawable/logo_mlkit.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="NewApi"
+    android:width="172dp"
+    android:height="129dp"
+    android:viewportWidth="172.43"
+    android:viewportHeight="129.06">
+    <path
+        android:fillColor="#4285f4"
+        android:pathData="M1.102,114.948l57.68,-109.632l16.815,8.847l-57.68,109.632z" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M9.5,119.43m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+    <path
+        android:fillColor="#abccfc"
+        android:pathData="M57.69,9.76h19v109.85h-19z" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M67.19,9.76m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+    <path
+        android:fillColor="#4285f4"
+        android:pathData="M58.779,114.962l57.68,-109.632l16.815,8.847l-57.68,109.632z" />
+    <path
+        android:fillColor="#abccfc"
+        android:pathData="M115.38,9.76h19v109.85h-19z" />
+    <path
+        android:fillColor="#4285f4"
+        android:pathData="M124.88,109.93h38.39v19h-38.39z" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M124.88,119.43m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M163.26,119.43m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M124.88,9.76m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+    <path
+        android:fillColor="#0d47a1"
+        android:pathData="M67.19,119.43m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0" />
+</vector>

+ 57 - 0
app/src/main/res/layout-land/activity_vision_camerax_live_preview.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+
+  <androidx.camera.view.PreviewView
+      android:id="@+id/preview_view"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      app:layout_constraintStart_toStartOf="parent"
+      app:layout_constraintEnd_toStartOf="@+id/control"/>
+
+  <com.google.mlkit.vision.demo.GraphicOverlay
+      android:id="@+id/graphic_overlay"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      app:layout_constraintStart_toStartOf="@id/preview_view"
+      app:layout_constraintEnd_toEndOf="@id/preview_view" />
+
+  <FrameLayout
+      android:id="@id/control"
+      android:layout_width="220dp"
+      android:layout_height="match_parent"
+      app:layout_constraintStart_toEndOf="@id/preview_view"
+      app:layout_constraintEnd_toEndOf="parent"
+      android:background="#000">
+
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top"/>
+
+    <ToggleButton
+        android:id="@+id/facing_switch"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="bottom|start"
+        android:background="@layout/toggle_style"
+        android:checked="false"
+        android:textOff=""
+        android:textOn=""/>
+
+    <ImageView
+        android:id="@+id/settings_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:padding="12dp"
+        android:contentDescription="@string/menu_item_settings"
+        android:src="@drawable/ic_settings_white_24dp"/>
+  </FrameLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 56 - 0
app/src/main/res/layout-land/activity_vision_live_preview.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+
+  <com.google.mlkit.vision.demo.CameraSourcePreview
+      android:id="@+id/preview_view"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      app:layout_constraintStart_toStartOf="parent"
+      app:layout_constraintEnd_toStartOf="@+id/control"/>
+
+  <com.google.mlkit.vision.demo.GraphicOverlay
+      android:id="@+id/graphic_overlay"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      app:layout_constraintStart_toStartOf="@id/preview_view"
+      app:layout_constraintEnd_toEndOf="@id/preview_view" />
+
+  <FrameLayout
+      android:id="@id/control"
+      android:layout_width="220dp"
+      android:layout_height="match_parent"
+      app:layout_constraintEnd_toEndOf="parent"
+      android:background="#000">
+
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top"/>
+
+    <ToggleButton
+        android:id="@+id/facing_switch"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="bottom|start"
+        android:background="@layout/toggle_style"
+        android:checked="false"
+        android:textOff=""
+        android:textOn=""/>
+
+    <ImageView
+        android:id="@+id/settings_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:padding="12dp"
+        android:contentDescription="@string/menu_item_settings"
+        android:src="@drawable/ic_settings_white_24dp"/>
+  </FrameLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 35 - 0
app/src/main/res/layout/activity_chooser.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:orientation="vertical">
+
+  <ImageView
+      android:id="@+id/imageView"
+      android:contentDescription="@string/app_name"
+      android:layout_width="match_parent"
+      android:layout_height="50dp"
+      android:layout_marginTop="32dp"
+      app:srcCompat="@drawable/logo_mlkit" />
+
+  <TextView
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:paddingTop="32dp"
+      android:paddingBottom="32dp"
+      android:fontFamily="google-sans"
+      android:gravity="center_horizontal"
+      android:text="@string/app_name"
+      android:textColor="@color/white"
+      android:textSize="18sp"/>
+
+  <ListView
+      android:id="@+id/test_activity_list_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+</LinearLayout>

+ 8 - 0
app/src/main/res/layout/activity_settings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/settings_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+</LinearLayout>

+ 77 - 0
app/src/main/res/layout/activity_still_image.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+
+  <ImageView
+      android:id="@+id/preview"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:adjustViewBounds="true"
+      app:layout_constraintBottom_toTopOf="@+id/control"
+      app:layout_constraintEnd_toEndOf="parent"
+      app:layout_constraintStart_toStartOf="parent"
+      app:layout_constraintTop_toTopOf="parent" />
+
+  <com.google.mlkit.vision.demo.GraphicOverlay
+      android:id="@+id/graphic_overlay"
+      android:layout_width="0dp"
+      android:layout_height="0dp"
+      app:layout_constraintLeft_toLeftOf="@id/preview"
+      app:layout_constraintRight_toRightOf="@id/preview"
+      app:layout_constraintTop_toTopOf="@id/preview"
+      app:layout_constraintBottom_toBottomOf="@id/preview"/>
+
+  <LinearLayout
+      android:id="@id/control"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      app:layout_constraintBottom_toBottomOf="parent"
+      android:background="#000"
+      android:orientation="vertical">
+
+    <Button
+        android:id="@+id/select_image_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_margin="12dp"
+        android:text="@string/select_image"/>
+
+    <LinearLayout
+        android:id="@+id/control2"
+        android:layout_width="match_parent"
+        android:layout_height="60dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:background="#000"
+        android:orientation="horizontal">
+
+      <Spinner
+          android:id="@+id/size_selector"
+          android:layout_width="0dp"
+          android:layout_weight="1"
+          android:layout_height="wrap_content"
+          android:layout_gravity="center"/>
+
+      <Spinner
+          android:id="@+id/feature_selector"
+          android:layout_width="0dp"
+          android:layout_weight="1"
+          android:layout_height="wrap_content"
+          android:layout_gravity="center"/>
+    </LinearLayout>
+  </LinearLayout>
+
+  <include
+      layout="@layout/settings_style"
+      android:id="@+id/settings_button"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      app:layout_constraintRight_toRightOf="@id/root"
+      app:layout_constraintTop_toTopOf="@id/root"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 60 - 0
app/src/main/res/layout/activity_vision_camerax_live_preview.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+
+  <androidx.camera.view.PreviewView
+      android:id="@+id/preview_view"
+      android:layout_width="match_parent"
+      android:layout_height="0dp"
+      app:layout_constraintTop_toTopOf="parent"
+      app:layout_constraintBottom_toTopOf="@+id/control"/>
+
+  <com.google.mlkit.vision.demo.GraphicOverlay
+      android:id="@+id/graphic_overlay"
+      android:layout_width="0dp"
+      android:layout_height="0dp"
+      app:layout_constraintLeft_toLeftOf="@id/preview_view"
+      app:layout_constraintRight_toRightOf="@id/preview_view"
+      app:layout_constraintTop_toTopOf="@id/preview_view"
+      app:layout_constraintBottom_toBottomOf="@id/preview_view"/>
+
+  <include
+      android:id="@+id/settings_button"
+      layout="@layout/settings_style"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      app:layout_constraintRight_toRightOf="@id/preview_view"
+      app:layout_constraintTop_toTopOf="@id/preview_view" />
+
+  <LinearLayout
+      android:id="@id/control"
+      android:layout_width="match_parent"
+      android:layout_height="60dp"
+      app:layout_constraintBottom_toBottomOf="parent"
+      android:background="#000"
+      android:orientation="horizontal">
+
+    <ToggleButton
+        android:id="@+id/facing_switch"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:background="@layout/toggle_style"
+        android:checked="false"
+        android:textOff=""
+        android:textOn=""/>
+
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"/>
+
+  </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 31 - 0
app/src/main/res/layout/activity_vision_entry_choice.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:orientation="vertical">
+
+    <TextView
+        android:background="@drawable/list_item_background"
+        android:id="@+id/java_entry_point"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/activity_vertical_margin"
+        android:padding="20dp"
+        android:text="@string/java_entry_title"
+        android:textSize="26sp" />
+
+    <TextView
+        android:background="@drawable/list_item_background"
+        android:id="@+id/kotlin_entry_point"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/activity_vertical_margin"
+        android:padding="20dp"
+        android:text="@string/kotlin_entry_title"
+        android:textSize="26sp" />
+</LinearLayout>

+ 18 - 0
app/src/main/res/layout/activity_vision_live_preview.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+
+  <com.google.mlkit.vision.demo.CameraSourcePreview
+      android:id="@+id/preview_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+
+  <com.google.mlkit.vision.demo.GraphicOverlay
+      android:id="@+id/graphic_overlay"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+</FrameLayout>

+ 8 - 0
app/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="@drawable/ic_settings_white_24dp"/>

+ 10 - 0
app/src/main/res/layout/spinner_style.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:textStyle="bold"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="1dip"
+    android:gravity="center"
+    android:textColor="#FFF"
+    android:textSize="16sp"/>

+ 9 - 0
app/src/main/res/layout/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>

+ 12 - 0
app/src/main/res/menu/camera_button_menu.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item android:id="@+id/select_images_from_local"
+      android:title="Select image from album"
+      android:orderInCategory="100"
+      app:showAsAction="never" />
+  <item android:id="@+id/take_photo_using_camera"
+      android:title="Take photo"
+      android:orderInCategory="100"
+      app:showAsAction="never" />
+</menu>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


+ 53 - 0
app/src/main/res/values/arrays.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <string-array name="pref_entries_face_detector_landmark_mode">
+    <item>@string/pref_entries_face_detector_landmark_mode_no_landmarks</item>
+    <item>@string/pref_entries_face_detector_landmark_mode_all_landmarks</item>
+  </string-array>
+
+  <string-array name="pref_entry_values_face_detector_landmark_mode">
+    <item>@string/pref_entry_values_face_detector_landmark_mode_no_landmarks</item>
+    <item>@string/pref_entry_values_face_detector_landmark_mode_all_landmarks</item>
+  </string-array>
+
+  <string-array name="pref_entries_face_detector_contour_mode">
+    <item>@string/pref_entries_face_detector_contour_mode_no_contours</item>
+    <item>@string/pref_entries_face_detector_contour_mode_all_contours</item>
+  </string-array>
+
+  <string-array name="pref_entry_values_face_detector_contour_mode">
+    <item>@string/pref_entry_values_face_detector_contour_mode_no_contours</item>
+    <item>@string/pref_entry_values_face_detector_contour_mode_all_contours</item>
+  </string-array>
+
+  <string-array name="pref_entries_face_detector_classification_mode">
+    <item>@string/pref_entries_face_detector_classification_mode_no_classifications</item>
+    <item>@string/pref_entries_face_detector_classification_mode_all_classifications</item>
+  </string-array>
+
+  <string-array name="pref_entry_values_face_detector_classification_mode">
+    <item>@string/pref_entry_values_face_detector_classification_mode_no_classifications</item>
+    <item>@string/pref_entry_values_face_detector_classification_mode_all_classifications</item>
+  </string-array>
+
+  <string-array name="pref_entries_face_detector_performance_mode">
+    <item>@string/pref_entries_face_detector_performance_mode_fast</item>
+    <item>@string/pref_entries_face_detector_performance_mode_accurate</item>
+  </string-array>
+
+  <string-array name="pref_entry_values_face_detector_performance_mode">
+    <item>@string/pref_entry_values_face_detector_performance_mode_fast</item>
+    <item>@string/pref_entry_values_face_detector_performance_mode_accurate</item>
+  </string-array>
+
+  <string-array name="pref_entries_values_pose_detector_performance_mode">
+    <item>@string/pref_entries_pose_detector_performance_mode_fast</item>
+    <item>@string/pref_entries_pose_detector_performance_mode_accurate</item>
+  </string-array>
+
+  <string-array name="pref_entry_values_pose_detector_performance_mode">
+    <item>@string/pref_entry_values_pose_detector_performance_mode_fast</item>
+    <item>@string/pref_entry_values_pose_detector_performance_mode_accurate</item>
+  </string-array>
+</resources>

+ 14 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#4CAF50</color>
+    <color name="colorPrimaryDark">#388E3C</color>
+    <color name="colorAccent">#7C4DFF</color>
+
+    <color name="blue_grey_400">#78909C</color>
+    <color name="light_grey_400">#E6E6E6</color>
+    <color name="light_green_700">#689F38</color>
+    <color name="gray">#BFBFBF</color>
+    <color name="white">#FFFFFF</color>
+    <color name="blue">#4286f4</color>
+    <color name="red">#f44242</color>
+</resources>

+ 6 - 0
app/src/main/res/values/dimens.xml

@@ -0,0 +1,6 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="padding_standard">10dp</dimen>
+</resources>

+ 116 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,116 @@
+<?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>
+
+    <!-- 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>
+
+</resources>

+ 3 - 0
app/src/main/res/values/styles.xml

@@ -0,0 +1,3 @@
+<resources>
+    <style name="AppTheme" parent="Theme.AppCompat.NoActionBar"/>
+</resources>

+ 121 - 0
app/src/main/res/xml/preference_live_preview_quickstart.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <PreferenceCategory
+      android:enabled="true"
+      android:key="@string/pref_category_key_camera"
+      android:title="@string/pref_category_title_camera">
+
+    <ListPreference
+        android:key="@string/pref_key_rear_camera_preview_size"
+        android:persistent="true"
+        android:title="@string/pref_title_rear_camera_preview_size"/>
+
+    <ListPreference
+        android:key="@string/pref_key_front_camera_preview_size"
+        android:persistent="true"
+        android:title="@string/pref_title_front_camera_preview_size"/>
+
+    <ListPreference
+        android:key="@string/pref_key_camerax_rear_camera_target_resolution"
+        android:persistent="true"
+        android:title="@string/pref_title_camerax_rear_camera_target_resolution"/>
+
+        <ListPreference
+        android:key="@string/pref_key_camerax_front_camera_target_resolution"
+        android:persistent="true"
+        android:title="@string/pref_title_camerax_front_camera_target_resolution"/>
+
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_camera_live_viewport"
+        android:persistent="true"
+        android:summary="@string/pref_summary_camera_live_viewport"
+        android:title="@string/pref_title_camera_live_viewport"/>
+
+  </PreferenceCategory>
+
+  <PreferenceCategory
+      android:title="@string/pref_category_object_detection">
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_live_preview_object_detector_enable_multiple_objects"
+        android:persistent="true"
+        android:title="@string/pref_title_object_detector_enable_multiple_objects"/>
+
+    <SwitchPreference
+        android:defaultValue="true"
+        android:key="@string/pref_key_live_preview_object_detector_enable_classification"
+        android:persistent="true"
+        android:title="@string/pref_title_object_detector_enable_classification"/>
+
+  </PreferenceCategory>
+
+  <PreferenceCategory
+      android:title="@string/pref_category_face_detection">
+
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_face_detector_landmark_mode_no_landmarks"
+        android:entries="@array/pref_entries_face_detector_landmark_mode"
+        android:entryValues="@array/pref_entry_values_face_detector_landmark_mode"
+        android:key="@string/pref_key_live_preview_face_detection_landmark_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_landmark_mode"/>
+
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_face_detector_contour_mode_all_contours"
+        android:entries="@array/pref_entries_face_detector_contour_mode"
+        android:entryValues="@array/pref_entry_values_face_detector_contour_mode"
+        android:key="@string/pref_key_live_preview_face_detection_contour_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_contour_mode"/>
+
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_face_detector_classification_mode_no_classifications"
+        android:entries="@array/pref_entries_face_detector_classification_mode"
+        android:entryValues="@array/pref_entry_values_face_detector_classification_mode"
+        android:key="@string/pref_key_live_preview_face_detection_classification_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_classification_mode"/>
+
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_face_detector_performance_mode_fast"
+        android:entries="@array/pref_entries_face_detector_performance_mode"
+        android:entryValues="@array/pref_entry_values_face_detector_performance_mode"
+        android:key="@string/pref_key_live_preview_face_detection_performance_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_performance_mode"/>
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_live_preview_face_detection_face_tracking"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_face_tracking"/>
+
+    <EditTextPreference
+        android:defaultValue="0.1"
+        android:dialogMessage="@string/pref_dialog_message_face_detector_min_face_size"
+        android:key="@string/pref_key_live_preview_face_detection_min_face_size"
+        android:persistent="true"
+        android:title="@string/pref_title_face_detector_min_face_size"/>
+  </PreferenceCategory>
+
+  <PreferenceCategory android:title="@string/pref_category_pose_detection">
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_pose_detector_performance_mode_fast"
+        android:entries="@array/pref_entries_values_pose_detector_performance_mode"
+        android:entryValues="@array/pref_entry_values_pose_detector_performance_mode"
+        android:key="@string/pref_key_live_preview_pose_detection_performance_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_pose_detector_performance_mode"
+        android:summary="%s"/>
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_live_preview_pose_detector_show_in_frame_likelihood"
+        android:persistent="true"
+        android:title="@string/pref_title_pose_detector_show_in_frame_likelihood"/>
+  </PreferenceCategory>
+</PreferenceScreen>

+ 37 - 0
app/src/main/res/xml/preference_still_image.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <PreferenceCategory
+      android:title="@string/pref_category_object_detection">
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_still_image_object_detector_enable_multiple_objects"
+        android:persistent="true"
+        android:title="@string/pref_title_object_detector_enable_multiple_objects"/>
+
+    <SwitchPreference
+        android:defaultValue="true"
+        android:key="@string/pref_key_still_image_object_detector_enable_classification"
+        android:persistent="true"
+        android:title="@string/pref_title_object_detector_enable_classification"/>
+
+  </PreferenceCategory>
+
+  <PreferenceCategory android:title="@string/pref_category_pose_detection">
+    <ListPreference
+        android:defaultValue="@string/pref_entry_values_pose_detector_performance_mode_fast"
+        android:entries="@array/pref_entries_values_pose_detector_performance_mode"
+        android:entryValues="@array/pref_entry_values_pose_detector_performance_mode"
+        android:key="@string/pref_key_still_image_pose_detection_performance_mode"
+        android:persistent="true"
+        android:title="@string/pref_title_pose_detector_performance_mode"
+        android:summary="%s"/>
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="@string/pref_key_still_image_pose_detector_show_in_frame_likelihood"
+        android:persistent="true"
+        android:title="@string/pref_title_pose_detector_show_in_frame_likelihood"/>
+  </PreferenceCategory>
+</PreferenceScreen>

+ 29 - 0
build.gradle

@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+    repositories {
+        mavenLocal()
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.1'
+        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        mavenLocal()
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 20 - 0
gradle.properties

@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Wed Jan 06 14:48:40 PST 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 8 - 0
local.properties

@@ -0,0 +1,8 @@
+## This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Thu Jan 07 15:20:50 CST 2021
+sdk.dir=E\:\\software\\Android\\newsdk

+ 2 - 0
settings.gradle

@@ -0,0 +1,2 @@
+rootProject.name='ML Kit Vision Quickstart'
+include ':app'