0

Translator: Consume user activation when availability is "after-download"

Requires user activation when availability() would return
"after-download".

Fixed: 394370687
Change-Id: Iab5dcaa5e9d3a33e61005f2b682f8d17ae444e6d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6243270
Reviewed-by: Mustaq Ahmed <mustaq@chromium.org>
Commit-Queue: Nathan Memmott <memmott@chromium.org>
Reviewed-by: Ming-Ying Chung <mych@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1420685}
This commit is contained in:
Nathan Memmott
2025-02-14 12:34:22 -08:00
committed by Chromium LUCI CQ
parent 52cbb28699
commit 08801f73c4
5 changed files with 119 additions and 41 deletions
third_party/blink

@ -52,6 +52,7 @@ class AIMojoClient : public ContextLifecycleObserver {
~AIMojoClient() override = default;
protected:
ScriptState* GetScriptState() { return script_state_; }
ScriptPromiseResolver<V8SessionObjectType>* GetResolver() {
return resolver_;
}

@ -7,10 +7,13 @@
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_ai_create_monitor_callback.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/modules/ai/ai.h"
#include "third_party/blink/renderer/modules/ai/ai_create_monitor.h"
#include "third_party/blink/renderer/modules/ai/ai_mojo_client.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/mojo/heap_mojo_receiver.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
namespace {
@ -18,20 +21,37 @@ namespace {
const char kExceptionMessageUnableToCreateTranslator[] =
"Unable to create translator for the given source and target language.";
bool RequiresUserActivation(mojom::blink::CanCreateTranslatorResult result) {
switch (result) {
case mojom::blink::CanCreateTranslatorResult::kAfterDownloadLibraryNotReady:
case mojom::blink::CanCreateTranslatorResult::
kAfterDownloadLanguagePackNotReady:
case mojom::blink::CanCreateTranslatorResult::
kAfterDownloadLibraryAndLanguagePackNotReady:
return true;
case mojom::blink::CanCreateTranslatorResult::kReadily:
case mojom::blink::CanCreateTranslatorResult::kNoNotSupportedLanguage:
case mojom::blink::CanCreateTranslatorResult::kNoAcceptLanguagesCheckFailed:
case mojom::blink::CanCreateTranslatorResult::
kNoExceedsLanguagePackCountLimitation:
case mojom::blink::CanCreateTranslatorResult::kNoServiceCrashed:
case mojom::blink::CanCreateTranslatorResult::kNoDisallowedByPolicy:
case mojom::blink::CanCreateTranslatorResult::
kNoExceedsServiceCountLimitation:
return false;
}
}
class CreateTranslatorClient
: public GarbageCollected<CreateTranslatorClient>,
public mojom::blink::TranslationManagerCreateTranslatorClient,
public AIMojoClient<AITranslator> {
public:
CreateTranslatorClient(
ScriptState* script_state,
AITranslatorFactory* translation,
AITranslatorCreateOptions* options,
scoped_refptr<base::SequencedTaskRunner> task_runner,
ScriptPromiseResolver<AITranslator>* resolver,
mojo::PendingReceiver<
mojom::blink::TranslationManagerCreateTranslatorClient>
pending_receiver)
CreateTranslatorClient(ScriptState* script_state,
AITranslatorFactory* translation,
AITranslatorCreateOptions* options,
scoped_refptr<base::SequencedTaskRunner> task_runner,
ScriptPromiseResolver<AITranslator>* resolver)
: AIMojoClient(script_state,
translation,
resolver,
@ -41,7 +61,6 @@ class CreateTranslatorClient
target_language_(options->targetLanguage()),
receiver_(this, translation_->GetExecutionContext()),
task_runner_(task_runner) {
receiver_.Bind(std::move(pending_receiver), task_runner);
if (options->hasMonitor()) {
monitor_ = MakeGarbageCollected<AICreateMonitor>(
translation_->GetExecutionContext(), task_runner);
@ -86,6 +105,30 @@ class CreateTranslatorClient
Cleanup();
}
void OnGotAvailability(mojom::blink::CanCreateTranslatorResult result) {
LocalDOMWindow* const window = LocalDOMWindow::From(GetScriptState());
if (RuntimeEnabledFeatures::TranslationAPIV1Enabled() &&
RequiresUserActivation(result) &&
!LocalFrame::ConsumeTransientUserActivation(window->GetFrame())) {
GetResolver()->RejectWithDOMException(
DOMExceptionCode::kNotAllowedError,
"Requires handling a user gesture when availability is "
"\"after-download\".");
return;
}
mojo::PendingRemote<mojom::blink::TranslationManagerCreateTranslatorClient>
client;
receiver_.Bind(client.InitWithNewPipeAndPassReceiver(), task_runner_);
translation_->GetTranslationManagerRemote()->CreateTranslator(
std::move(client),
mojom::blink::TranslatorCreateOptions::New(
mojom::blink::TranslatorLanguageCode::New(source_language_),
mojom::blink::TranslatorLanguageCode::New(target_language_)));
}
void ResetReceiver() override { receiver_.reset(); }
private:
@ -111,14 +154,15 @@ ScriptPromise<AITranslator> AITranslatorFactory::create(
ScriptState* script_state,
AITranslatorCreateOptions* options,
ExceptionState& exception_state) {
// If `sourceLanguage` and `targetLanguage` are not passed, A TypeError should
// be thrown before we get here.
CHECK(options && options->sourceLanguage() && options->targetLanguage());
if (!script_state->ContextIsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"The execution context is not valid.");
return EmptyPromise();
}
// If `sourceLanguage` and `targetLanguage` are not passed, A TypeError should
// be thrown before we get here.
CHECK(options && options->sourceLanguage() && options->targetLanguage());
AbortSignal* signal = options->getSignalOr(nullptr);
if (HandleAbortSignal(signal, script_state, exception_state)) {
@ -128,17 +172,15 @@ ScriptPromise<AITranslator> AITranslatorFactory::create(
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<AITranslator>>(script_state);
mojo::PendingRemote<mojom::blink::TranslationManagerCreateTranslatorClient>
client;
MakeGarbageCollected<CreateTranslatorClient>(
script_state, this, options, task_runner_, resolver,
client.InitWithNewPipeAndPassReceiver());
GetTranslationManagerRemote()->CreateTranslator(
std::move(client),
mojom::blink::TranslatorCreateOptions::New(
mojom::blink::TranslatorLanguageCode::New(options->sourceLanguage()),
mojom::blink::TranslatorLanguageCode::New(
options->targetLanguage())));
CreateTranslatorClient* create_translator_client =
MakeGarbageCollected<CreateTranslatorClient>(script_state, this, options,
task_runner_, resolver);
GetTranslationManagerRemote()->CanCreateTranslator(
mojom::blink::TranslatorLanguageCode::New(options->sourceLanguage()),
mojom::blink::TranslatorLanguageCode::New(options->targetLanguage()),
WTF::BindOnce(&CreateTranslatorClient::OnGotAvailability,
WrapPersistent(create_translator_client)));
return resolver->Promise();
}

@ -34,12 +34,12 @@ class AITranslatorFactory final : public ScriptWrappable,
ScriptState* script_state,
ExceptionState& exception_state);
void Trace(Visitor* visitor) const override;
private:
HeapMojoRemote<mojom::blink::TranslationManager>&
GetTranslationManagerRemote();
void Trace(Visitor* visitor) const override;
private:
scoped_refptr<base::SequencedTaskRunner> task_runner_;
HeapMojoRemote<mojom::blink::TranslationManager> translation_manager_remote_{
nullptr};

@ -4498,6 +4498,7 @@
origin_trial_feature_name: "TranslationAPI",
base_feature_status: "enabled",
copied_from_base_feature_if: "overridden",
implied_by: ["TranslationAPIV1"],
},
{
name: "TranslationAPIEntryPoint",
@ -4505,6 +4506,11 @@
origin_trial_feature_name: "TranslationAPIEntryPoint",
implied_by: ["TranslationAPI", "LanguageDetectionAPI"],
},
{
name: "TranslationAPIV1",
status: "test",
copied_from_base_feature_if: "overridden",
},
{
name: "TrustedTypeBeforePolicyCreationEvent",
status: "experimental",

@ -1,24 +1,54 @@
// META: title=Translate from English to Japanese
// META: global=window,worker
// META: global=window
// META: timeout=long
// META: script=../resources/util.js
// META: script=../resources/language_codes.js
// META: script=/resources/testdriver.js
//
// Setting `timeout=long` as this test may require downloading the translation
// library and the language models.
'use strict';
async function createTranslator(options) {
return await test_driver.bless('Create translator', async () => {
return await ai.translator.create(options);
});
}
promise_test(async t => {
const translatorFactory = ai.translator;
assert_not_equals(translatorFactory, null);
const translator = await translatorFactory.create(
{sourceLanguage: 'en', targetLanguage: 'ja'});
const languagePair = {sourceLanguage: 'en', targetLanguage: 'ja'};
// Creating the translator without user activation rejects with
// NotAllowedError.
const createPromise = ai.translator.create(languagePair);
await promise_rejects_dom(t, 'NotAllowedError', createPromise);
// Creating the translator with user activation succeeds.
await createTranslator(languagePair);
// TODO(crbug.com/390459310): Replace with availability.
//
// Creating it should have switched it to readily.
const capabilities = await ai.translator.capabilities();
const {sourceLanguage, targetLanguage} = languagePair;
assert_equals(
capabilities.languagePairAvailable(sourceLanguage, targetLanguage),
'readily');
// Now that it is readily, we should no longer need user activation.
await ai.translator.create(languagePair);
}, 'AITranslator.create() requires user activation when availability is "after-download".');
promise_test(async t => {
const translator =
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
assert_equals(await translator.translate('hello'), 'こんにちは');
}, 'Simple AITranslator.translate() call');
promise_test(async () => {
const translator =
await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
const streamingResponse = translator.translateStreaming('hello');
assert_equals(
Object.prototype.toString.call(streamingResponse),
@ -32,14 +62,14 @@ promise_test(async () => {
promise_test(async t => {
const translator =
await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
assert_equals(translator.sourceLanguage, 'en');
assert_equals(translator.targetLanguage, 'ja');
}, 'AITranslator: sourceLanguage and targetLanguage are equal to their respective option passed in to AITranslatorFactory.create.')
promise_test(async (t) => {
const translator =
await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
translator.destroy();
await promise_rejects_dom(
t, 'InvalidStateError', translator.translate('hello'));
@ -49,7 +79,7 @@ promise_test(async t => {
const controller = new AbortController();
controller.abort();
const createPromise = ai.translator.create(
const createPromise = createTranslator(
{signal: controller.signal, sourceLanguage: 'en', targetLanguage: 'ja'});
await promise_rejects_dom(t, 'AbortError', createPromise);
@ -57,7 +87,7 @@ promise_test(async t => {
promise_test(async t => {
await testAbortPromise(t, signal => {
return ai.translator.create(
return createTranslator(
{signal, sourceLanguage: 'en', targetLanguage: 'ja'});
});
}, 'Aborting AITranslatorFactory.create().');
@ -67,7 +97,7 @@ promise_test(async t => {
controller.abort();
const translator =
await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
const translatePromise =
translator.translate('hello', {signal: controller.signal});
@ -76,7 +106,7 @@ promise_test(async t => {
promise_test(async t => {
const translator =
await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'});
await testAbortPromise(t, signal => {
return translator.translate('hello', {signal});
});
@ -93,8 +123,7 @@ promise_test(async t => {
});
}
await ai.translator.create(
{sourceLanguage: 'en', targetLanguage: 'ja', monitor});
await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja', monitor});
// Monitor callback must be called.
assert_true(monitorCalled);