• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include <jni.h>
18 
19 #include <memory>
20 #include <vector>
21 
22 #include "utils/flatbuffers/mutable.h"
23 #include "utils/intents/intent-generator.h"
24 #include "utils/intents/remote-action-template.h"
25 #include "utils/java/jni-helper.h"
26 #include "utils/jvm-test-utils.h"
27 #include "utils/resources_generated.h"
28 #include "utils/testing/logging_event_listener.h"
29 #include "utils/variant.h"
30 #include "gmock/gmock.h"
31 #include "gtest/gtest.h"
32 #include "flatbuffers/reflection.h"
33 
34 namespace libtextclassifier3 {
35 namespace {
36 
37 using ::testing::ElementsAre;
38 using ::testing::IsEmpty;
39 using ::testing::SizeIs;
40 
BuildTestIntentFactoryModel(const std::string & entity_type,const std::string & generator_code)41 flatbuffers::DetachedBuffer BuildTestIntentFactoryModel(
42     const std::string& entity_type, const std::string& generator_code) {
43   // Test intent generation options.
44   IntentFactoryModelT options;
45   options.generator.emplace_back(new IntentFactoryModel_::IntentGeneratorT());
46   options.generator.back()->type = entity_type;
47   options.generator.back()->lua_template_generator = std::vector<unsigned char>(
48       generator_code.data(), generator_code.data() + generator_code.size());
49   flatbuffers::FlatBufferBuilder builder;
50   builder.Finish(IntentFactoryModel::Pack(builder, &options));
51   return builder.Release();
52 }
53 
BuildTestResources()54 flatbuffers::DetachedBuffer BuildTestResources() {
55   // Custom string resources.
56   ResourcePoolT test_resources;
57   test_resources.locale.emplace_back(new LanguageTagT);
58   test_resources.locale.back()->language = "en";
59   test_resources.locale.emplace_back(new LanguageTagT);
60   test_resources.locale.back()->language = "de";
61 
62   // Add `add_calendar_event`
63   test_resources.resource_entry.emplace_back(new ResourceEntryT);
64   test_resources.resource_entry.back()->name = "add_calendar_event";
65 
66   // en
67   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
68   test_resources.resource_entry.back()->resource.back()->content = "Schedule";
69   test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
70 
71   // Add `add_calendar_event_desc`
72   test_resources.resource_entry.emplace_back(new ResourceEntryT);
73   test_resources.resource_entry.back()->name = "add_calendar_event_desc";
74 
75   // en
76   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
77   test_resources.resource_entry.back()->resource.back()->content =
78       "Schedule event for selected time";
79   test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
80 
81   // Add `map`.
82   test_resources.resource_entry.emplace_back(new ResourceEntryT);
83   test_resources.resource_entry.back()->name = "map";
84 
85   // en
86   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
87   test_resources.resource_entry.back()->resource.back()->content = "Map";
88   test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
89 
90   // de
91   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
92   test_resources.resource_entry.back()->resource.back()->content = "Karte";
93   test_resources.resource_entry.back()->resource.back()->locale.push_back(1);
94 
95   // Add `map_desc`.
96   test_resources.resource_entry.emplace_back(new ResourceEntryT);
97   test_resources.resource_entry.back()->name = "map_desc";
98 
99   // en
100   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
101   test_resources.resource_entry.back()->resource.back()->content =
102       "Locate selected address";
103   test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
104 
105   // de
106   test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
107   test_resources.resource_entry.back()->resource.back()->content =
108       "Ausgewählte Adresse finden";
109   test_resources.resource_entry.back()->resource.back()->locale.push_back(1);
110 
111   flatbuffers::FlatBufferBuilder builder;
112   builder.Finish(ResourcePool::Pack(builder, &test_resources));
113   return builder.Release();
114 }
115 
116 // Common methods for intent generator tests.
117 class IntentGeneratorTest : public testing::Test {
118  protected:
IntentGeneratorTest()119   explicit IntentGeneratorTest()
120       : jni_cache_(JniCache::Create(GetJenv())),
121         resource_buffer_(BuildTestResources()),
122         resources_(
123             flatbuffers::GetRoot<ResourcePool>(resource_buffer_.data())) {}
124 
125   const std::shared_ptr<JniCache> jni_cache_;
126   const flatbuffers::DetachedBuffer resource_buffer_;
127   const ResourcePool* resources_;
128 };
129 
TEST_F(IntentGeneratorTest,HandlesDefaultClassification)130 TEST_F(IntentGeneratorTest, HandlesDefaultClassification) {
131   flatbuffers::DetachedBuffer intent_factory_model =
132       BuildTestIntentFactoryModel("unused", "");
133   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
134       /*options=*/flatbuffers::GetRoot<IntentFactoryModel>(
135           intent_factory_model.data()),
136       /*resources=*/resources_,
137       /*jni_cache=*/jni_cache_);
138   ClassificationResult classification;
139   std::vector<RemoteActionTemplate> intents;
140   EXPECT_TRUE(generator->GenerateIntents(
141       /*device_locales=*/nullptr, classification, /*reference_time_ms_utc=*/0,
142       /*text=*/"", /*selection_indices=*/{kInvalidIndex, kInvalidIndex},
143       /*context=*/nullptr,
144       /*annotations_entity_data_schema=*/nullptr,
145       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
146       &intents));
147   EXPECT_THAT(intents, IsEmpty());
148 }
149 
TEST_F(IntentGeneratorTest,FailsGracefully)150 TEST_F(IntentGeneratorTest, FailsGracefully) {
151   flatbuffers::DetachedBuffer intent_factory_model =
152       BuildTestIntentFactoryModel("test", R"lua(
153 return {
154   {
155     -- Should fail, as no app GetAndroidContext() is provided.
156     data = external.android.package_name,
157   }
158 })lua");
159   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
160       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
161       /*resources=*/resources_, jni_cache_);
162   ClassificationResult classification = {"test", 1.0};
163   std::vector<RemoteActionTemplate> intents;
164   EXPECT_FALSE(generator->GenerateIntents(
165       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
166       classification,
167       /*reference_time_ms_utc=*/0, "test", {0, 4}, /*context=*/nullptr,
168       /*annotations_entity_data_schema=*/nullptr,
169       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
170       &intents));
171   EXPECT_THAT(intents, IsEmpty());
172 }
173 
TEST_F(IntentGeneratorTest,HandlesEntityIntentGeneration)174 TEST_F(IntentGeneratorTest, HandlesEntityIntentGeneration) {
175   flatbuffers::DetachedBuffer intent_factory_model =
176       BuildTestIntentFactoryModel("address", R"lua(
177 return {
178   {
179     title_without_entity = external.android.R.map,
180     title_with_entity = external.entity.text,
181     description = external.android.R.map_desc,
182     action = "android.intent.action.VIEW",
183     data = "geo:0,0?q=" ..
184     external.android.urlencode(external.entity.text),
185   }
186 })lua");
187   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
188       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
189       /*resources=*/resources_, jni_cache_);
190   ClassificationResult classification = {"address", 1.0};
191   std::vector<RemoteActionTemplate> intents;
192   EXPECT_TRUE(generator->GenerateIntents(
193       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
194       classification,
195       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
196       GetAndroidContext(),
197       /*annotations_entity_data_schema=*/nullptr,
198       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
199       &intents));
200   EXPECT_THAT(intents, SizeIs(1));
201   EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
202   EXPECT_EQ(intents[0].title_with_entity.value(), "333 E Wonderview Ave");
203   EXPECT_EQ(intents[0].description.value(), "Locate selected address");
204   EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
205   EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=333%20E%20Wonderview%20Ave");
206 }
207 
TEST_F(IntentGeneratorTest,HandlesAddContactIntentEnabledGeneration)208 TEST_F(IntentGeneratorTest, HandlesAddContactIntentEnabledGeneration) {
209   flatbuffers::DetachedBuffer intent_factory_model =
210       BuildTestIntentFactoryModel("address", R"lua(
211 if external.enable_add_contact_intent then return {
212   {
213     title_without_entity = external.android.R.map,
214     title_with_entity = external.entity.text,
215     description = external.android.R.map_desc,
216     action = "android.intent.action.VIEW",
217     data = "geo:0,0?q=" ..
218     external.android.urlencode(external.entity.text),
219   }
220 } else return {} end)lua");
221   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
222       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
223       /*resources=*/resources_, jni_cache_);
224   ClassificationResult classification = {"address", 1.0};
225   std::vector<RemoteActionTemplate> intents;
226   EXPECT_TRUE(generator->GenerateIntents(
227       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
228       classification,
229       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
230       GetAndroidContext(),
231       /*annotations_entity_data_schema=*/nullptr,
232       /*enable_add_contact_intent=*/true, /*enable_search_intent=*/false,
233       &intents));
234   EXPECT_THAT(intents, SizeIs(1));
235   EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
236 }
237 
TEST_F(IntentGeneratorTest,HandlesAddContactIntentDisabledGeneration)238 TEST_F(IntentGeneratorTest, HandlesAddContactIntentDisabledGeneration) {
239   flatbuffers::DetachedBuffer intent_factory_model =
240       BuildTestIntentFactoryModel("address", R"lua(
241 if external.enable_add_contact_intent then return {
242   {
243     title_without_entity = external.android.R.map,
244     title_with_entity = external.entity.text,
245     description = external.android.R.map_desc,
246     action = "android.intent.action.VIEW",
247     data = "geo:0,0?q=" ..
248     external.android.urlencode(external.entity.text),
249   }
250 } else return {} end)lua");
251   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
252       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
253       /*resources=*/resources_, jni_cache_);
254   ClassificationResult classification = {"address", 1.0};
255   std::vector<RemoteActionTemplate> intents;
256   EXPECT_TRUE(generator->GenerateIntents(
257       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
258       classification,
259       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
260       GetAndroidContext(),
261       /*annotations_entity_data_schema=*/nullptr,
262       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
263       &intents));
264   EXPECT_THAT(intents, SizeIs(0));
265 }
266 
TEST_F(IntentGeneratorTest,HandlesAddSearchIntentEnabledGeneration)267 TEST_F(IntentGeneratorTest, HandlesAddSearchIntentEnabledGeneration) {
268   flatbuffers::DetachedBuffer intent_factory_model =
269       BuildTestIntentFactoryModel("address", R"lua(
270 if external.enable_search_intent then return {
271   {
272     title_without_entity = external.android.R.map,
273     title_with_entity = external.entity.text,
274     description = external.android.R.map_desc,
275     action = "android.intent.action.VIEW",
276     data = "geo:0,0?q=" ..
277     external.android.urlencode(external.entity.text),
278   }
279 } else return {} end)lua");
280   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
281       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
282       /*resources=*/resources_, jni_cache_);
283   ClassificationResult classification = {"address", 1.0};
284   std::vector<RemoteActionTemplate> intents;
285   EXPECT_TRUE(generator->GenerateIntents(
286       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
287       classification,
288       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
289       GetAndroidContext(),
290       /*annotations_entity_data_schema=*/nullptr,
291       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/true,
292       &intents));
293   EXPECT_THAT(intents, SizeIs(1));
294   EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
295 }
296 
TEST_F(IntentGeneratorTest,HandlesSearchIntentDisabledGeneration)297 TEST_F(IntentGeneratorTest, HandlesSearchIntentDisabledGeneration) {
298   flatbuffers::DetachedBuffer intent_factory_model =
299       BuildTestIntentFactoryModel("address", R"lua(
300 if external.enable_search_intent then return {
301   {
302     title_without_entity = external.android.R.map,
303     title_with_entity = external.entity.text,
304     description = external.android.R.map_desc,
305     action = "android.intent.action.VIEW",
306     data = "geo:0,0?q=" ..
307     external.android.urlencode(external.entity.text),
308   }
309 } else return {} end)lua");
310   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
311       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
312       /*resources=*/resources_, jni_cache_);
313   ClassificationResult classification = {"address", 1.0};
314   std::vector<RemoteActionTemplate> intents;
315   EXPECT_TRUE(generator->GenerateIntents(
316       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
317       classification,
318       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
319       GetAndroidContext(),
320       /*annotations_entity_data_schema=*/nullptr,
321       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
322       &intents));
323   EXPECT_THAT(intents, SizeIs(0));
324 }
325 
TEST_F(IntentGeneratorTest,HandlesCallbacks)326 TEST_F(IntentGeneratorTest, HandlesCallbacks) {
327   flatbuffers::DetachedBuffer intent_factory_model =
328       BuildTestIntentFactoryModel("test", R"lua(
329 local test = external.entity["text"]
330 return {
331   {
332     data = "encoded=" .. external.android.urlencode(test),
333     category = { "test_category" },
334     extra = {
335       { name = "package", string_value = external.android.package_name},
336       { name = "scheme",
337         string_value = external.android.url_schema("https://google.com")},
338       { name = "host",
339         string_value = external.android.url_host("https://google.com/search")},
340       { name = "permission",
341         bool_value = external.android.user_restrictions["no_sms"] },
342       { name = "language",
343         string_value = external.android.device_locales[1].language },
344       { name = "description",
345         string_value = external.format("$1 $0", "hello", "world") },
346     },
347     request_code = external.hash(test)
348   }
349 })lua");
350   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
351       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
352       /*resources=*/resources_, jni_cache_);
353   ClassificationResult classification = {"test", 1.0};
354   std::vector<RemoteActionTemplate> intents;
355   EXPECT_TRUE(generator->GenerateIntents(
356       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
357       classification,
358       /*reference_time_ms_utc=*/0, "this is a test", {0, 14},
359       GetAndroidContext(),
360       /*annotations_entity_data_schema=*/nullptr,
361       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
362       &intents));
363   EXPECT_THAT(intents, SizeIs(1));
364   EXPECT_EQ(intents[0].data.value(), "encoded=this%20is%20a%20test");
365   EXPECT_THAT(intents[0].category, ElementsAre("test_category"));
366   EXPECT_THAT(intents[0].extra, SizeIs(6));
367   EXPECT_EQ(intents[0].extra["package"].ConstRefValue<std::string>(),
368             "com.google.android.textclassifier.tests"
369   );
370   EXPECT_EQ(intents[0].extra["scheme"].ConstRefValue<std::string>(), "https");
371   EXPECT_EQ(intents[0].extra["host"].ConstRefValue<std::string>(),
372             "google.com");
373   EXPECT_FALSE(intents[0].extra["permission"].Value<bool>());
374   EXPECT_EQ(intents[0].extra["language"].ConstRefValue<std::string>(), "en");
375   EXPECT_TRUE(intents[0].request_code.has_value());
376   EXPECT_EQ(intents[0].extra["description"].ConstRefValue<std::string>(),
377             "world hello");
378 }
379 
TEST_F(IntentGeneratorTest,HandlesActionIntentGeneration)380 TEST_F(IntentGeneratorTest, HandlesActionIntentGeneration) {
381   flatbuffers::DetachedBuffer intent_factory_model =
382       BuildTestIntentFactoryModel("view_map", R"lua(
383 return {
384   {
385     title_without_entity = external.android.R.map,
386     description = external.android.R.map_desc,
387     description_with_app_name = external.android.R.map,
388     action = "android.intent.action.VIEW",
389     data = "geo:0,0?q=" ..
390     external.android.urlencode(external.entity.annotation["location"].text),
391   }
392 })lua");
393   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
394       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
395       /*resources=*/resources_, jni_cache_);
396   Conversation conversation = {{{/*user_id=*/1, "hello there"}}};
397   ActionSuggestionAnnotation annotation;
398   annotation.entity = {"address", 1.0};
399   annotation.span = {/*message_index=*/0,
400                      /*span=*/{6, 11},
401                      /*text=*/"there"};
402   annotation.name = "location";
403   ActionSuggestion suggestion = {/*response_text=""*/ "",
404                                  /*type=*/"view_map",
405                                  /*score=*/1.0,
406                                  /*priority_score=*/0.0,
407                                  /*annotations=*/
408                                  {annotation}};
409   std::vector<RemoteActionTemplate> intents;
410   EXPECT_TRUE(generator->GenerateIntents(
411       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
412       suggestion, conversation, GetAndroidContext(),
413       /*annotations_entity_data_schema=*/nullptr,
414       /*actions_entity_data_schema=*/nullptr, &intents));
415   EXPECT_THAT(intents, SizeIs(1));
416   EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
417   EXPECT_EQ(intents[0].description.value(), "Locate selected address");
418   EXPECT_EQ(intents[0].description_with_app_name.value(), "Map");
419   EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
420   EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=there");
421 }
422 
TEST_F(IntentGeneratorTest,HandlesTimezoneAndReferenceTime)423 TEST_F(IntentGeneratorTest, HandlesTimezoneAndReferenceTime) {
424   flatbuffers::DetachedBuffer intent_factory_model =
425       BuildTestIntentFactoryModel("test", R"lua(
426 local conversation = external.conversation
427 return {
428   {
429     extra = {
430       { name = "timezone", string_value = conversation[#conversation].timezone },
431       { name = "num_messages", int_value = #conversation },
432       { name = "reference_time", long_value = conversation[#conversation].time_ms_utc }
433     },
434   }
435 })lua");
436   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
437       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
438       /*resources=*/resources_, jni_cache_);
439   Conversation conversation = {
440       {{/*user_id=*/0, "hello there", /*reference_time_ms_utc=*/0,
441         /*reference_timezone=*/"Testing/Test"},
442        {/*user_id=*/1, "general retesti", /*reference_time_ms_utc=*/1000,
443         /*reference_timezone=*/"Europe/Zurich"}}};
444   ActionSuggestion suggestion = {/*response_text=""*/ "",
445                                  /*type=*/"test",
446                                  /*score=*/1.0,
447                                  /*priority_score=*/0.0,
448                                  /*annotations=*/
449                                  {}};
450   std::vector<RemoteActionTemplate> intents;
451   EXPECT_TRUE(generator->GenerateIntents(
452       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
453       suggestion, conversation, GetAndroidContext(),
454       /*annotations_entity_data_schema=*/nullptr,
455       /*actions_entity_data_schema=*/nullptr, &intents));
456   EXPECT_THAT(intents, SizeIs(1));
457   EXPECT_EQ(intents[0].extra["timezone"].ConstRefValue<std::string>(),
458             "Europe/Zurich");
459   EXPECT_EQ(intents[0].extra["num_messages"].Value<int>(), 2);
460   EXPECT_EQ(intents[0].extra["reference_time"].Value<int64>(), 1000);
461 }
462 
TEST_F(IntentGeneratorTest,HandlesActionIntentGenerationMultipleAnnotations)463 TEST_F(IntentGeneratorTest, HandlesActionIntentGenerationMultipleAnnotations) {
464   flatbuffers::DetachedBuffer intent_factory_model =
465       BuildTestIntentFactoryModel("create_event", R"lua(
466 return {
467   {
468     title_without_entity = external.android.R.add_calendar_event,
469     description = external.android.R.add_calendar_event_desc,
470     extra = {
471       {name = "time", string_value =
472        external.entity.annotation["time"].text},
473       {name = "location",
474        string_value = external.entity.annotation["location"].text},
475     }
476   }
477 })lua");
478   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
479       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
480       /*resources=*/resources_, jni_cache_);
481   Conversation conversation = {{{/*user_id=*/1, "hello there at 1pm"}}};
482   ActionSuggestionAnnotation location_annotation, time_annotation;
483   location_annotation.entity = {"address", 1.0};
484   location_annotation.span = {/*message_index=*/0,
485                               /*span=*/{6, 11},
486                               /*text=*/"there"};
487   location_annotation.name = "location";
488   time_annotation.entity = {"datetime", 1.0};
489   time_annotation.span = {/*message_index=*/0,
490                           /*span=*/{15, 18},
491                           /*text=*/"1pm"};
492   time_annotation.name = "time";
493   ActionSuggestion suggestion = {/*response_text=""*/ "",
494                                  /*type=*/"create_event",
495                                  /*score=*/1.0,
496                                  /*priority_score=*/0.0,
497                                  /*annotations=*/
498                                  {location_annotation, time_annotation}};
499   std::vector<RemoteActionTemplate> intents;
500   EXPECT_TRUE(generator->GenerateIntents(
501       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
502       suggestion, conversation, GetAndroidContext(),
503       /*annotations_entity_data_schema=*/nullptr,
504       /*actions_entity_data_schema=*/nullptr, &intents));
505   EXPECT_THAT(intents, SizeIs(1));
506   EXPECT_EQ(intents[0].title_without_entity.value(), "Schedule");
507   EXPECT_THAT(intents[0].extra, SizeIs(2));
508   EXPECT_EQ(intents[0].extra["location"].ConstRefValue<std::string>(), "there");
509   EXPECT_EQ(intents[0].extra["time"].ConstRefValue<std::string>(), "1pm");
510 }
511 
TEST_F(IntentGeneratorTest,HandlesActionIntentGenerationMultipleAnnotationsWithIndices)512 TEST_F(IntentGeneratorTest,
513        HandlesActionIntentGenerationMultipleAnnotationsWithIndices) {
514   flatbuffers::DetachedBuffer intent_factory_model =
515       BuildTestIntentFactoryModel("time_range", R"lua(
516 return {
517   {
518     title_without_entity = "test",
519     description = "test",
520     extra = {
521       {name = "from", string_value = external.entity.annotation[1].text},
522       {name = "to", string_value = external.entity.annotation[2].text},
523     }
524   }
525 })lua");
526   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
527       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
528       /*resources=*/resources_, jni_cache_);
529   Conversation conversation = {{{/*user_id=*/1, "from 1pm to 2pm"}}};
530   ActionSuggestionAnnotation from_annotation, to_annotation;
531   from_annotation.entity = {"datetime", 1.0};
532   from_annotation.span = {/*message_index=*/0,
533                           /*span=*/{5, 8},
534                           /*text=*/"1pm"};
535   to_annotation.entity = {"datetime", 1.0};
536   to_annotation.span = {/*message_index=*/0,
537                         /*span=*/{12, 15},
538                         /*text=*/"2pm"};
539   ActionSuggestion suggestion = {/*response_text=""*/ "",
540                                  /*type=*/"time_range",
541                                  /*score=*/1.0,
542                                  /*priority_score=*/0.0,
543                                  /*annotations=*/
544                                  {from_annotation, to_annotation}};
545   std::vector<RemoteActionTemplate> intents;
546   EXPECT_TRUE(generator->GenerateIntents(
547       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
548       suggestion, conversation, GetAndroidContext(),
549       /*annotations_entity_data_schema=*/nullptr,
550       /*actions_entity_data_schema=*/nullptr, &intents));
551   EXPECT_THAT(intents, SizeIs(1));
552   EXPECT_THAT(intents[0].extra, SizeIs(2));
553   EXPECT_EQ(intents[0].extra["from"].ConstRefValue<std::string>(), "1pm");
554   EXPECT_EQ(intents[0].extra["to"].ConstRefValue<std::string>(), "2pm");
555 }
556 
TEST_F(IntentGeneratorTest,HandlesResources)557 TEST_F(IntentGeneratorTest, HandlesResources) {
558   flatbuffers::DetachedBuffer intent_factory_model =
559       BuildTestIntentFactoryModel("address", R"lua(
560 return {
561   {
562     title_without_entity = external.android.R.map,
563     description = external.android.R.map_desc,
564     action = "android.intent.action.VIEW",
565     data = "geo:0,0?q=" ..
566     external.android.urlencode(external.entity.text),
567   }
568 })lua");
569   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
570       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
571       resources_, jni_cache_);
572   ClassificationResult classification = {"address", 1.0};
573   std::vector<RemoteActionTemplate> intents;
574   EXPECT_TRUE(generator->GenerateIntents(
575       JniHelper::NewStringUTF(GetJenv(), "de-DE").ValueOrDie().get(),
576       classification,
577       /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
578       GetAndroidContext(),
579       /*annotations_entity_data_schema=*/nullptr,
580       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
581       &intents));
582   EXPECT_THAT(intents, SizeIs(1));
583   EXPECT_EQ(intents[0].title_without_entity.value(), "Karte");
584   EXPECT_EQ(intents[0].description.value(), "Ausgewählte Adresse finden");
585   EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
586   EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=333%20E%20Wonderview%20Ave");
587 }
588 
TEST_F(IntentGeneratorTest,HandlesIteration)589 TEST_F(IntentGeneratorTest, HandlesIteration) {
590   flatbuffers::DetachedBuffer intent_factory_model =
591       BuildTestIntentFactoryModel("iteration_test", R"lua(
592 local extra = {{ name = "length", int_value = #external.entity.annotation }}
593 for annotation_id, annotation in pairs(external.entity.annotation) do
594   table.insert(extra,
595     { name = annotation.name,
596       string_value = annotation.text })
597 end
598 return {{ extra = extra }})lua");
599   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
600       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
601       /*resources=*/resources_, jni_cache_);
602   Conversation conversation = {{{/*user_id=*/1, "hello there"}}};
603   ActionSuggestionAnnotation location_annotation;
604   location_annotation.entity = {"address", 1.0};
605   location_annotation.span = {/*message_index=*/0,
606                               /*span=*/{6, 11},
607                               /*text=*/"there"};
608   location_annotation.name = "location";
609   ActionSuggestionAnnotation greeting_annotation;
610   greeting_annotation.entity = {"greeting", 1.0};
611   greeting_annotation.span = {/*message_index=*/0,
612                               /*span=*/{0, 5},
613                               /*text=*/"hello"};
614   greeting_annotation.name = "greeting";
615   ActionSuggestion suggestion = {/*response_text=""*/ "",
616                                  /*type=*/"iteration_test",
617                                  /*score=*/1.0,
618                                  /*priority_score=*/0.0,
619                                  /*annotations=*/
620                                  {location_annotation, greeting_annotation}};
621   std::vector<RemoteActionTemplate> intents;
622   EXPECT_TRUE(generator->GenerateIntents(
623       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
624       suggestion, conversation, GetAndroidContext(),
625       /*annotations_entity_data_schema=*/nullptr,
626       /*actions_entity_data_schema=*/nullptr, &intents));
627   EXPECT_THAT(intents, SizeIs(1));
628   EXPECT_EQ(intents[0].extra["length"].Value<int>(), 2);
629   EXPECT_EQ(intents[0].extra["location"].ConstRefValue<std::string>(), "there");
630   EXPECT_EQ(intents[0].extra["greeting"].ConstRefValue<std::string>(), "hello");
631 }
632 
TEST_F(IntentGeneratorTest,HandlesEntityDataLookups)633 TEST_F(IntentGeneratorTest, HandlesEntityDataLookups) {
634   flatbuffers::DetachedBuffer intent_factory_model =
635       BuildTestIntentFactoryModel("fake", R"lua(
636 local person = external.entity.person
637 return {
638   {
639     title_without_entity = "Add to contacts",
640     extra = {
641       {name = "name", string_value = string.lower(person.name)},
642       {name = "encoded_phone", string_value = external.android.urlencode(person.phone)},
643       {name = "age", int_value = person.age_years},
644     }
645   }
646 })lua");
647 
648   // Create fake entity data schema meta data.
649   // Cannot use object oriented API here as that is not available for the
650   // reflection schema.
651   flatbuffers::FlatBufferBuilder schema_builder;
652   std::vector<flatbuffers::Offset<reflection::Field>> person_fields = {
653       reflection::CreateField(
654           schema_builder,
655           /*name=*/schema_builder.CreateString("name"),
656           /*type=*/
657           reflection::CreateType(schema_builder,
658                                  /*base_type=*/reflection::String),
659           /*id=*/0,
660           /*offset=*/4),
661       reflection::CreateField(
662           schema_builder,
663           /*name=*/schema_builder.CreateString("phone"),
664           /*type=*/
665           reflection::CreateType(schema_builder,
666                                  /*base_type=*/reflection::String),
667           /*id=*/1,
668           /*offset=*/6),
669       reflection::CreateField(
670           schema_builder,
671           /*name=*/schema_builder.CreateString("age_years"),
672           /*type=*/
673           reflection::CreateType(schema_builder,
674                                  /*base_type=*/reflection::Int),
675           /*id=*/2,
676           /*offset=*/8),
677   };
678   std::vector<flatbuffers::Offset<reflection::Field>> entity_data_fields = {
679       reflection::CreateField(
680           schema_builder,
681           /*name=*/schema_builder.CreateString("person"),
682           /*type=*/
683           reflection::CreateType(schema_builder,
684                                  /*base_type=*/reflection::Obj,
685                                  /*element=*/reflection::None,
686                                  /*index=*/1),
687           /*id=*/0,
688           /*offset=*/4)};
689   std::vector<flatbuffers::Offset<reflection::Enum>> enums;
690   std::vector<flatbuffers::Offset<reflection::Object>> objects = {
691       reflection::CreateObject(
692           schema_builder,
693           /*name=*/schema_builder.CreateString("EntityData"),
694           /*fields=*/
695           schema_builder.CreateVectorOfSortedTables(&entity_data_fields)),
696       reflection::CreateObject(
697           schema_builder,
698           /*name=*/schema_builder.CreateString("person"),
699           /*fields=*/
700           schema_builder.CreateVectorOfSortedTables(&person_fields))};
701   schema_builder.Finish(reflection::CreateSchema(
702       schema_builder, schema_builder.CreateVectorOfSortedTables(&objects),
703       schema_builder.CreateVectorOfSortedTables(&enums),
704       /*(unused) file_ident=*/0,
705       /*(unused) file_ext=*/0,
706       /*root_table*/ objects[0]));
707   const reflection::Schema* entity_data_schema =
708       flatbuffers::GetRoot<reflection::Schema>(
709           schema_builder.GetBufferPointer());
710 
711   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
712       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
713       /*resources=*/resources_, jni_cache_);
714 
715   ClassificationResult classification = {"fake", 1.0};
716 
717   // Build test entity data.
718   MutableFlatbufferBuilder entity_data_builder(entity_data_schema);
719   std::unique_ptr<MutableFlatbuffer> entity_data_buffer =
720       entity_data_builder.NewRoot();
721   MutableFlatbuffer* person = entity_data_buffer->Mutable("person");
722   person->Set("name", "Kenobi");
723   person->Set("phone", "1 800 HIGHGROUND");
724   person->Set("age_years", 38);
725   classification.serialized_entity_data = entity_data_buffer->Serialize();
726 
727   std::vector<RemoteActionTemplate> intents;
728   EXPECT_TRUE(generator->GenerateIntents(
729       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
730       classification,
731       /*reference_time_ms_utc=*/0, "highground", {0, 10}, GetAndroidContext(),
732       /*annotations_entity_data_schema=*/entity_data_schema,
733       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
734       &intents));
735   EXPECT_THAT(intents, SizeIs(1));
736   EXPECT_THAT(intents[0].extra, SizeIs(3));
737   EXPECT_EQ(intents[0].extra["name"].ConstRefValue<std::string>(), "kenobi");
738   EXPECT_EQ(intents[0].extra["encoded_phone"].ConstRefValue<std::string>(),
739             "1%20800%20HIGHGROUND");
740   EXPECT_EQ(intents[0].extra["age"].Value<int>(), 38);
741 }
742 
TEST_F(IntentGeneratorTest,ReadExtras)743 TEST_F(IntentGeneratorTest, ReadExtras) {
744   flatbuffers::DetachedBuffer intent_factory_model =
745       BuildTestIntentFactoryModel("test", R"lua(
746         return {
747           {
748             extra = {
749               { name = "languages", string_array_value = {"en", "zh"}},
750               { name = "scores", float_array_value = {0.6, 0.4}},
751               { name = "ints", int_array_value = {7, 2, 1}},
752               { name = "bundle",
753               named_variant_array_value =
754               {
755                 { name = "inner_string", string_value = "a" },
756                 { name = "inner_int", int_value = 42 }
757               }
758             }
759           }
760         }}
761   )lua");
762   std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
763       flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
764       /*resources=*/resources_, jni_cache_);
765   const ClassificationResult classification = {"test", 1.0};
766   std::vector<RemoteActionTemplate> intents;
767 
768   EXPECT_TRUE(generator->GenerateIntents(
769       JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
770       classification,
771       /*reference_time_ms_utc=*/0, "test", {0, 4}, GetAndroidContext(),
772       /*annotations_entity_data_schema=*/nullptr,
773       /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
774       &intents));
775 
776   EXPECT_THAT(intents, SizeIs(1));
777   RemoteActionTemplate intent = intents[0];
778   EXPECT_THAT(intent.extra, SizeIs(4));
779   EXPECT_THAT(
780       intent.extra["languages"].ConstRefValue<std::vector<std::string>>(),
781       ElementsAre("en", "zh"));
782   EXPECT_THAT(intent.extra["scores"].ConstRefValue<std::vector<float>>(),
783               ElementsAre(0.6, 0.4));
784   EXPECT_THAT(intent.extra["ints"].ConstRefValue<std::vector<int>>(),
785               ElementsAre(7, 2, 1));
786   const std::map<std::string, Variant>& map =
787       intent.extra["bundle"].ConstRefValue<std::map<std::string, Variant>>();
788   EXPECT_THAT(map, SizeIs(2));
789   EXPECT_EQ(map.at("inner_string").ConstRefValue<std::string>(), "a");
790   EXPECT_EQ(map.at("inner_int").Value<int>(), 42);
791 }
792 
793 }  // namespace
794 }  // namespace libtextclassifier3
795