Bug 1844521 - GeckoView Initial Session Translations API r=geckoview-reviewers,kaya,calu

This patch lands an initial GeckoView session API for using toolkit
translations. This patch adds an initial session translate delegate,
functions to translate, detect languages, and restore to an original
page. The runtime API will follow in bug 1852313 and additional session
API changes are planned.

Differential Revision: https://phabricator.services.mozilla.com/D189228
This commit is contained in:
Olivia Hall 2023-10-05 22:18:54 +00:00
Родитель 9a4832dfe3
Коммит 3eceb9a32f
18 изменённых файлов: 1073 добавлений и 8 удалений

Просмотреть файл

@ -83,3 +83,8 @@ pref("pdfjs.handleOctetStream", true);
pref("browser.download.open_pdf_attachments_inline", true);
pref("pdfjs.annotationEditorMode", -1);
pref("pdfjs.enableFloatingToolbar", true);
// Bug 1809922 to enable translations
#ifdef NIGHTLY_BUILD
pref("browser.translations.enable", true);
#endif

Просмотреть файл

@ -856,6 +856,12 @@ function startup() {
},
},
},
{
name: "GeckoViewTranslations",
onInit: {
resource: "resource://gre/modules/GeckoViewTranslations.sys.mjs",
},
},
]);
if (!Services.appinfo.sessionHistoryInParent) {

Просмотреть файл

@ -108,6 +108,7 @@ import org.mozilla.geckoview.SessionPdfFileSaver;
import org.mozilla.geckoview.SessionTextInput;
import org.mozilla.geckoview.SlowScriptResponse;
import org.mozilla.geckoview.StorageController;
import org.mozilla.geckoview.TranslationsController;
import org.mozilla.geckoview.WebExtension;
import org.mozilla.geckoview.WebExtensionController;
import org.mozilla.geckoview.WebMessage;
@ -966,9 +967,11 @@ package org.mozilla.geckoview {
method @AnyThread @Nullable public GeckoSession.PromptDelegate getPromptDelegate();
method @Nullable @UiThread public GeckoSession.ScrollDelegate getScrollDelegate();
method @AnyThread @Nullable public GeckoSession.SelectionActionDelegate getSelectionActionDelegate();
method @AnyThread @Nullable public TranslationsController.SessionTranslation getSessionTranslation();
method @AnyThread @NonNull public GeckoSessionSettings getSettings();
method @UiThread public void getSurfaceBounds(@NonNull Rect);
method @AnyThread @NonNull public SessionTextInput getTextInput();
method @AnyThread @Nullable public TranslationsController.SessionTranslation.Delegate getTranslationsSessionDelegate();
method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
method @NonNull @UiThread public WebExtension.SessionController getWebExtensionController();
method @AnyThread public void goBack();
@ -1011,6 +1014,7 @@ package org.mozilla.geckoview {
method @AnyThread public void setPromptDelegate(@Nullable GeckoSession.PromptDelegate);
method @UiThread public void setScrollDelegate(@Nullable GeckoSession.ScrollDelegate);
method @UiThread public void setSelectionActionDelegate(@Nullable GeckoSession.SelectionActionDelegate);
method @AnyThread public void setTranslationsSessionDelegate(@Nullable TranslationsController.SessionTranslation.Delegate);
method @AnyThread public void stop();
method @UiThread protected void setShouldPinOnScreen(boolean);
field public static final int FINDER_DISPLAY_DIM_PAGE = 2;
@ -2201,6 +2205,56 @@ package org.mozilla.geckoview {
@Retention(value=RetentionPolicy.SOURCE) public static interface StorageController.StorageControllerClearFlags {
}
public class TranslationsController {
ctor public TranslationsController();
}
public static class TranslationsController.SessionTranslation {
ctor public SessionTranslation(GeckoSession);
method @AnyThread @NonNull public TranslationsController.SessionTranslation.Handler getHandler();
method @AnyThread @NonNull public GeckoResult<Void> restoreOriginalPage();
method @AnyThread @NonNull public GeckoResult<Void> translate(@NonNull String, @NonNull String, @Nullable TranslationsController.SessionTranslation.TranslationOptions);
method @AnyThread @NonNull public GeckoResult<Void> translate(@NonNull TranslationsController.SessionTranslation.TranslationPair, @Nullable TranslationsController.SessionTranslation.TranslationOptions);
}
@AnyThread public static interface TranslationsController.SessionTranslation.Delegate {
method default public void onExpectedTranslate(@NonNull GeckoSession);
method default public void onOfferTranslate(@NonNull GeckoSession);
method default public void onTranslationStateChange(@NonNull GeckoSession, @Nullable TranslationsController.SessionTranslation.TranslationState);
}
public static class TranslationsController.SessionTranslation.DetectedLanguages {
ctor public DetectedLanguages(@Nullable String, @NonNull Boolean, @Nullable String);
field @Nullable public final String docLangTag;
field @NonNull public final Boolean isDocLangTagSupported;
field @Nullable public final String userLangTag;
}
@AnyThread public static class TranslationsController.SessionTranslation.TranslationOptions {
ctor protected TranslationOptions(@NonNull TranslationsController.SessionTranslation.TranslationOptions.Builder);
field @NonNull public final boolean downloadModel;
}
@AnyThread public static class TranslationsController.SessionTranslation.TranslationOptions.Builder {
ctor public Builder();
method @AnyThread @NonNull public TranslationsController.SessionTranslation.TranslationOptions build();
method @NonNull public TranslationsController.SessionTranslation.TranslationOptions.Builder downloadModel(@NonNull boolean);
}
public static class TranslationsController.SessionTranslation.TranslationPair {
ctor public TranslationPair(@Nullable String, @Nullable String);
field @Nullable public final String fromLanguage;
field @Nullable public final String toLanguage;
}
public static class TranslationsController.SessionTranslation.TranslationState {
ctor public TranslationState(@Nullable TranslationsController.SessionTranslation.TranslationPair, @Nullable String, @Nullable TranslationsController.SessionTranslation.DetectedLanguages, @NonNull Boolean);
field @Nullable public final TranslationsController.SessionTranslation.DetectedLanguages detectedLanguages;
field @Nullable public final String error;
field @NonNull public final Boolean isEngineReady;
field @Nullable public final TranslationsController.SessionTranslation.TranslationPair requestedTranslationPair;
}
public class WebExtension {
method @Nullable @UiThread public WebExtension.BrowsingDataDelegate getBrowsingDataDelegate();
method @Nullable @UiThread public WebExtension.DownloadDelegate getDownloadDelegate();

Просмотреть файл

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<!-- See toolkit for original test.-->
<head>
<meta charset="utf-8" />
<title>Translations Test</title>
<style>
div {
margin: 10px auto;
width: 300px;
}
p {
margin: 47px 0;
font-size: 21px;
line-height: 2;
}
</style>
</head>
<body>
<div>
<!-- The following is an excerpt from The Wondeful Wizard of Oz, which is in the public domain -->
<h1>"The Wonderful Wizard of Oz" by L. Frank Baum</h1>
<p>
The little girl, seeing she had lost one of her pretty shoes, grew
angry, and said to the Witch, “Give me back my shoe!”
</p>
<p>
“I will not,” retorted the Witch, “for it is now my shoe, and not
yours.”
</p>
<p>
“You are a wicked creature!” cried Dorothy. “You have no right to take
my shoe from me.”
</p>
<p>
“I shall keep it, just the same,” said the Witch, laughing at her, “and
someday I shall get the other one from you, too.”
</p>
<p>
This made Dorothy so very angry that she picked up the bucket of water
that stood near and dashed it over the Witch, wetting her from head to
foot.
</p>
<p>
Instantly the wicked woman gave a loud cry of fear, and then, as Dorothy
looked at her in wonder, the Witch began to shrink and fall away.
</p>
<p>
“See what you have done!” she screamed. “In a minute I shall melt away.”
</p>
<p>
“Im very sorry, indeed,” said Dorothy, who was truly frightened to see
the Witch actually melting away like brown sugar before her very eyes.
</p>
<p>
“Didnt you know water would be the end of me?” asked the Witch, in a
wailing, despairing voice.
</p>
<p>“Of course not,” answered Dorothy. “How should I?”</p>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="es">
<!-- See toolkit for original test.-->
<head>
<meta charset="utf-8" />
<title>Translations Test</title>
<style>
div {
margin: 10px auto;
width: 300px;
}
p {
margin: 47px 0;
font-size: 21px;
line-height: 2;
}
</style>
</head>
<body>
<div>
<header lang="en">
The following is an excerpt from Don Quijote de la Mancha, which is in
the public domain
</header>
<h1>Don Quijote de La Mancha</h1>
<h2>Capítulo VIII.</h2>
<p>
Del buen suceso que el valeroso don Quijote tuvo en la espantable y
jamás imaginada aventura de los molinos de viento, con otros sucesos
dignos de felice recordación
</p>
<p>
En esto, descubrieron treinta o cuarenta molinos de viento que hay en
aquel campo; y, así como don Quijote los vio, dijo a su escudero:
</p>
<p>
— La ventura va guiando nuestras cosas mejor de lo que acertáramos a
desear, porque ves allí, amigo Sancho Panza, donde se descubren treinta,
o pocos más, desaforados gigantes, con quien pienso hacer batalla y
quitarles a todos las vidas, con cuyos despojos comenzaremos a
enriquecer; que ésta es buena guerra, y es gran servicio de Dios quitar
tan mala simiente de sobre la faz de la tierra.
</p>
<p>— ¿Qué gigantes? —dijo Sancho Panza.</p>
<p>
— Aquellos que allí ves —respondió su amo— de los brazos largos, que los
suelen tener algunos de casi dos leguas.
</p>
<p>
— Mire vuestra merced —respondió Sancho— que aquellos que allí se
parecen no son gigantes, sino molinos de viento, y lo que en ellos
parecen brazos son las aspas, que, volteadas del viento, hacen andar la
piedra del molino.
</p>
<p>
— Bien parece —respondió don Quijote— que no estás cursado en esto de
las aventuras: ellos son gigantes; y si tienes miedo, quítate de ahí, y
ponte en oración en el espacio que yo voy a entrar con ellos en fiera y
desigual batalla.
</p>
<p>
Y, diciendo esto, dio de espuelas a su caballo Rocinante, sin atender a
las voces que su escudero Sancho le daba, advirtiéndole que, sin duda
alguna, eran molinos de viento, y no gigantes, aquellos que iba a
acometer. Pero él iba tan puesto en que eran gigantes, que ni oía las
voces de su escudero Sancho ni echaba de ver, aunque estaba ya bien
cerca, lo que eran; antes, iba diciendo en voces altas:
</p>
<p>
— Non fuyades, cobardes y viles criaturas, que un solo caballero es el
que os acomete.
</p>
<p>
Levantóse en esto un poco de viento y las grandes aspas comenzaron a
moverse, lo cual visto por don Quijote, dijo:
</p>
<p>
— Pues, aunque mováis más brazos que los del gigante Briareo, me lo
habéis de pagar.
</p>
</div>
</body>
</html>

Просмотреть файл

@ -135,6 +135,8 @@ open class BaseSessionTest(
const val HELLO_PDF_WORLD_PDF_PATH = "/assets/www/helloPDFWorld.pdf"
const val ORANGE_PDF_PATH = "/assets/www/orange.pdf"
const val NO_META_VIEWPORT_HTML_PATH = "/assets/www/no-meta-viewport.html"
const val TRANSLATIONS_EN = "/assets/www/translations-tester-en.html"
const val TRANSLATIONS_ES = "/assets/www/translations-tester-es.html"
const val TEST_ENDPOINT = GeckoSessionTestRule.TEST_ENDPOINT
const val TEST_HOST = GeckoSessionTestRule.TEST_HOST

Просмотреть файл

@ -0,0 +1,135 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.geckoview.test
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import junit.framework.TestCase.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.TranslationsController.SessionTranslation.Delegate
import org.mozilla.geckoview.TranslationsController.SessionTranslation.TranslationOptions
import org.mozilla.geckoview.TranslationsController.SessionTranslation.TranslationState
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
@RunWith(AndroidJUnit4::class)
@MediumTest
class TranslationsTest : BaseSessionTest() {
@Before
fun setup() {
sessionRule.setPrefsUntilTestEnd(
mapOf(
"browser.translations.enable" to true,
"browser.translations.automaticallyPopup" to true,
),
)
}
@Test
fun onExpectedTranslateDelegateTest() {
sessionRule.delegateDuringNextWait(object : Delegate {
@AssertCalled(count = 0)
override fun onExpectedTranslate(session: GeckoSession) {
}
})
mainSession.loadTestPath(TRANSLATIONS_ES)
mainSession.waitForPageStop()
// ToDo: bug 1853469 is to research getting automation testing working fully.
}
@Test
fun onOfferTranslateDelegateTest() {
sessionRule.delegateDuringNextWait(object : Delegate {
@AssertCalled(count = 0)
override fun onOfferTranslate(session: GeckoSession) {
}
})
mainSession.loadTestPath(TRANSLATIONS_ES)
mainSession.waitForPageStop()
// ToDo: bug 1853469 is to research getting onOfferTranslate to work in automation.
}
@Test
fun onTranslationStateChangeDelegateTest() {
sessionRule.delegateDuringNextWait(object : Delegate {
@AssertCalled(count = 1)
override fun onTranslationStateChange(
session: GeckoSession,
translationState: TranslationState?,
) {
assertTrue(
"Translations correctly does not have an engine ready.",
translationState?.isEngineReady == false,
)
assertTrue("Translations correctly does not have a requested pair. ", translationState?.requestedTranslationPair == null)
}
})
mainSession.loadTestPath(TRANSLATIONS_ES)
mainSession.waitForPageStop()
}
@Test
fun translateTest() {
sessionRule.delegateUntilTestEnd(object : Delegate {
@AssertCalled(count = 1)
override fun onTranslationStateChange(
session: GeckoSession,
translationState: TranslationState?,
) {
assertTrue(
"Translations correctly does not have an engine ready. ",
translationState?.isEngineReady == false,
)
assertTrue("Translations correctly does not have a requested pair. ", translationState?.requestedTranslationPair == null)
}
})
mainSession.loadTestPath(TRANSLATIONS_ES)
mainSession.waitForPageStop()
val translate = mainSession.sessionTranslation!!.translate("es", "en", null)
try {
sessionRule.waitForResult(translate)
// ToDo: bug 1853469 models not available in automation
assertTrue("Should not be able to translate.", false)
} catch (e: Exception) {
assertTrue("Should have an exception.", true)
}
}
@Test
fun restoreOriginalPageLanguageTest() {
sessionRule.delegateUntilTestEnd(object : Delegate {
@AssertCalled(count = 2)
override fun onTranslationStateChange(
session: GeckoSession,
translationState: TranslationState?,
) {
assertTrue(
"Translations correctly does not have an engine ready. ",
translationState?.isEngineReady == false,
)
assertTrue("Translations correctly does not have a requested pair. ", translationState?.requestedTranslationPair == null)
}
})
mainSession.loadTestPath(TRANSLATIONS_ES)
mainSession.waitForPageStop()
val restore = mainSession.sessionTranslation!!.restoreOriginalPage()
try {
sessionRule.waitForResult(restore)
assertTrue("Should be able to restore.", true)
} catch (e: Exception) {
assertTrue("Should not have an exception.", false)
}
}
@Test
fun testTranslationOptions() {
var options = TranslationOptions.Builder().downloadModel(true).build()
assertTrue("TranslationOptions builder options work as expected.", options.downloadModel)
// ToDo: bug 1853055 will develop this further.
}
}

Просмотреть файл

@ -91,6 +91,7 @@ import org.mozilla.geckoview.MediaSession;
import org.mozilla.geckoview.OrientationController;
import org.mozilla.geckoview.RuntimeTelemetry;
import org.mozilla.geckoview.SessionTextInput;
import org.mozilla.geckoview.TranslationsController;
import org.mozilla.geckoview.WebExtension;
import org.mozilla.geckoview.WebExtensionController;
import org.mozilla.geckoview.WebNotificationDelegate;
@ -776,6 +777,7 @@ public class GeckoSessionTestRule implements TestRule {
DEFAULT_DELEGATES.add(ScrollDelegate.class);
DEFAULT_DELEGATES.add(SelectionActionDelegate.class);
DEFAULT_DELEGATES.add(TextInputDelegate.class);
DEFAULT_DELEGATES.add(TranslationsController.SessionTranslation.Delegate.class);
}
private static final Set<Class<?>> DEFAULT_RUNTIME_DELEGATES = new HashSet<>();
@ -808,6 +810,7 @@ public class GeckoSessionTestRule implements TestRule {
ScrollDelegate,
SelectionActionDelegate,
TextInputDelegate,
TranslationsController.SessionTranslation.Delegate,
// Runtime delegates
ActivityDelegate,
Autocomplete.StorageDelegate,
@ -1010,6 +1013,9 @@ public class GeckoSessionTestRule implements TestRule {
session.setAutofillDelegate((Autofill.Delegate) delegate);
} else if (cls == MediaSession.Delegate.class) {
session.setMediaSessionDelegate((MediaSession.Delegate) delegate);
} else if (cls == TranslationsController.SessionTranslation.Delegate.class) {
session.setTranslationsSessionDelegate(
(TranslationsController.SessionTranslation.Delegate) delegate);
} else {
GeckoSession.class.getMethod("set" + cls.getSimpleName(), cls).invoke(session, delegate);
}
@ -1082,6 +1088,9 @@ public class GeckoSessionTestRule implements TestRule {
if (cls == MediaSession.Delegate.class) {
return GeckoSession.class.getMethod("getMediaSessionDelegate").invoke(session);
}
if (cls == TranslationsController.SessionTranslation.Delegate.class) {
return GeckoSession.class.getMethod("getTranslationsSessionDelegate").invoke(session);
}
return GeckoSession.class.getMethod("get" + cls.getSimpleName()).invoke(session);
}

Просмотреть файл

@ -144,6 +144,8 @@ public class GeckoSession {
private SessionAccessibility mAccessibility;
private SessionFinder mFinder;
private SessionPdfFileSaver mPdfFileSaver;
private TranslationsController.SessionTranslation mTranslations =
new TranslationsController.SessionTranslation(this);
/** {@code SessionMagnifier} handles magnifying glass. */
/* package */ interface SessionMagnifier {
@ -1219,6 +1221,8 @@ public class GeckoSession {
};
private final MediaSession.Handler mMediaSessionHandler = new MediaSession.Handler(this);
private final TranslationsController.SessionTranslation.Handler mTranslationsHandler =
mTranslations.getHandler();
/* package */ int handlersCount;
@ -1234,6 +1238,7 @@ public class GeckoSession {
mProgressHandler,
mScrollHandler,
mSelectionActionDelegate,
mTranslationsHandler,
mContentBlockingHandler,
mMediaSessionHandler,
mExperimentHandler
@ -3355,6 +3360,40 @@ public class GeckoSession {
return mMediaSessionHandler.getDelegate();
}
/**
* The session translation object coordinates receiving and sending session messages with the
* translations toolkit. Notably, it can be used to request translations.
*
* @return The current translation session coordinator.
*/
@AnyThread
public @Nullable TranslationsController.SessionTranslation getSessionTranslation() {
return mTranslations;
}
/**
* Set the translation delegate, which receives translations events.
*
* @param delegate An implementation of @link{TranslationsController.SessionTranslation.Delegate}.
*/
@AnyThread
public void setTranslationsSessionDelegate(
final @Nullable TranslationsController.SessionTranslation.Delegate delegate) {
mTranslationsHandler.setDelegate(delegate, this);
}
/**
* Get the translations delegate. The application embedder must initially set the translations
* delegate for use.
*
* @return The current translations delegate.
*/
@AnyThread
public @Nullable TranslationsController.SessionTranslation.Delegate
getTranslationsSessionDelegate() {
return mTranslationsHandler.getDelegate();
}
/**
* Get the current selection action delegate for this GeckoSession.
*

Просмотреть файл

@ -0,0 +1,451 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview;
import android.util.Log;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
/**
* The translations controller coordinates the session and runtime messaging between GeckoView and
* the translations toolkit. Initial runtime component will be added in ToDo: bug 1852313.
*/
public class TranslationsController {
private static final boolean DEBUG = false;
private static final String LOGTAG = "TranslationsController";
/**
* Session translation coordinates session messaging between the translations toolkit actor and
* GeckoView.
*
* <p>Performs translations actions that are dependent on the page.
*/
public static class SessionTranslation {
// Events Dispatched to Toolkit Translations
private static final String TRANSLATE_EVENT = "GeckoView:Translations:Translate";
private static final String RESTORE_PAGE_EVENT = "GeckoView:Translations:RestorePage";
// Events Dispatched from Toolkit Translations
private static final String ON_OFFER_EVENT = "GeckoView:Translations:Offer";
private static final String ON_STATE_CHANGE_EVENT = "GeckoView:Translations:StateChange";
private final GeckoSession mSession;
private final SessionTranslation.Handler mHandler;
/**
* Construct a new translations session.
*
* @param session that will be dispatching and receiving events.
*/
public SessionTranslation(final GeckoSession session) {
mSession = session;
mHandler = new SessionTranslation.Handler(mSession);
}
/**
* Handler for receiving messages about translations.
*
* @return associated session handler
*/
@AnyThread
public @NonNull Handler getHandler() {
return mHandler;
}
/**
* Translates the session's current page based on criteria.
*
* <p>Currently when translating, the necessary language models will be automatically
* downloaded.
*
* <p>ToDo: bug 1853055 will adjust this flow to add an option for automatic/non-automatic
* downloads.
*
* @param fromLanguage BCP 47 language tag that the page should be translated from. Usually will
* be the suggested detected language or user specified.
* @param toLanguage BCP 47 language tag that the page should be translated to. Usually will be
* the suggested preference language (will be added in ToDo: bug 1852313) or user specified.
* @param options no-op, ToDo: bug 1853055 will add options
* @return if translate process begins or exceptionally if an issue occurs.
*/
@AnyThread
public @NonNull GeckoResult<Void> translate(
@NonNull final String fromLanguage,
@NonNull final String toLanguage,
@Nullable final TranslationOptions options) {
if (DEBUG) {
Log.d(
LOGTAG,
"Translate page requested - fromLanguage: "
+ fromLanguage
+ " toLanguage: "
+ toLanguage
+ " options: "
+ options);
}
final GeckoBundle bundle = new GeckoBundle(2);
bundle.putString("fromLanguage", fromLanguage);
bundle.putString("toLanguage", toLanguage);
// ToDo: bug 1853055 - Translate options will be configured in a later iteration.
return mSession.getEventDispatcher().queryVoid(TRANSLATE_EVENT, bundle);
}
/**
* Convenience method for calling {@link #translate(String, String, TranslationOptions)} with a
* translation pair.
*
* @param translationPair the object with a from and to language
* @param options no-op, ToDo: bug 1853055 will add options
* @return if translate process begins or exceptionally if an issue occurs.
*/
@AnyThread
public @NonNull GeckoResult<Void> translate(
@NonNull final TranslationPair translationPair,
@Nullable final TranslationOptions options) {
return translate(translationPair.fromLanguage, translationPair.toLanguage, options);
}
/**
* Restores a page to the original or pre-translated state.
*
* @return if page restoration process begins or exceptionally if an issue occurs.
*/
@AnyThread
public @NonNull GeckoResult<Void> restoreOriginalPage() {
if (DEBUG) {
Log.d(LOGTAG, "Restore translated page requested");
}
return mSession.getEventDispatcher().queryVoid(RESTORE_PAGE_EVENT);
}
/**
* Options available for translating. The options available for translating. Will be developed
* in ToDo: bug 1853055.
*
* <p>Options (default):
*
* <p>downloadModel (true) - Downloads any models automatically that are needed for translation.
*/
@AnyThread
public static class TranslationOptions {
/** If the model should be automatically downloaded or stopped. */
public final @NonNull boolean downloadModel;
/**
* Options for translation.
*
* @param builder that populated the translation options
*/
protected TranslationOptions(final @NonNull Builder builder) {
this.downloadModel = builder.mDownloadModel;
}
/** Builder for making translation options. */
@AnyThread
public static class Builder {
/* package */ boolean mDownloadModel = true;
/**
* Build setter for the option for downloading a model.
*
* @param downloadModel should the model be automatically download or not
* @return the model to download for the translation options
*/
public @NonNull Builder downloadModel(final @NonNull boolean downloadModel) {
mDownloadModel = downloadModel;
return this;
}
/**
* Final call to build the specified options.
*
* @return a constructed translation options
*/
@AnyThread
public @NonNull TranslationOptions build() {
return new TranslationOptions(this);
}
}
}
/**
* The translations session delegate is used for receiving translation events and information.
*/
@AnyThread
public interface Delegate {
/**
* onOfferTranslate occurs when a page should be offered for translation.
*
* <p>An offer should occur when all conditions are met:
*
* <p>* The page is not in the user's preferred language
*
* <p>* The page language is eligible for translation
*
* <p>* The host hasn't been offered for translation in this session
*
* <p>* No user preferences indicate that translation shouldn't be offered
*
* <p>* It is possible to translate
*
* <p>Usual use-case is to show a pop-up recommending a translation.
*
* @param session The associated GeckoSession.
*/
default void onOfferTranslate(@NonNull final GeckoSession session) {}
/**
* onExpectedTranslate occurs when it is likely the user will want to translate and it is
* feasible. For example, if the page is in a different language than the user preferred
* language or languages.
*
* <p>Usual use-case is to add a toolbar option for translate.
*
* @param session The associated GeckoSession.
*/
default void onExpectedTranslate(@NonNull final GeckoSession session) {}
/**
* onTranslationStateChange occurs when new information about the translation state is
* available. This includes information when first visiting the page and after calls to
* translate.
*
* @param session The associated GeckoSession.
* @param translationState The state of the translation as reported by the translation engine.
*/
default void onTranslationStateChange(
@NonNull final GeckoSession session, @Nullable TranslationState translationState) {}
}
/** Translation pair is the from language and to language set on the translation state. */
public static class TranslationPair {
/** Language the page is translated from originally. */
public final @Nullable String fromLanguage;
/** Language the page is translated to. */
public final @Nullable String toLanguage;
/**
* Requested translation pair constructor.
*
* @param fromLanguage original language of page (detected or specified)
* @param toLanguage translated to language of page (detected or specified)
*/
public TranslationPair(
@Nullable final String fromLanguage, @Nullable final String toLanguage) {
this.fromLanguage = fromLanguage;
this.toLanguage = toLanguage;
}
@Override
public String toString() {
return "TranslationPair {"
+ "fromLanguage='"
+ fromLanguage
+ '\''
+ ", toLanguage='"
+ toLanguage
+ '\''
+ '}';
}
/**
* Convenience method for deserializing translation state information.
*
* @param bundle contains translation pair information.
* @return translation pair
*/
/* package */
static @Nullable TranslationPair fromBundle(final GeckoBundle bundle) {
if (bundle == null) {
return null;
}
return new TranslationPair(
bundle.getString("fromLanguage"), bundle.getString("toLanguage"));
}
}
/** DetectedLanguages is information that was detected about the page or user preferences. */
public static class DetectedLanguages {
/** The user's preferred language tag */
public final @Nullable String userLangTag;
/** If the engine supports the document language. */
public final @NonNull Boolean isDocLangTagSupported;
/** Detected language tag of page. */
public final @Nullable String docLangTag;
/**
* DetectedLanguages constructor.
*
* @param userLangTag - the user's preferred language tag
* @param isDocLangTagSupported - if the engine supports the document language for translation
* @param docLangTag - the document's detected language tag
*/
public DetectedLanguages(
@Nullable final String userLangTag,
@NonNull final Boolean isDocLangTagSupported,
@Nullable final String docLangTag) {
this.userLangTag = userLangTag;
this.isDocLangTagSupported = isDocLangTagSupported;
this.docLangTag = docLangTag;
}
@Override
public String toString() {
return "DetectedLanguages {"
+ "userLangTag='"
+ userLangTag
+ '\''
+ ", isDocLangTagSupported="
+ isDocLangTagSupported
+ ", docLangTag='"
+ docLangTag
+ '\''
+ '}';
}
/**
* Convenience method for deserializing detected language state information.
*
* @param bundle contains detected language information.
* @return detected language information
*/
/* package */
static @Nullable DetectedLanguages fromBundle(final GeckoBundle bundle) {
if (bundle == null) {
return null;
}
return new DetectedLanguages(
bundle.getString("userLangTag"),
bundle.getBoolean("isDocLangTagSupported", false),
bundle.getString("docLangTag"));
}
}
/** The representation of the translation state. */
public static class TranslationState {
/** The language pair to translate. */
public final @Nullable TranslationPair requestedTranslationPair;
/** If an error state occurred. */
public final @Nullable String error;
/** Detected information about preferences and page information. */
public final @Nullable DetectedLanguages detectedLanguages;
/** If the translation engine is ready for use or will need to be loaded. */
public final @NonNull Boolean isEngineReady;
/**
* Translation State constructor.
*
* @param requestedTranslationPair the language pair to translate
* @param error if an error occurred
* @param detectedLanguages detected language
* @param isEngineReady if the engine is ready for translations
*/
public TranslationState(
final @Nullable TranslationPair requestedTranslationPair,
final @Nullable String error,
final @Nullable DetectedLanguages detectedLanguages,
final @NonNull Boolean isEngineReady) {
this.requestedTranslationPair = requestedTranslationPair;
this.error = error;
this.detectedLanguages = detectedLanguages;
this.isEngineReady = isEngineReady;
}
@Override
public String toString() {
return "TranslationState {"
+ "requestedTranslationPair="
+ requestedTranslationPair
+ ", error='"
+ error
+ '\''
+ ", detectedLanguages="
+ detectedLanguages
+ ", isEngineReady="
+ isEngineReady
+ '}';
}
/**
* Convenience method for deserializing translation state information.
*
* @param bundle contains information about translation state.
* @return translation state
*/
/* package */
static @Nullable TranslationState fromBundle(final GeckoBundle bundle) {
if (bundle == null) {
return null;
}
return new TranslationState(
TranslationPair.fromBundle(bundle.getBundle("requestedTranslationPair")),
bundle.getString("error"),
DetectedLanguages.fromBundle(bundle.getBundle("detectedLanguages")),
bundle.getBoolean("isEngineReady", false));
}
}
/* package */ static class Handler extends GeckoSessionHandler<SessionTranslation.Delegate> {
private final GeckoSession mSession;
private Handler(final GeckoSession session) {
super(
"GeckoViewTranslations",
session,
new String[] {
ON_OFFER_EVENT, ON_STATE_CHANGE_EVENT,
});
mSession = session;
}
@Override
public void handleMessage(
final Delegate delegate,
final String event,
final GeckoBundle message,
final EventCallback callback) {
if (DEBUG) {
Log.d(LOGTAG, "handleMessage " + event);
}
if (delegate == null) {
Log.w(LOGTAG, "The translations session delegate is not set.");
return;
}
if (ON_OFFER_EVENT.equals(event)) {
delegate.onOfferTranslate(mSession);
return;
} else if (ON_STATE_CHANGE_EVENT.equals(event)) {
final GeckoBundle data = message.getBundle("data");
final TranslationState translationState = TranslationState.fromBundle(data);
delegate.onTranslationStateChange(mSession, translationState);
if (translationState != null
&& translationState.detectedLanguages != null
&& translationState.detectedLanguages.docLangTag != null
&& translationState.detectedLanguages.userLangTag != null
&& translationState.detectedLanguages.isDocLangTagSupported)
// Also check if engine is supported when runtime functions are added in ToDo: bug 1852313
{
delegate.onExpectedTranslate(mSession);
}
return;
}
}
}
}
}

Просмотреть файл

@ -19,11 +19,16 @@ exclude: true
- Added `Builder` pattern constructors for [`ReviewAnalysis`][120.2] and [`Recommendation`][120.3] (part of [bug 1846341]({{bugzilla}}1846341))
- Added `DisabledFlags.APP_VERSION` for extensions disabled because they aren't compatible with the application version. ([bug 1847266]({{bugzilla}}1847266))
- Added more metadata to the [WebExtension][120.4] class. ([bug 1850674]({{bugzilla}}1850674))
- Added session and translations controller. Includes [`TranslationsController`][120.5], [`TranslationsController.SessionTranslation`][120.6] (notably [translate][120.7]), and a [translations delegate][120.8].
[120.1]: {{javadoc_uri}}/WebExtensionController.html#disableExtensionProcessSpawning
[120.2]: {{javadoc_uri}}/GeckoSession.html#ReviewAnalysis.Builder.html
[120.3]: {{javadoc_uri}}/GeckoSession.html#Recommendation.Builder.html
[120.4]: {{javadoc_uri}}/WebExtension.html)
[120.5]: {{javadoc_uri}}/TranslationsController.html
[120.6]: {{javadoc_uri}}/TranslationsController.SessionTranslation.html
[120.7]: {{javadoc_uri}}/TranslationsController.SessionTranslation.html#translate(java.lang.String,java.lang.String,org.mozilla.geckoview.TranslationsController.SessionTranslation.TranslationOptions)
[120.8]: {{javadoc_uri}}/TranslationsController.SessionTranslation.Delegate.html
## v119
- Added `remoteType` to GeckoView child crash intent. ([bug 1851518]({{bugzilla}}1851518))
@ -1444,4 +1449,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: 834b35e061b37da5e862ad47dd45f9b26a80ed1f
[api-version]: a3b3103cf8ea7f5d05c8b73c197399196c57d391

Просмотреть файл

@ -39,8 +39,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@ -87,6 +89,7 @@ import org.mozilla.geckoview.MediaSession;
import org.mozilla.geckoview.OrientationController;
import org.mozilla.geckoview.RuntimeTelemetry;
import org.mozilla.geckoview.SlowScriptResponse;
import org.mozilla.geckoview.TranslationsController;
import org.mozilla.geckoview.WebExtension;
import org.mozilla.geckoview.WebExtensionController;
import org.mozilla.geckoview.WebNotification;
@ -440,6 +443,10 @@ public class GeckoViewActivity extends AppCompatActivity
private boolean mCanGoBack;
private boolean mCanGoForward;
private boolean mFullScreen;
private boolean mExpectedTranslate = false;
private boolean mTranslateRestore = false;
private String mDetectedLanguage = null;
private HashMap<String, Integer> mNotificationIDMap = new HashMap<>();
private int mLastID = 100;
@ -1134,6 +1141,8 @@ public class GeckoViewActivity extends AppCompatActivity
session.setMediaSessionDelegate(new ExampleMediaSessionDelegate(this));
session.setTranslationsSessionDelegate(new ExampleTranslationsSessionDelegate());
session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
if (sExtensionManager.extension != null) {
final WebExtension.SessionController sessionController = session.getWebExtensionController();
@ -1230,7 +1239,8 @@ public class GeckoViewActivity extends AppCompatActivity
menu.findItem(R.id.action_tpe).setEnabled(hasSession && mTrackingProtectionPermission != null);
menu.findItem(R.id.action_pb).setEnabled(hasSession);
menu.findItem(R.id.desktop_mode).setEnabled(hasSession);
menu.findItem(R.id.translate).setVisible(mExpectedTranslate);
menu.findItem(R.id.translate_restore).setVisible(mTranslateRestore);
return true;
}
@ -1297,6 +1307,12 @@ public class GeckoViewActivity extends AppCompatActivity
case R.id.poll_shopping_analysis_status:
pollForAnalysisCompleted(session, mCurrentUri);
break;
case R.id.translate:
translate(session);
break;
case R.id.translate_restore:
translateRestore(session);
break;
default:
return super.onOptionsItemSelected(item);
}
@ -1405,6 +1421,88 @@ public class GeckoViewActivity extends AppCompatActivity
session.didPrintPageContent();
}
private void translate(GeckoSession session) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.translate);
// Bug 1844518 - Change to dropdowns with language pairs
final EditText fromSelect = new EditText(this);
fromSelect.setText(mDetectedLanguage);
final EditText toSelect = new EditText(this);
// Bug 1844518 - Detect to language preference
toSelect.setText("en");
builder.setView(translateLayout(fromSelect, toSelect));
builder.setPositiveButton(
R.string.translate_action,
(dialog, which) -> {
final String fromLang = fromSelect.getText().toString();
final String toLang = toSelect.getText().toString();
session.getSessionTranslation().translate(fromLang, toLang, null);
mTranslateRestore = true;
});
builder.setNegativeButton(
R.string.cancel,
(dialog, which) -> {
// Nothing to do
});
builder.show();
}
private void translateRestore(GeckoSession session) {
session
.getSessionTranslation()
.restoreOriginalPage()
.then(
new GeckoResult.OnValueListener<Void, Object>() {
@Nullable
@Override
public GeckoResult<Object> onValue(@Nullable Void value) throws Throwable {
mTranslateRestore = false;
return null;
}
});
}
private RelativeLayout translateLayout(EditText fromSelect, EditText toSelect) {
// From fields
TextView fromLangLabel = new TextView(this);
fromLangLabel.setText(R.string.translate_language_from_hint);
fromSelect.setInputType(InputType.TYPE_CLASS_TEXT);
fromSelect.setHint(R.string.translate_language_from_hint);
LinearLayout from = new LinearLayout(this);
from.setId(View.generateViewId());
from.addView(fromLangLabel);
from.addView(fromSelect);
RelativeLayout.LayoutParams fromParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
fromParams.setMarginStart(30);
// To fields
TextView toLangLabel = new TextView(this);
toLangLabel.setText(R.string.translate_language_to_hint);
toSelect.setInputType(InputType.TYPE_CLASS_TEXT);
toSelect.setHint(R.string.translate_language_to_hint);
LinearLayout to = new LinearLayout(this);
to.setId(View.generateViewId());
to.addView(toLangLabel);
to.addView(toSelect);
RelativeLayout.LayoutParams toParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
toParams.setMarginStart(30);
toParams.addRule(RelativeLayout.BELOW, from.getId());
// Layout
RelativeLayout layout = new RelativeLayout(this);
layout.addView(from, fromParams);
layout.addView(to, toParams);
return layout;
}
@Override
public void closeTab(TabSession session) {
mTabSessionManager.closeSession(session);
@ -1918,6 +2016,8 @@ public class GeckoViewActivity extends AppCompatActivity
Log.i(LOGTAG, "Starting to load page at " + url);
Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - page load start");
mCb.clearCounters();
mExpectedTranslate = false;
mTranslateRestore = false;
}
@Override
@ -2545,6 +2645,30 @@ public class GeckoViewActivity extends AppCompatActivity
}
}
private class ExampleTranslationsSessionDelegate
implements TranslationsController.SessionTranslation.Delegate {
@Override
public void onOfferTranslate(@NonNull GeckoSession session) {
Log.i(LOGTAG, "onOfferTranslate");
}
@Override
public void onExpectedTranslate(@NonNull GeckoSession session) {
Log.i(LOGTAG, "onExpectedTranslate");
mExpectedTranslate = true;
}
@Override
public void onTranslationStateChange(
@NonNull GeckoSession session,
@Nullable TranslationsController.SessionTranslation.TranslationState translationState) {
Log.i(LOGTAG, "onTranslationStateChange");
if (translationState.detectedLanguages != null) {
mDetectedLanguage = translationState.detectedLanguages.docLangTag;
}
}
}
private class ExampleMediaSessionDelegate implements MediaSession.Delegate {
private final Activity mActivity;

Просмотреть файл

@ -18,5 +18,7 @@
<item android:title="Create Shopping Analysis" android:id="@+id/create_shopping_analysis"/>
<item android:title="Get Shopping Analysis Status" android:id="@+id/get_shopping_analysis_status"/>
<item android:title="Poll Until Analysis Completed" android:id="@+id/poll_shopping_analysis_status"/>
<item android:title="@string/translate" android:id="@+id/translate"/>
<item android:title="@string/translate_restore" android:id="@+id/translate_restore"/>
<item android:title="@string/settings" android:id="@+id/settings" app:showAsAction="never" />
</menu>

Просмотреть файл

@ -52,6 +52,11 @@
<string name="addon_uri">WebExtension xpi URL</string>
<string name="save_pdf">Save as PDF</string>
<string name="print_page">Print Page</string>
<string name="translate">Translate</string>
<string name="translate_restore">Restore to Original</string>
<string name="translate_language_from_hint">From Language</string>
<string name="translate_language_to_hint">To Language</string>
<string name="translate_action">Translate</string>
# Preferences
<string name="key_tracking_protection">tracking_protection</string>

Просмотреть файл

@ -12,6 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
LoadURIDelegate: "resource://gre/modules/LoadURIDelegate.sys.mjs",
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
@ -651,7 +652,7 @@ export class GeckoViewNavigation extends GeckoViewModule {
isTopLevel: aWebProgress.isTopLevel,
permissions,
};
lazy.TranslationsParent.onLocationChange(this.browser);
this.eventDispatcher.sendRequest(message);
this.isProductURL(aLocationURI);

Просмотреть файл

@ -0,0 +1,75 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
export class GeckoViewTranslations extends GeckoViewModule {
onInit() {
debug`onInit`;
this.registerListener([
"GeckoView:Translations:Translate",
"GeckoView:Translations:RestorePage",
]);
}
onEnable() {
debug`onEnable`;
this.window.addEventListener("TranslationsParent:OfferTranslation", this);
this.window.addEventListener("TranslationsParent:LanguageState", this);
}
onDisable() {
debug`onDisable`;
this.window.removeEventListener(
"TranslationsParent:OfferTranslation",
this
);
this.window.removeEventListener("TranslationsParent:LanguageState", this);
}
onEvent(aEvent, aData, aCallback) {
debug`onEvent: event=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoView:Translations:Translate":
const { fromLanguage, toLanguage } = aData;
try {
this.getActor("Translations").translate(fromLanguage, toLanguage);
aCallback.onSuccess();
} catch (error) {
// Bug 1853055 will add named error states.
aCallback.onError(`Could not translate: ${error}`);
}
break;
case "GeckoView:Translations:RestorePage":
try {
this.getActor("Translations").restorePage();
aCallback.onSuccess();
} catch (error) {
// Bug 1853055 will add named error states.
aCallback.onError(`Could not restore page: ${error}`);
}
break;
}
}
handleEvent(aEvent) {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "TranslationsParent:OfferTranslation":
this.eventDispatcher.sendRequest({
type: "GeckoView:Translations:Offer",
});
break;
case "TranslationsParent:LanguageState":
this.eventDispatcher.sendRequest({
type: "GeckoView:Translations:StateChange",
data: aEvent.detail,
});
break;
}
}
}
const { debug, warn } = GeckoViewTranslations.initLogging(
"GeckoViewTranslations"
);

Просмотреть файл

@ -33,6 +33,7 @@ EXTRA_JS_MODULES += [
"GeckoViewTab.sys.mjs",
"GeckoViewTelemetry.sys.mjs",
"GeckoViewTestUtils.sys.mjs",
"GeckoViewTranslations.sys.mjs",
"GeckoViewUtils.sys.mjs",
"GeckoViewWebExtension.sys.mjs",
"LoadURIDelegate.sys.mjs",

Просмотреть файл

@ -433,11 +433,17 @@ export class TranslationsParent extends JSWindowActorParent {
}
// Only offer the translation if it's still the current page.
if (
documentURI.spec ===
this.browsingContext.topChromeWindow.gBrowser.selectedBrowser.documentURI
.spec
) {
var isCurrentPage = false;
if (AppConstants.platform !== "android") {
isCurrentPage =
documentURI.spec ===
this.browsingContext.topChromeWindow.gBrowser.selectedBrowser
.documentURI.spec;
} else {
// In Android, the active window is the active tab.
isCurrentPage = documentURI.spec === browser.documentURI.spec;
}
if (isCurrentPage) {
lazy.console.log(
"maybeOfferTranslations - Offering a translation",
documentURI.spec,