diff --git a/android/translate/LICENSE b/android/translate/LICENSE new file mode 100644 index 0000000000..973b3b7670 --- /dev/null +++ b/android/translate/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 diff --git a/android/translate/app/build.gradle b/android/translate/app/build.gradle index abb97902ba..af34cf7da5 100644 --- a/android/translate/app/build.gradle +++ b/android/translate/app/build.gradle @@ -1,59 +1,43 @@ -/* - * 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 + compileSdkVersion 30 defaultConfig { - applicationId "com.google.mlkit.samples.translate" + applicationId 'com.google.mlkit.samples.nl.translate' + // The SDK only requires minSdkVersion 16. We are using minSdkVersion + // 21 to make some code in the sample shorter. minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 + multiDexEnabled true versionCode 1 versionName "1.0" + setProperty("archivesBaseName", "mlkit_translate_sample") } + buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation project(':internal:chooserx') - implementation project(":internal:lintchecks") - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72" + implementation 'com.google.mlkit:translate:16.1.2' - implementation 'androidx.media:media:1.1.0' + // Those dependencies are not required by the SDK. They are used for the sample itself. + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.multidex:multidex:2.0.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'org.apache.commons:commons-collections4:4.4' - implementation 'com.google.guava:guava:27.1-android' - - implementation 'com.google.mlkit:translate:16.1.1' - - testImplementation 'junit:junit:4.13' + implementation 'com.google.guava:guava:24.1-android' } diff --git a/android/translate/app/src/main/AndroidManifest.xml b/android/translate/app/src/main/AndroidManifest.xml index b8838e4bbb..ff7a611fdf 100644 --- a/android/translate/app/src/main/AndroidManifest.xml +++ b/android/translate/app/src/main/AndroidManifest.xml @@ -1,47 +1,50 @@ - - - - - - - - - - - - - - - - - - - - - - + package="com.google.mlkit.samples.nl.translate" + android:versionCode="1" + android:versionName="v1"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/EntryChoiceActivity.kt b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/EntryChoiceActivity.kt new file mode 100644 index 0000000000..1b20371867 --- /dev/null +++ b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/EntryChoiceActivity.kt @@ -0,0 +1,25 @@ +package com.google.mlkit.samples.nl.translate + +import android.content.Intent +import com.google.mlkit.samples.nl.translate.java.MainActivityJava +import com.google.mlkit.samples.nl.translate.kotlin.MainActivityKotlin +import com.mlkit.example.internal.BaseEntryChoiceActivity +import com.mlkit.example.internal.Choice + +class EntryChoiceActivity : BaseEntryChoiceActivity() { + + override fun getChoices(): List { + return listOf( + Choice( + "Java", + "Run the ML Kit Translate quickstart written in Java.", + Intent(this, MainActivityJava::class.java) + ), + Choice( + "Kotlin", + "Run the ML Kit Translate quickstart written in Kotlin.", + Intent(this, MainActivityKotlin::class.java) + ) + ) + } +} diff --git a/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/MainActivityJava.java b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/MainActivityJava.java new file mode 100644 index 0000000000..ff22f34a41 --- /dev/null +++ b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/MainActivityJava.java @@ -0,0 +1,207 @@ +package com.google.mlkit.samples.nl.translate.java; + +import android.app.ProgressDialog; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.util.LruCache; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import com.google.common.collect.ImmutableList; +import com.google.mlkit.nl.translate.TranslateLanguage; +import com.google.mlkit.nl.translate.TranslateLanguage.Language; +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.samples.nl.translate.R; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +public class MainActivityJava extends AppCompatActivity { + + private static final String TAG = MainActivityJava.class.getCanonicalName(); + /** + * This specifies the number of translators instance we want to keep in our LRU cache. Each + * instance of the translator is built with different options based on the source language and the + * target language, and since we want to be able to manage the number of translator instances to + * keep around, an LRU cache is an easy way to achieve this. + */ + private static final int NUM_TRANSLATORS = 3; + + /** Current translatorOptions for the selected source and target languages */ + private TranslatorOptions translatorOptions; + + private final LruCache translatorsCache = + new LruCache(NUM_TRANSLATORS) { + @Override + public Translator create(TranslatorOptions options) { + return Translation.getClient(options); + } + + @Override + public void entryRemoved( + boolean evicted, TranslatorOptions key, Translator oldValue, Translator newValue) { + oldValue.close(); + } + }; + + /** Text box where the user types in their language */ + private EditText sourceBox; + /** Text box where translated text will appear */ + private TextView targetBox; + + private ImmutableList languages; + private Spinner sourceLanguageSpinner; + private Spinner targetLanguageSpinner; + + @Override + protected void onCreate(@Nullable Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.activity_main); + sourceBox = findViewById(R.id.sourceBox); + targetBox = findViewById(R.id.targetBox); + sourceLanguageSpinner = findViewById(R.id.sourceLanguageSpinner); + targetLanguageSpinner = findViewById(R.id.targetLanguageSpinner); + + sourceBox.setOnEditorActionListener( + (v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND) { + closeKeyboard(); + translate(v.getText().toString()); + return true; + } + return false; + }); + findViewById(R.id.translateButton) + .setOnClickListener(v -> translate(sourceBox.getText().toString())); + + List languageNames = buildLanguagesList(); + setupLanguageSpinner(sourceLanguageSpinner, languageNames, TranslateLanguage.GERMAN); + setupLanguageSpinner(targetLanguageSpinner, languageNames, TranslateLanguage.FRENCH); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_translate, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.model_management) { + startActivity(ModelManagementActivityJava.makeLaunchIntent(this)); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + translatorsCache.evictAll(); + super.onDestroy(); + } + + // This returns a list of language names (user readable) but also builds the `languages` field, + // with the list of TranslateLanguages. + private ImmutableList buildLanguagesList() { + List languageSet = TranslateLanguage.getAllLanguages(); + List locales = new ArrayList<>(languageSet.size()); + for (/* @Language */ String language : languageSet) { + locales.add(new Locale(language)); + } + Collections.sort( + locales, (l1, l2) -> l1.getDisplayLanguage().compareTo(l2.getDisplayLanguage())); + + ImmutableList.Builder languagesBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder languageNames = new ImmutableList.Builder<>(); + for (Locale locale : locales) { + languagesBuilder.add(locale.getLanguage()); + languageNames.add(locale.getDisplayLanguage()); + } + languages = languagesBuilder.build(); + + return languageNames.build(); + } + + private void setupLanguageSpinner( + Spinner spinner, List languageNames, @Language String defaultValue) { + spinner.setAdapter( + new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, languageNames)); + spinner.setOnItemSelectedListener( + new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + MainActivityJava.this.updateLanguages(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + spinner.setSelection(languages.indexOf(defaultValue)); + } + + private void updateLanguages() { + @Language + String sourceLanguage = + languages.get(Math.max(sourceLanguageSpinner.getSelectedItemPosition(), 0)); + @Language + String targetLanguage = + languages.get(Math.max(targetLanguageSpinner.getSelectedItemPosition(), 0)); + translatorOptions = + new TranslatorOptions.Builder() + .setSourceLanguage(sourceLanguage) + .setTargetLanguage(targetLanguage) + .build(); + } + + private void translate(String sourceString) { + final ProgressDialog dialog = + ProgressDialog.show( + this, + "Translate", + "Downloading Translate model", + /*indeterminate=*/ true, + /*cancelable=*/ false); + + translatorsCache + .get(translatorOptions) + .downloadModelIfNeeded() + .addOnCompleteListener(ignored -> dialog.dismiss()) + .addOnSuccessListener( + task -> + translatorsCache + .get(translatorOptions) + .translate(sourceString) + .addOnSuccessListener(targetString -> targetBox.setText(targetString)) + .addOnFailureListener(this::onFailure)) + .addOnFailureListener(this::onFailure); + } + + private void onFailure(@NonNull Exception e) { + Log.e(TAG, e.getMessage(), e); + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + + private void closeKeyboard() { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View view = getCurrentFocus(); + if (view == null) { + view = new View(this); + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } +} diff --git a/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/ModelManagementActivityJava.java b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/ModelManagementActivityJava.java new file mode 100644 index 0000000000..c49e7670e0 --- /dev/null +++ b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/java/ModelManagementActivityJava.java @@ -0,0 +1,243 @@ +package com.google.mlkit.samples.nl.translate.java; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.mlkit.common.model.DownloadConditions; +import com.google.mlkit.common.model.RemoteModelManager; +import com.google.mlkit.nl.translate.TranslateLanguage; +import com.google.mlkit.nl.translate.TranslateLanguage.Language; +import com.google.mlkit.nl.translate.TranslateRemoteModel; +import com.google.mlkit.samples.nl.translate.R; +import com.google.mlkit.samples.nl.translate.kotlin.ModelManagementActivityKotlin; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** Activity for Model Management. */ +public class ModelManagementActivityJava extends AppCompatActivity { + + private static final String TAG = "ModelManagementActivity"; + private RemoteModelManager modelManager; + + private ModelManagementAdapter listAdapter; + private ImmutableList languages; + @Nullable private Set availableModels; + + private final List currentDownloads = new ArrayList<>(); + @Nullable private Snackbar downloadsSnackbar; + + public static Intent makeLaunchIntent(Context context) { + return new Intent(context, ModelManagementActivityKotlin.class); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_model_management); + + modelManager = RemoteModelManager.getInstance(); + ImmutableList.Builder languagesBuilder = ImmutableList.builder(); + for (String language : TranslateLanguage.getAllLanguages()) { + if ("ENGLISH".equals(language)) { + continue; + } + languagesBuilder.add(language); + } + languages = languagesBuilder.build(); + + ListView listView = findViewById(android.R.id.list); + listAdapter = new ModelManagementAdapter(); + listView.setAdapter(listAdapter); + listView.setOnItemClickListener(listAdapter); + } + + @Override + protected void onStart() { + super.onStart(); + refreshAvailabilityData(); + } + + private void refreshAvailabilityData() { + modelManager + .getDownloadedModels(TranslateRemoteModel.class) + .addOnSuccessListener( + this, + result -> { + availableModels = result; + listAdapter.notifyDataSetChanged(); + }) + .addOnFailureListener(this, e -> showError(e, R.string.error_get_models)); + } + + private void downloadModel(TranslateRemoteModel model) { + currentDownloads.add(model.getLanguage()); + updateDownloadsSnackbar(); + + modelManager + .download(model, new DownloadConditions.Builder().requireWifi().build()) + .addOnFailureListener(this, e -> showError(e, R.string.error_download)) + .addOnSuccessListener(this, aVoid -> refreshAvailabilityData()) + .addOnCompleteListener( + this, + task -> { + currentDownloads.remove(model.getLanguage()); + updateDownloadsSnackbar(); + }); + } + + private void updateDownloadsSnackbar() { + if (downloadsSnackbar == null) { + downloadsSnackbar = + Snackbar.make(findViewById(android.R.id.content), "", Snackbar.LENGTH_INDEFINITE); + } + + if (currentDownloads.isEmpty()) { + downloadsSnackbar.dismiss(); + return; + } + + downloadsSnackbar.setText( + getString( + R.string.download_progress, + FluentIterable.from(currentDownloads) + .transform(input -> new Locale(input).getDisplayName()) + .join(Joiner.on(", ")))); + + downloadsSnackbar.show(); + } + + private void deleteModel(TranslateRemoteModel model) { + if (availableModels != null && !availableModels.contains(model)) { + showToast(R.string.model_not_downloaded); + return; + } + + String name = new Locale(model.getLanguage()).getDisplayName(); + final Dialog dialog = buildProgressDialog(getString(R.string.deletion_progress, name)); + modelManager + .deleteDownloadedModel(model) + .addOnSuccessListener(aVoid -> showToast(R.string.deletion_successful)) + .addOnFailureListener(e -> showError(e, R.string.error_delete)) + .addOnCompleteListener( + task -> { + dialog.dismiss(); + refreshAvailabilityData(); + }); + } + + private class ModelManagementAdapter extends BaseAdapter implements OnItemClickListener { + + @Override + public int getCount() { + return languages.size(); + } + + @Override + public String getItem(int position) { + return languages.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Language + private String getLanguage(int position) { + return languages.get(position); + } + + private TranslateRemoteModel getModel(int position) { + return new TranslateRemoteModel.Builder(getLanguage(position)).build(); + } + + private String getLanguageName(int position) { + return new Locale(getItem(position)).getDisplayLanguage(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = getLayoutInflater().inflate(R.layout.item_model_management, parent, false); + } + + TextView textView = convertView.findViewById(android.R.id.text1); + textView.setText(getLanguageName(position)); + + if (availableModels != null) { + textView.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + ContextCompat.getDrawable( + ModelManagementActivityJava.this, + availableModels.contains(getModel(position)) + ? R.drawable.ic_baseline_delete_24 + : R.drawable.ic_file_download_white_24dp), + null); + } + + return convertView; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (availableModels == null) { + showToast(R.string.error_get_models); + return; + } + if (availableModels.contains(getModel(position))) { + new AlertDialog.Builder(ModelManagementActivityJava.this) + .setMessage(getString(R.string.deletion_confirmation_prompt, getLanguageName(position))) + .setPositiveButton(R.string.yes, (dialog, which) -> deleteModel(getModel(position))) + .setNegativeButton( + R.string.no, + (dialog1, which) -> { + // Do nothing. + }) + .show(); + } else { + downloadModel(getModel(position)); + } + } + } + + private void showError(Exception exception, @StringRes int messageId) { + showToast(messageId); + Log.e(TAG, getString(messageId), exception); + } + + private Dialog buildProgressDialog(String message) { + return ProgressDialog.show( + this, + getString(R.string.app_name), + message, + /*indeterminate=*/ true, + /*cancelable=*/ false); + } + + private void showToast(@StringRes int messageId) { + Toast.makeText(this, messageId, Toast.LENGTH_LONG).show(); + } +} diff --git a/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/kotlin/MainActivityKotlin.kt b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/kotlin/MainActivityKotlin.kt new file mode 100644 index 0000000000..1e3eb17549 --- /dev/null +++ b/android/translate/app/src/main/java/com/google/mlkit/samples/nl/translate/kotlin/MainActivityKotlin.kt @@ -0,0 +1,190 @@ +package com.google.mlkit.samples.nl.translate.kotlin + +import android.app.ProgressDialog +import android.content.Context +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.util.Log +import android.util.LruCache +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.TranslateLanguage.Language +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.samples.nl.translate.R +import java.util.Locale + +class MainActivityKotlin : AppCompatActivity() { + + companion object { + private val TAG = MainActivityKotlin::class.java.canonicalName + /** + * This specifies the number of translators instance we want to keep in our LRU cache. Each + * instance of the translator is built with different options based on the source language and the + * target language, and since we want to be able to manage the number of translator instances to + * keep around, an LRU cache is an easy way to achieve this. + */ + private const val NUM_TRANSLATORS = 3 + } + + /** Current translatorOptions for the selected source and target languages */ + private lateinit var translatorOptions: TranslatorOptions + + private val translatorsCache: LruCache = + object : + LruCache(NUM_TRANSLATORS) { + public override fun create(options: TranslatorOptions): Translator { + return Translation.getClient(options) + } + + public override fun entryRemoved( + evicted: Boolean, + key: TranslatorOptions, + oldValue: Translator, + newValue: Translator + ) { + oldValue.close() + } + } + + /** Text box where the user types in their language */ + private lateinit var sourceBox: EditText + /** Text box where translated text will appear */ + private lateinit var targetBox: TextView + + private val languageSet = TranslateLanguage.getAllLanguages() + private val languageLocales = languageSet.map { Locale(it) }.sortedBy { it.displayLanguage } + private val languages = languageLocales.map { it.language } + private val languageNames = languageLocales.map { it.displayLanguage } + + private lateinit var sourceLanguageSpinner: Spinner + private lateinit var targetLanguageSpinner: Spinner + + override fun onCreate(bundle: Bundle?) { + super.onCreate(bundle) + setContentView(R.layout.activity_main) + + sourceBox = findViewById(R.id.sourceBox) + targetBox = findViewById(R.id.targetBox) + sourceLanguageSpinner = findViewById(R.id.sourceLanguageSpinner) + targetLanguageSpinner = findViewById(R.id.targetLanguageSpinner) + + sourceBox.setOnEditorActionListener { v: TextView, actionId: Int, _: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_SEND) { + closeKeyboard() + translate(v.text.toString()) + true + } else { + false + } + } + findViewById