diff --git a/translate/final/app/build.gradle b/translate/final/app/build.gradle
new file mode 100644
index 0000000..8135786
--- /dev/null
+++ b/translate/final/app/build.gradle
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019 Google Inc. 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.
+ *
+ */
+
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.2"
+ defaultConfig {
+ applicationId "com.google.mlkit.codelab.translate"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.3.0'
+ implementation 'androidx.fragment:fragment-ktx:1.2.4'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ // Add CameraX dependencies
+ def camerax_version = "1.0.0-beta10"
+ implementation "androidx.camera:camera-core:${camerax_version}"
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
+ implementation "androidx.camera:camera-view:1.0.0-alpha17"
+
+ // Add ML Kit dependencies
+ implementation 'com.google.android.gms:play-services-mlkit-text-recognition:16.1.1'
+ implementation 'com.google.mlkit:language-id:16.1.1'
+ implementation 'com.google.mlkit:translate:16.1.1'
+}
diff --git a/translate/final/app/proguard-rules.pro b/translate/final/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/translate/final/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/translate/final/app/src/main/AndroidManifest.xml b/translate/final/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8ed4db2
--- /dev/null
+++ b/translate/final/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/MainActivity.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/MainActivity.kt
new file mode 100644
index 0000000..006e0cb
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/MainActivity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 Google Inc. 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.codelab.translate
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import com.google.mlkit.codelab.translate.main.MainFragment
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_activity)
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.container, MainFragment.newInstance())
+ .commitNow()
+ }
+ }
+
+}
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/analyzer/TextAnalyzer.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/analyzer/TextAnalyzer.kt
new file mode 100644
index 0000000..07a875b
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/analyzer/TextAnalyzer.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2019 Google Inc. 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.codelab.translate.analyzer
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.Log
+import android.widget.Toast
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageProxy
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.MutableLiveData
+import com.google.android.gms.tasks.Task
+import com.google.mlkit.common.MlKitException
+import com.google.mlkit.codelab.translate.util.ImageUtils
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.text.Text
+import com.google.mlkit.vision.text.TextRecognition
+import java.lang.Exception
+
+/**
+ * Analyzes the frames passed in from the camera and returns any detected text within the requested
+ * crop region.
+ */
+class TextAnalyzer(
+ private val context: Context,
+ private val lifecycle: Lifecycle,
+ private val result: MutableLiveData,
+ private val imageCropPercentages: MutableLiveData>
+) : ImageAnalysis.Analyzer {
+
+ // TODO: Instantiate TextRecognition detector
+
+ // TODO: Add lifecycle observer to properly close ML Kit detectors
+
+ @androidx.camera.core.ExperimentalGetImage
+ override fun analyze(imageProxy: ImageProxy) {
+ val mediaImage = imageProxy.image ?: return
+
+ val rotationDegrees = imageProxy.imageInfo.rotationDegrees
+
+ // We requested a setTargetAspectRatio, but it's not guaranteed that's what the camera
+ // stack is able to support, so we calculate the actual ratio from the first frame to
+ // know how to appropriately crop the image we want to analyze.
+ val imageHeight = mediaImage.height
+ val imageWidth = mediaImage.width
+
+ val actualAspectRatio = imageWidth / imageHeight
+
+ val convertImageToBitmap = ImageUtils.convertYuv420888ImageToBitmap(mediaImage)
+ val cropRect = Rect(0, 0, imageWidth, imageHeight)
+
+ // If the image has a way wider aspect ratio than expected, crop less of the height so we
+ // don't end up cropping too much of the image. If the image has a way taller aspect ratio
+ // than expected, we don't have to make any changes to our cropping so we don't handle it
+ // here.
+ val currentCropPercentages = imageCropPercentages.value ?: return
+ if (actualAspectRatio > 3) {
+ val originalHeightCropPercentage = currentCropPercentages.first
+ val originalWidthCropPercentage = currentCropPercentages.second
+ imageCropPercentages.value =
+ Pair(originalHeightCropPercentage / 2, originalWidthCropPercentage)
+ }
+
+ // If the image is rotated by 90 (or 270) degrees, swap height and width when calculating
+ // the crop.
+ val cropPercentages = imageCropPercentages.value ?: return
+ val heightCropPercent = cropPercentages.first
+ val widthCropPercent = cropPercentages.second
+ val (widthCrop, heightCrop) = when (rotationDegrees) {
+ 90, 270 -> Pair(heightCropPercent / 100f, widthCropPercent / 100f)
+ else -> Pair(widthCropPercent / 100f, heightCropPercent / 100f)
+ }
+
+ cropRect.inset(
+ (imageWidth * widthCrop / 2).toInt(),
+ (imageHeight * heightCrop / 2).toInt()
+ )
+ val croppedBitmap =
+ ImageUtils.rotateAndCrop(convertImageToBitmap, rotationDegrees, cropRect)
+
+ // TODO call recognizeText() once implemented
+ }
+
+ fun recognizeText() {
+ // TODO Use ML Kit's TextRecognition to analyze frames from the camera live feed.
+ }
+
+ private fun getErrorMessage(exception: Exception): String? {
+ val mlKitException = exception as? MlKitException ?: return exception.message
+ return if (mlKitException.errorCode == MlKitException.UNAVAILABLE) {
+ "Waiting for text recognition model to be downloaded"
+ } else exception.message
+ }
+
+ companion object {
+ private const val TAG = "TextAnalyzer"
+ }
+}
\ No newline at end of file
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainFragment.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainFragment.kt
new file mode 100644
index 0000000..c7390ee
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainFragment.kt
@@ -0,0 +1,366 @@
+/*
+ * Copyright 2020 Google Inc. 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.codelab.translate.main
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.graphics.*
+import android.os.Bundle
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.SurfaceHolder
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.camera.core.*
+import androidx.camera.core.Camera
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import com.google.mlkit.codelab.translate.R
+import com.google.mlkit.codelab.translate.analyzer.TextAnalyzer
+import com.google.mlkit.codelab.translate.util.Language
+import com.google.mlkit.codelab.translate.util.ScopedExecutor
+import kotlinx.android.synthetic.main.main_fragment.*
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import kotlin.math.abs
+import kotlin.math.ln
+import kotlin.math.max
+import kotlin.math.min
+
+class MainFragment : Fragment() {
+
+ companion object {
+ fun newInstance() = MainFragment()
+
+ // We only need to analyze the part of the image that has text, so we set crop percentages
+ // to avoid analyze the entire image from the live camera feed.
+ const val DESIRED_WIDTH_CROP_PERCENT = 8
+ const val DESIRED_HEIGHT_CROP_PERCENT = 74
+
+ // This is an arbitrary number we are using to keep tab of the permission
+ // request. Where an app has multiple context for requesting permission,
+ // this can help differentiate the different contexts
+ private const val REQUEST_CODE_PERMISSIONS = 10
+
+ // This is an array of all the permission specified in the manifest
+ private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
+ private const val RATIO_4_3_VALUE = 4.0 / 3.0
+ private const val RATIO_16_9_VALUE = 16.0 / 9.0
+ private const val TAG = "MainFragment"
+ }
+
+ private var displayId: Int = -1
+ private val viewModel: MainViewModel by viewModels()
+ private var cameraProvider: ProcessCameraProvider? = null
+ private var camera: Camera? = null
+ private var imageAnalyzer: ImageAnalysis? = null
+ private lateinit var container: ConstraintLayout
+ private lateinit var viewFinder: PreviewView
+
+ /** Blocking camera operations are performed using this executor */
+ private lateinit var cameraExecutor: ExecutorService
+
+ private lateinit var scopedExecutor: ScopedExecutor
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return inflater.inflate(R.layout.main_fragment, container, false)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+
+ // Shut down our background executor
+ cameraExecutor.shutdown()
+ scopedExecutor.shutdown()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ container = view as ConstraintLayout
+ viewFinder = container.findViewById(R.id.viewfinder)
+
+ // Initialize our background executor
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ scopedExecutor = ScopedExecutor(cameraExecutor)
+
+ // Request camera permissions
+ if (allPermissionsGranted()) {
+ // Wait for the views to be properly laid out
+ viewFinder.post {
+ // Keep track of the display in which this view is attached
+ displayId = viewFinder.display.displayId
+
+ // Set up the camera and its use cases
+ setUpCamera()
+ }
+ } else {
+ requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
+ }
+
+ // Get available language list and set up the target language spinner
+ // with default selections.
+ val adapter = ArrayAdapter(
+ requireContext(),
+ android.R.layout.simple_spinner_dropdown_item, viewModel.availableLanguages
+ )
+
+ targetLangSelector.adapter = adapter
+ targetLangSelector.setSelection(adapter.getPosition(Language("en")))
+ targetLangSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(
+ parent: AdapterView<*>,
+ view: View?,
+ position: Int,
+ id: Long
+ ) {
+ viewModel.targetLang.value = adapter.getItem(position)
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>) {}
+ }
+
+ viewModel.sourceLang.observe(viewLifecycleOwner, Observer { srcLang.text = it.displayName })
+ viewModel.translatedText.observe(viewLifecycleOwner, Observer { resultOrError ->
+ resultOrError?.let {
+ if (it.error != null) {
+ translatedText.error = resultOrError.error?.localizedMessage
+ } else {
+ translatedText.text = resultOrError.result
+ }
+ }
+ })
+ viewModel.modelDownloading.observe(viewLifecycleOwner, Observer { isDownloading ->
+ progressBar.visibility = if (isDownloading) {
+ View.VISIBLE
+ } else {
+ View.INVISIBLE
+ }
+ progressText.visibility = progressBar.visibility
+ })
+
+ overlay.apply {
+ setZOrderOnTop(true)
+ holder.setFormat(PixelFormat.TRANSPARENT)
+ holder.addCallback(object : SurfaceHolder.Callback {
+ override fun surfaceChanged(
+ holder: SurfaceHolder?,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder?) {
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder?) {
+ holder?.let { drawOverlay(it, DESIRED_HEIGHT_CROP_PERCENT, DESIRED_WIDTH_CROP_PERCENT) }
+ }
+
+ })
+ }
+ }
+
+
+ /** Initialize CameraX, and prepare to bind the camera use cases */
+ private fun setUpCamera() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
+ cameraProviderFuture.addListener(Runnable {
+
+ // CameraProvider
+ cameraProvider = cameraProviderFuture.get()
+
+ // Build and bind the camera use cases
+ bindCameraUseCases()
+ }, ContextCompat.getMainExecutor(requireContext()))
+ }
+
+ private fun bindCameraUseCases() {
+ val cameraProvider = cameraProvider
+ ?: throw IllegalStateException("Camera initialization failed.")
+
+ // Get screen metrics used to setup camera for full screen resolution
+ val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
+ Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
+
+ val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
+ Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
+
+ val rotation = viewFinder.display.rotation
+
+ val preview = Preview.Builder()
+ .setTargetAspectRatio(screenAspectRatio)
+ .setTargetRotation(rotation)
+ .build()
+
+ // Build the image analysis use case and instantiate our analyzer
+ imageAnalyzer = ImageAnalysis.Builder()
+ // We request aspect ratio but no resolution
+ .setTargetAspectRatio(screenAspectRatio)
+ .setTargetRotation(rotation)
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build()
+ .also {
+ it.setAnalyzer(
+ cameraExecutor
+ , TextAnalyzer(
+ requireContext(),
+ lifecycle,
+ viewModel.sourceText,
+ viewModel.imageCropPercentages
+ )
+ )
+ }
+ viewModel.sourceText.observe(viewLifecycleOwner, Observer { srcText.text = it })
+ viewModel.imageCropPercentages.observe(viewLifecycleOwner,
+ Observer { drawOverlay(overlay.holder, it.first, it.second) })
+
+ // Select back camera since text detection does not work with front camera
+ val cameraSelector =
+ CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
+
+ try {
+ // Unbind use cases before rebinding
+ cameraProvider.unbindAll()
+
+ // Bind use cases to camera
+ camera = cameraProvider.bindToLifecycle(
+ this, cameraSelector, preview, imageAnalyzer
+ )
+ preview.setSurfaceProvider(viewFinder.surfaceProvider)
+ } catch (exc: IllegalStateException) {
+ Log.e(TAG, "Use case binding failed. This must be running on main thread.", exc)
+ }
+ }
+
+ private fun drawOverlay(
+ holder: SurfaceHolder,
+ heightCropPercent: Int,
+ widthCropPercent: Int
+ ) {
+ val canvas = holder.lockCanvas()
+ val bgPaint = Paint().apply {
+ alpha = 140
+ }
+ canvas.drawPaint(bgPaint)
+ val rectPaint = Paint()
+ rectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+ rectPaint.style = Paint.Style.FILL
+ rectPaint.color = Color.WHITE
+ val outlinePaint = Paint()
+ outlinePaint.style = Paint.Style.STROKE
+ outlinePaint.color = Color.WHITE
+ outlinePaint.strokeWidth = 4f
+ val surfaceWidth = holder.surfaceFrame.width()
+ val surfaceHeight = holder.surfaceFrame.height()
+
+ val cornerRadius = 25f
+ // Set rect centered in frame
+ val rectTop = surfaceHeight * heightCropPercent / 2 / 100f
+ val rectLeft = surfaceWidth * widthCropPercent / 2 / 100f
+ val rectRight = surfaceWidth * (1 - widthCropPercent / 2 / 100f)
+ val rectBottom = surfaceHeight * (1 - heightCropPercent / 2 / 100f)
+ val rect = RectF(rectLeft, rectTop, rectRight, rectBottom)
+ canvas.drawRoundRect(
+ rect, cornerRadius, cornerRadius, rectPaint
+ )
+ canvas.drawRoundRect(
+ rect, cornerRadius, cornerRadius, outlinePaint
+ )
+ val textPaint = Paint()
+ textPaint.color = Color.WHITE
+ textPaint.textSize = 50F
+
+ val overlayText = getString(R.string.overlay_help)
+ val textBounds = Rect()
+ textPaint.getTextBounds(overlayText, 0, overlayText.length, textBounds)
+ val textX = (surfaceWidth - textBounds.width()) / 2f
+ val textY = rectBottom + textBounds.height() + 15f // put text below rect and 15f padding
+ canvas.drawText(getString(R.string.overlay_help), textX, textY, textPaint)
+ holder.unlockCanvasAndPost(canvas)
+ }
+
+ /**
+ * [androidx.camera.core.ImageAnalysisConfig] requires enum value of
+ * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
+ *
+ * Detecting the most suitable ratio for dimensions provided in @params by comparing absolute
+ * of preview ratio to one of the provided values.
+ *
+ * @param width - preview width
+ * @param height - preview height
+ * @return suitable aspect ratio
+ */
+ private fun aspectRatio(width: Int, height: Int): Int {
+ val previewRatio = ln(max(width, height).toDouble() / min(width, height))
+ if (abs(previewRatio - ln(RATIO_4_3_VALUE))
+ <= abs(previewRatio - ln(RATIO_16_9_VALUE))
+ ) {
+ return AspectRatio.RATIO_4_3
+ }
+ return AspectRatio.RATIO_16_9
+ }
+
+ /**
+ * Process result from permission request dialog box, has the request
+ * been granted? If yes, start Camera. Otherwise display a toast
+ */
+ override fun onRequestPermissionsResult(
+ requestCode: Int, permissions: Array, grantResults: IntArray
+ ) {
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ viewFinder.post {
+ // Keep track of the display in which this view is attached
+ displayId = viewFinder.display.displayId
+
+ // Set up the camera and its use cases
+ setUpCamera()
+ }
+ } else {
+ Toast.makeText(
+ context,
+ "Permissions not granted by the user.",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ /**
+ * Check if all permission specified in the manifest have been granted
+ */
+ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(
+ requireContext(), it
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+}
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainViewModel.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainViewModel.kt
new file mode 100644
index 0000000..bf047c4
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/main/MainViewModel.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 Google Inc. 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.codelab.translate.main
+
+import android.app.Application
+import android.os.Handler
+import android.util.LruCache
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.android.gms.tasks.Task
+import com.google.android.gms.tasks.Tasks
+import com.google.mlkit.codelab.translate.util.Language
+import com.google.mlkit.codelab.translate.util.ResultOrError
+import com.google.mlkit.codelab.translate.util.SmoothedMutableLiveData
+import com.google.mlkit.nl.languageid.LanguageIdentification
+import com.google.mlkit.nl.translate.TranslateLanguage
+import com.google.mlkit.nl.translate.Translation
+import com.google.mlkit.nl.translate.Translator
+import com.google.mlkit.nl.translate.TranslatorOptions
+import com.google.mlkit.codelab.translate.main.MainFragment.Companion.DESIRED_HEIGHT_CROP_PERCENT
+import com.google.mlkit.codelab.translate.main.MainFragment.Companion.DESIRED_WIDTH_CROP_PERCENT
+
+class MainViewModel(application: Application) : AndroidViewModel(application) {
+
+ // TODO Instantiate LanguageIdentification
+ val targetLang = MutableLiveData()
+ val sourceText = SmoothedMutableLiveData(SMOOTHING_DURATION)
+
+ // We set desired crop percentages to avoid having to analyze the whole image from the live
+ // camera feed. However, we are not guaranteed what aspect ratio we will get from the camera, so
+ // we use the first frame we get back from the camera to update these crop percentages based on
+ // the actual aspect ratio of images.
+ val imageCropPercentages = MutableLiveData>()
+ .apply { value = Pair(DESIRED_HEIGHT_CROP_PERCENT, DESIRED_WIDTH_CROP_PERCENT) }
+ val translatedText = MediatorLiveData()
+ private val translating = MutableLiveData()
+ val modelDownloading = SmoothedMutableLiveData(SMOOTHING_DURATION)
+
+ private var modelDownloadTask: Task = Tasks.forCanceled()
+
+ private val translators =
+ object : LruCache(NUM_TRANSLATORS) {
+ override fun create(options: TranslatorOptions): Translator {
+ return Translation.getClient(options)
+ }
+
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: TranslatorOptions,
+ oldValue: Translator,
+ newValue: Translator?
+ ) {
+ oldValue.close()
+ }
+ }
+
+ val sourceLang = Transformations.switchMap(sourceText) { text ->
+ val result = MutableLiveData()
+ // TODO Call the language identification method and assigns the result if it is not
+ // undefined (“und”)
+ result
+ }
+
+ override fun onCleared() {
+ // TODO Shut down ML Kit clients.
+ }
+
+ private fun translate(): Task {
+ // TODO Take the source language value, target language value, and the source text and
+ // perform the translation.
+ // If the chosen target language model has not been downloaded to the device yet,
+ // call downloadModelIfNeeded() and then proceed with the translation.
+ return Tasks.forResult("") // replace this with your code
+ }
+
+ // Gets a list of all available translation languages.
+ val availableLanguages: List = TranslateLanguage.getAllLanguages()
+ .map { Language(it) }
+
+ init {
+ modelDownloading.setValue(false)
+ translating.value = false
+ // Create a translation result or error object.
+ val processTranslation =
+ OnCompleteListener { task ->
+ if (task.isSuccessful) {
+ translatedText.value = ResultOrError(task.result, null)
+ } else {
+ if (task.isCanceled) {
+ // Tasks are cancelled for reasons such as gating; ignore.
+ return@OnCompleteListener
+ }
+ translatedText.value = ResultOrError(null, task.exception)
+ }
+ }
+ // Start translation if any of the following change: detected text, source lang, target lang.
+ translatedText.addSource(sourceText) { translate().addOnCompleteListener(processTranslation) }
+ translatedText.addSource(sourceLang) { translate().addOnCompleteListener(processTranslation) }
+ translatedText.addSource(targetLang) { translate().addOnCompleteListener(processTranslation) }
+ }
+
+ companion object {
+ // Amount of time (in milliseconds) to wait for detected text to settle
+ private const val SMOOTHING_DURATION = 50L
+
+ private const val NUM_TRANSLATORS = 1
+ }
+}
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ImageUtils.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ImageUtils.kt
new file mode 100644
index 0000000..1c28187
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ImageUtils.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 Google Inc. 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.codelab.translate.util
+
+import android.graphics.Bitmap
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.media.Image
+import androidx.annotation.ColorInt
+
+/**
+ * Utility class for manipulating images.
+ */
+object ImageUtils {
+ private val CHANNEL_RANGE = 0 until (1 shl 18)
+
+ fun convertYuv420888ImageToBitmap(image: Image): Bitmap {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Unsupported image format $(image.format)"
+ }
+
+ val planes = image.planes
+
+ // Because of the variable row stride it's not possible to know in
+ // advance the actual necessary dimensions of the yuv planes.
+ val yuvBytes = planes.map { plane ->
+ val buffer = plane.buffer
+ val yuvBytes = ByteArray(buffer.capacity())
+ buffer[yuvBytes]
+ buffer.rewind() // Be kind…
+ yuvBytes
+ }
+
+ val yRowStride = planes[0].rowStride
+ val uvRowStride = planes[1].rowStride
+ val uvPixelStride = planes[1].pixelStride
+ val width = image.width
+ val height = image.height
+ @ColorInt val argb8888 = IntArray(width * height)
+ var i = 0
+ for (y in 0 until height) {
+ val pY = yRowStride * y
+ val uvRowStart = uvRowStride * (y shr 1)
+ for (x in 0 until width) {
+ val uvOffset = (x shr 1) * uvPixelStride
+ argb8888[i++] =
+ yuvToRgb(
+ yuvBytes[0][pY + x].toIntUnsigned(),
+ yuvBytes[1][uvRowStart + uvOffset].toIntUnsigned(),
+ yuvBytes[2][uvRowStart + uvOffset].toIntUnsigned()
+ )
+ }
+ }
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ bitmap.setPixels(argb8888, 0, width, 0, 0, width, height)
+ return bitmap
+ }
+
+ fun rotateAndCrop(
+ bitmap: Bitmap,
+ imageRotationDegrees: Int,
+ cropRect: Rect
+ ): Bitmap {
+ val matrix = Matrix()
+ matrix.preRotate(imageRotationDegrees.toFloat())
+ return Bitmap.createBitmap(
+ bitmap,
+ cropRect.left,
+ cropRect.top,
+ cropRect.width(),
+ cropRect.height(),
+ matrix,
+ true
+ )
+ }
+
+ @ColorInt
+ private fun yuvToRgb(nY: Int, nU: Int, nV: Int): Int {
+ var nY = nY
+ var nU = nU
+ var nV = nV
+ nY -= 16
+ nU -= 128
+ nV -= 128
+ nY = nY.coerceAtLeast(0)
+
+ // This is the floating point equivalent. We do the conversion in integer
+ // because some Android devices do not have floating point in hardware.
+ // nR = (int)(1.164 * nY + 2.018 * nU);
+ // nG = (int)(1.164 * nY - 0.813 * nV - 0.391 * nU);
+ // nB = (int)(1.164 * nY + 1.596 * nV);
+ var nR = 1192 * nY + 1634 * nV
+ var nG = 1192 * nY - 833 * nV - 400 * nU
+ var nB = 1192 * nY + 2066 * nU
+
+ // Clamp the values before normalizing them to 8 bits.
+ nR = nR.coerceIn(CHANNEL_RANGE) shr 10 and 0xff
+ nG = nG.coerceIn(CHANNEL_RANGE) shr 10 and 0xff
+ nB = nB.coerceIn(CHANNEL_RANGE) shr 10 and 0xff
+ return -0x1000000 or (nR shl 16) or (nG shl 8) or nB
+ }
+}
+
+private fun Byte.toIntUnsigned(): Int {
+ return toInt() and 0xFF
+}
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/Language.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/Language.kt
new file mode 100644
index 0000000..628c573
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/Language.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 Google Inc. 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.codelab.translate.util
+
+import java.util.*
+
+/**
+ * Holds the language code (i.e. "en") and the corresponding localized full language name
+ * (i.e. "English")
+ */
+class Language(val code: String) : Comparable {
+
+ val displayName: String
+ get() = Locale(code).displayName
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+
+ if (other !is Language) {
+ return false
+ }
+
+ val otherLang = other as Language?
+ return otherLang!!.code == code
+ }
+
+ override fun toString(): String {
+ return displayName
+ }
+
+ override fun compareTo(other: Language): Int {
+ return this.displayName.compareTo(other.displayName)
+ }
+
+ override fun hashCode(): Int {
+ return code.hashCode()
+ }
+}
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ResultOrError.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ResultOrError.kt
new file mode 100644
index 0000000..553acc1
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ResultOrError.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019 Google Inc. 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.codelab.translate.util
+
+/**
+ * Holds a result or some operation or the exception.
+ */
+class ResultOrError(var result: String?, var error: Exception?)
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ScopedExecutor.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ScopedExecutor.kt
new file mode 100644
index 0000000..f514858
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/ScopedExecutor.kt
@@ -0,0 +1,19 @@
+package com.google.mlkit.codelab.translate.util
+
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicBoolean
+
+class ScopedExecutor(private val executor: Executor) : Executor {
+
+ private val isShutdown = AtomicBoolean()
+
+ fun shutdown() {
+ isShutdown.set(true)
+ }
+
+ override fun execute(command: Runnable) {
+ executor.execute {
+ if (!isShutdown.get()) command.run()
+ }
+ }
+}
\ No newline at end of file
diff --git a/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/SmoothedMutableLiveData.kt b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/SmoothedMutableLiveData.kt
new file mode 100644
index 0000000..f7179bf
--- /dev/null
+++ b/translate/final/app/src/main/java/com/google/mlkit/codelab/translate/util/SmoothedMutableLiveData.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 Google Inc. 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.codelab.translate.util
+
+import android.os.Handler
+import androidx.lifecycle.MutableLiveData
+
+/**
+ * A {@link MutableLiveData} that only emits change events when the underlying data has been stable
+ * for the configured amount of time.
+ *
+ * @param duration time delay to wait in milliseconds
+ */
+class SmoothedMutableLiveData(private val duration: Long) : MutableLiveData() {
+ private var pendingValue: T? = null
+ private val runnable = Runnable {
+ super.setValue(pendingValue)
+ }
+
+ override fun setValue(value: T) {
+ if (value != pendingValue) {
+ pendingValue = value
+ Handler().removeCallbacks(runnable)
+ Handler().postDelayed(runnable, duration)
+ }
+ }
+}
diff --git a/translate/final/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/translate/final/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..93b7d80
--- /dev/null
+++ b/translate/final/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/translate/final/app/src/main/res/drawable/greyscale_regular_3x.png b/translate/final/app/src/main/res/drawable/greyscale_regular_3x.png
new file mode 100644
index 0000000..1a1f571
Binary files /dev/null and b/translate/final/app/src/main/res/drawable/greyscale_regular_3x.png differ
diff --git a/translate/final/app/src/main/res/drawable/ic_launcher_background.xml b/translate/final/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..5408240
--- /dev/null
+++ b/translate/final/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/translate/final/app/src/main/res/font/pt_sans.ttf b/translate/final/app/src/main/res/font/pt_sans.ttf
new file mode 100644
index 0000000..83a21b7
Binary files /dev/null and b/translate/final/app/src/main/res/font/pt_sans.ttf differ
diff --git a/translate/final/app/src/main/res/layout/main_activity.xml b/translate/final/app/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000..127c258
--- /dev/null
+++ b/translate/final/app/src/main/res/layout/main_activity.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/translate/final/app/src/main/res/layout/main_fragment.xml b/translate/final/app/src/main/res/layout/main_fragment.xml
new file mode 100644
index 0000000..c266ae3
--- /dev/null
+++ b/translate/final/app/src/main/res/layout/main_fragment.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..2bef0f7
--- /dev/null
+++ b/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..2bef0f7
--- /dev/null
+++ b/translate/final/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher.png b/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher.png b/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
Binary files /dev/null and b/translate/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/translate/final/app/src/main/res/values/colors.xml b/translate/final/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..1cf589b
--- /dev/null
+++ b/translate/final/app/src/main/res/values/colors.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/translate/final/app/src/main/res/values/strings.xml b/translate/final/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1cb9e22
--- /dev/null
+++ b/translate/final/app/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+
+
+ ML Kit Translate Codelab
+ Unknown error occurred.
+ Google Translate attribution
+ Center text in box
+ Downloading model files...
+
diff --git a/translate/final/app/src/main/res/values/styles.xml b/translate/final/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fbd2516
--- /dev/null
+++ b/translate/final/app/src/main/res/values/styles.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/translate/final/build.gradle b/translate/final/build.gradle
new file mode 100644
index 0000000..a7b59eb
--- /dev/null
+++ b/translate/final/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 Google Inc. 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.
+ *
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/translate/final/gradle.properties b/translate/final/gradle.properties
new file mode 100644
index 0000000..b5e9f60
--- /dev/null
+++ b/translate/final/gradle.properties
@@ -0,0 +1,38 @@
+#
+# Copyright 2019 Google Inc. 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.
+#
+#
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# 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
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/translate/final/gradle/wrapper/gradle-wrapper.jar b/translate/final/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/translate/final/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/translate/final/gradle/wrapper/gradle-wrapper.properties b/translate/final/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..978d878
--- /dev/null
+++ b/translate/final/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,23 @@
+#
+# Copyright 2019 Google Inc. 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.
+#
+#
+
+#Wed Sep 18 03:11:32 EDT 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/translate/final/gradlew b/translate/final/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/translate/final/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/translate/final/gradlew.bat b/translate/final/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/translate/final/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/translate/final/settings.gradle b/translate/final/settings.gradle
new file mode 100644
index 0000000..8178fd4
--- /dev/null
+++ b/translate/final/settings.gradle
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2019 Google Inc. 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.
+ *
+ */
+
+include ':app'
+rootProject.name='ML Kit Translate Codelab'