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:

committed by
Chromium LUCI CQ

parent
52cbb28699
commit
08801f73c4
third_party/blink
renderer
modules
ai
platform
web_tests
external
wpt
ai
@ -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",
|
||||
|
57
third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js
vendored
57
third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js
vendored
@ -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);
|
||||
|
Reference in New Issue
Block a user