1 /*
2 * Copyright (C) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "core/common/ai/data_detector_adapter.h"
17
18 #include "adapter/ohos/entrance/ace_container.h"
19 #include "base/utils/utf_helper.h"
20 #include "core/common/ai/data_detector_mgr.h"
21 #include "core/common/ai/data_url_analyzer_mgr.h"
22 #include "core/components/text_overlay/text_overlay_theme.h"
23 #include "core/pipeline_ng/pipeline_context.h"
24
25 namespace OHOS::Ace {
26
27 constexpr int32_t AI_TEXT_MAX_LENGTH = 500;
28 constexpr int32_t AI_TEXT_GAP = 100;
29 constexpr int32_t AI_DELAY_TIME = 100;
30 constexpr uint32_t SECONDS_TO_MILLISECONDS = 1000;
31 constexpr uint8_t URL_DETECT_FINISH = (1 << 0);
32 constexpr uint8_t OTHER_DETECT_FINISH = (1 << 1);
33 constexpr uint8_t ALL_DETECT_FINISH = URL_DETECT_FINISH | OTHER_DETECT_FINISH;
34
35 const std::string ALL_TEXT_DETECT_TYPES = "phoneNum,url,email,location,datetime";
36 const std::string TEXT_DETECT_TYPES_WITHOUT_URL = "phoneNum,email,location,datetime";
37 const std::string ASK_CELIA_TAG = "askCelia";
38
39 const std::unordered_map<TextDataDetectType, std::string> TEXT_DETECT_MAP = {
40 { TextDataDetectType::PHONE_NUMBER, "phoneNum" }, { TextDataDetectType::URL, "url" },
41 { TextDataDetectType::EMAIL, "email" }, { TextDataDetectType::ADDRESS, "location" },
42 { TextDataDetectType::DATE_TIME, "datetime" }
43 };
44 const std::unordered_map<std::string, TextDataDetectType> TEXT_DETECT_MAP_REVERSE = {
45 { "phoneNum", TextDataDetectType::PHONE_NUMBER }, { "url", TextDataDetectType::URL },
46 { "email", TextDataDetectType::EMAIL }, { "location", TextDataDetectType::ADDRESS },
47 { "datetime", TextDataDetectType::DATE_TIME }
48 };
49
GetAIEntityMenu()50 void DataDetectorAdapter::GetAIEntityMenu()
51 {
52 auto context = PipelineContext::GetCurrentContextSafely();
53 CHECK_NULL_VOID(context);
54 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
55 uiTaskExecutor.PostTask(
56 [weak = AceType::WeakClaim(this), instanceId = context->GetInstanceId()] {
57 ContainerScope scope(instanceId);
58 auto dataDetectorAdapter = weak.Upgrade();
59 CHECK_NULL_VOID(dataDetectorAdapter);
60 TAG_LOGI(AceLogTag::ACE_TEXT, "Get AI entity menu from ai_engine");
61 DataDetectorMgr::GetInstance().GetAIEntityMenu(dataDetectorAdapter->textDetectResult_);
62 },
63 "ArkUITextInitDataDetect");
64 }
65
ShowAIEntityMenu(const AISpan & aiSpan,const NG::RectF & aiRect,const RefPtr<NG::FrameNode> & targetNode,AIMenuInfo info)66 bool DataDetectorAdapter::ShowAIEntityMenu(
67 const AISpan& aiSpan, const NG::RectF& aiRect, const RefPtr<NG::FrameNode>& targetNode, AIMenuInfo info)
68 {
69 std::vector<std::pair<std::string, std::function<void()>>> menuOptions;
70 if (!GetAiEntityMenuOptions(aiSpan, targetNode, info, menuOptions)) {
71 return false;
72 }
73 auto pipeline = NG::PipelineContext::GetCurrentContextSafelyWithCheck();
74 CHECK_NULL_RETURN(pipeline, false);
75 auto overlayManager = pipeline->GetOverlayManager();
76 CHECK_NULL_RETURN(overlayManager, false);
77 return overlayManager->ShowAIEntityMenu(menuOptions, aiRect, targetNode);
78 }
79
GetAiEntityMenuOptions(const AISpan & aiSpan,const RefPtr<NG::FrameNode> & targetNode,AIMenuInfo info,std::vector<std::pair<std::string,std::function<void ()>>> & menuOptions)80 bool DataDetectorAdapter::GetAiEntityMenuOptions(const AISpan& aiSpan, const RefPtr<NG::FrameNode>& targetNode,
81 AIMenuInfo info, std::vector<std::pair<std::string, std::function<void()>>>& menuOptions)
82 {
83 if (textDetectResult_.menuOptionAndAction.empty()) {
84 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
85 GetAIEntityMenu();
86 return false;
87 }
88
89 mainContainerId_ = Container::CurrentId();
90 auto menuOptionAndAction = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
91 if (menuOptionAndAction.empty()) {
92 return false;
93 }
94 if (!info.isShowSelectText) {
95 // delete the last option: selectText.
96 menuOptionAndAction.pop_back();
97 if (!info.isShowCopy) {
98 // delete the last option: copy.
99 menuOptionAndAction.pop_back();
100 }
101 }
102
103 auto index = -1;
104 for (auto menuOption : menuOptionAndAction) {
105 ++index;
106 std::function<void()> onClickEvent = [aiSpan, menuOption, weak = AceType::WeakClaim(this),
107 targetNodeWeak = AceType::WeakClaim(AceType::RawPtr(targetNode)),
108 mainId = Container::CurrentIdSafelyWithCheck(), index]() {
109 ContainerScope scope(mainId);
110 auto dataDetectorAdapter = weak.Upgrade();
111 CHECK_NULL_VOID(dataDetectorAdapter);
112 auto targetNode = targetNodeWeak.Upgrade();
113 CHECK_NULL_VOID(targetNode);
114 dataDetectorAdapter->OnClickAIMenuOption(aiSpan, menuOption, targetNode);
115 };
116 menuOptions.push_back(std::make_pair(menuOption.first, onClickEvent));
117 }
118 return true;
119 }
120
GetPreviewMenuOptionCallback(TextDataDetectType type,const std::string & content)121 std::function<void()> DataDetectorAdapter::GetPreviewMenuOptionCallback(
122 TextDataDetectType type, const std::string& content)
123 {
124 return [content, type, mainId = Container::CurrentIdSafelyWithCheck()]() {
125 ContainerScope scope(mainId);
126 auto pipeline = NG::PipelineContext::GetCurrentContextSafelyWithCheck();
127 CHECK_NULL_VOID(pipeline);
128 auto fontManager = pipeline->GetFontManager();
129 CHECK_NULL_VOID(fontManager);
130 fontManager->OnPreviewMenuOptionClick(type, content);
131 };
132 }
133
CreateAIEntityMenu(const AISpan & aiSpan,const RefPtr<NG::FrameNode> & targetNode,AIMenuInfo info)134 RefPtr<NG::FrameNode> DataDetectorAdapter::CreateAIEntityMenu(
135 const AISpan& aiSpan, const RefPtr<NG::FrameNode>& targetNode, AIMenuInfo info)
136 {
137 std::vector<std::pair<std::string, std::function<void()>>> menuOptions;
138 if (!GetAiEntityMenuOptions(aiSpan, targetNode, info, menuOptions)) {
139 return nullptr;
140 }
141 CHECK_NULL_RETURN(targetNode, nullptr);
142 auto pipeline = NG::PipelineContext::GetCurrentContextSafelyWithCheck();
143 CHECK_NULL_RETURN(pipeline, nullptr);
144 auto overlayManager = pipeline->GetOverlayManager();
145 CHECK_NULL_RETURN(overlayManager, nullptr);
146 auto theme = pipeline->GetTheme<TextOverlayTheme>();
147 CHECK_NULL_RETURN(theme, nullptr);
148 auto name = theme->GetAiMenuPreviewOptionName(aiSpan.type);
149 if (!menuOptions.empty() && !name.empty()) {
150 auto& option = menuOptions.front();
151 option.first = name;
152 option.second = GetPreviewMenuOptionCallback(aiSpan.type, aiSpan.content);
153 }
154 auto menuNode = overlayManager->BuildAIEntityMenu(menuOptions);
155 CHECK_NULL_RETURN(menuNode, nullptr);
156 return menuNode;
157 }
158
OnClickAIMenuOption(const AISpan & aiSpan,const std::pair<std::string,FuncVariant> & menuOption,const RefPtr<NG::FrameNode> & targetNode)159 void DataDetectorAdapter::OnClickAIMenuOption(const AISpan& aiSpan,
160 const std::pair<std::string, FuncVariant>& menuOption, const RefPtr<NG::FrameNode>& targetNode)
161 {
162 TAG_LOGI(AceLogTag::ACE_TEXT, "Click AI menu option: %{public}s", menuOption.first.c_str());
163 auto pipeline = NG::PipelineContext::GetCurrentContextSafelyWithCheck();
164 CHECK_NULL_VOID(pipeline);
165 auto overlayManager = pipeline->GetOverlayManager();
166 CHECK_NULL_VOID(overlayManager);
167 if (targetNode) {
168 overlayManager->CloseAIEntityMenu(targetNode->GetId());
169 }
170 if (mainContainerId_ == -1) {
171 mainContainerId_ = Container::CurrentId();
172 }
173 Container::UpdateCurrent(mainContainerId_);
174
175 auto runtimeContext = Platform::AceContainer::GetRuntimeContext(pipeline->GetInstanceId());
176 CHECK_NULL_VOID(runtimeContext);
177 auto token = runtimeContext->GetToken();
178 auto bundleName = runtimeContext->GetBundleName();
179
180 hasClickedMenuOption_ = true;
181 if (aiSpan.type == TextDataDetectType::ASK_CELIA) {
182 auto vectorStringFunc = textDetectResult_.menuOptionAndAction.find(ASK_CELIA_TAG);
183 if (vectorStringFunc == textDetectResult_.menuOptionAndAction.end()) {
184 TAG_LOGW(AceLogTag::ACE_TEXT, "No askCelia option");
185 } else {
186 auto funcVariant = vectorStringFunc->second.begin()->second;
187 if (std::holds_alternative<std::function<void(int, std::string)>>(funcVariant) &&
188 std::get<std::function<void(int, std::string)>>(funcVariant)) {
189 TAG_LOGI(AceLogTag::ACE_TEXT, "DataDetectorAdapter::OnClickAIMenuOption, call ask celia");
190 std::get<std::function<void(int, std::string)>>(funcVariant)(true, aiSpan.content);
191 }
192 }
193 hasClickedMenuOption_ = false;
194 return;
195 }
196 if (onClickMenu_ && std::holds_alternative<std::function<std::string()>>(menuOption.second)) {
197 onClickMenu_(std::get<std::function<std::string()>>(menuOption.second)());
198 } else if (std::holds_alternative<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)) {
199 std::get<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)(token, aiSpan.content);
200 } else if (std::holds_alternative<std::function<void(int32_t, std::string)>>(menuOption.second)) {
201 std::get<std::function<void(int32_t, std::string)>>(menuOption.second)(mainContainerId_, aiSpan.content);
202 } else if (std::holds_alternative<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(
203 menuOption.second)) {
204 std::get<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(menuOption.second)(
205 mainContainerId_, UtfUtils::Str16DebugToStr8(textForAI_), bundleName, aiSpan.start, aiSpan.content);
206 TAG_LOGI(AceLogTag::ACE_TEXT, "textForAI:%{public}d, start:%{public}d, aiSpan.length:%{public}d",
207 static_cast<int32_t>(textForAI_.length()), aiSpan.start, static_cast<int32_t>(aiSpan.content.length()));
208 } else {
209 TAG_LOGW(AceLogTag::ACE_TEXT, "No matching menu option");
210 }
211 hasClickedMenuOption_ = false;
212 }
213
ResponseBestMatchItem(const AISpan & aiSpan)214 void DataDetectorAdapter::ResponseBestMatchItem(const AISpan& aiSpan)
215 {
216 if (textDetectResult_.menuOptionAndAction.empty()) {
217 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
218 GetAIEntityMenu();
219 return;
220 }
221 auto menuOptions = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
222 if (menuOptions.empty()) {
223 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty");
224 return;
225 }
226 OnClickAIMenuOption(aiSpan, menuOptions[0]);
227 }
228
SetTextDetectTypes(const std::string & textDetectTypes)229 void DataDetectorAdapter::SetTextDetectTypes(const std::string& textDetectTypes)
230 {
231 auto types = textDetectTypes;
232 if (types.empty()) {
233 types.append(ALL_TEXT_DETECT_TYPES);
234 }
235
236 std::set<std::string> newTypesSet;
237 std::istringstream iss(types);
238 std::string type;
239 while (std::getline(iss, type, ',')) {
240 newTypesSet.insert(type);
241 }
242 std::string detectTypesResult;
243 bool hasUrlType = false;
244 std::for_each(newTypesSet.begin(), newTypesSet.end(), [&hasUrlType, &detectTypesResult](const std::string& type) {
245 if (type == "url") {
246 hasUrlType = true;
247 } else {
248 detectTypesResult.append(type).append(",");
249 }
250 });
251 if (!detectTypesResult.empty()) {
252 detectTypesResult.pop_back();
253 }
254 hasUrlType_ = hasUrlType;
255 textDetectTypes_ = detectTypesResult;
256 if (newTypesSet != textDetectTypesSet_) {
257 textDetectTypesSet_ = newTypesSet;
258 typeChanged_ = true;
259 aiDetectInitialized_ = false;
260 MarkDirtyNode();
261 }
262 }
263
ParseOriText(const std::unique_ptr<JsonValue> & entityJson,std::u16string & text)264 bool DataDetectorAdapter::ParseOriText(const std::unique_ptr<JsonValue>& entityJson, std::u16string& text)
265 {
266 TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text entry");
267 auto runtimeContext = Platform::AceContainer::GetRuntimeContext(Container::CurrentId());
268 CHECK_NULL_RETURN(runtimeContext, false);
269 if (runtimeContext->GetBundleName() != entityJson->GetString("bundleName")) {
270 TAG_LOGW(AceLogTag::ACE_TEXT,
271 "Wrong bundleName, the context bundleName is: %{public}s, but your bundleName is: %{public}s",
272 runtimeContext->GetBundleName().c_str(), entityJson->GetString("bundleName").c_str());
273 return false;
274 }
275 auto aiSpanArray = entityJson->GetValue("entity");
276 if (aiSpanArray->IsNull() || !aiSpanArray->IsArray()) {
277 TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI entity");
278 return false;
279 }
280
281 aiSpanMap_.clear();
282 aiSpanRects_.clear();
283 detectTexts_.clear();
284 AISpan aiSpan;
285 for (int32_t i = 0; i < aiSpanArray->GetArraySize(); ++i) {
286 auto item = aiSpanArray->GetArrayItem(i);
287 aiSpan.content = item->GetString("entityContent");
288 aiSpan.type = TEXT_DETECT_MAP_REVERSE.at(item->GetString("entityType"));
289 aiSpan.start = item->GetInt("start");
290 aiSpan.end = item->GetInt("end");
291 aiSpanMap_[aiSpan.start] = aiSpan;
292 }
293 aiDetectInitialized_ = true;
294 text = UtfUtils::Str8ToStr16(entityJson->GetString("content"));
295 textForAI_ = text;
296 lastTextForAI_ = textForAI_;
297 if (textDetectResult_.menuOptionAndAction.empty()) {
298 GetAIEntityMenu();
299 }
300
301 TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text successful");
302 return true;
303 }
304
PreprocessTextDetect()305 void DataDetectorAdapter::PreprocessTextDetect()
306 {
307 aiDetectFlag_ = 0;
308 if (textDetectTypes_.empty() && !hasUrlType_) {
309 textDetectTypes_ = TEXT_DETECT_TYPES_WITHOUT_URL;
310 hasUrlType_ = true;
311 }
312 if (textDetectTypes_.empty()) {
313 aiDetectFlag_ |= OTHER_DETECT_FINISH;
314 }
315 if (!hasUrlType_) {
316 aiDetectFlag_ |= URL_DETECT_FINISH;
317 }
318 }
319
InitTextDetect(int32_t startPos,std::string detectText)320 void DataDetectorAdapter::InitTextDetect(int32_t startPos, std::string detectText)
321 {
322 CHECK_NULL_VOID(!textDetectTypes_.empty());
323 TextDataDetectInfo info;
324 info.text = detectText;
325 info.module = textDetectTypes_;
326
327 auto context = PipelineContext::GetCurrentContextSafelyWithCheck();
328 CHECK_NULL_VOID(context);
329 int32_t instanceID = context->GetInstanceId();
330 auto textFunc = [weak = WeakClaim(this),
331 instanceID, startPos, detectTypesSet = textDetectTypesSet_](const TextDataDetectResult result) {
332 ContainerScope scope(instanceID);
333 auto context = PipelineContext::GetCurrentContextSafelyWithCheck();
334 CHECK_NULL_VOID(context);
335 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
336 uiTaskExecutor.PostTask(
337 [result, weak, instanceID, startPos, detectTypesSet] {
338 ContainerScope scope(instanceID);
339 auto dataDetectorAdapter = weak.Upgrade();
340 CHECK_NULL_VOID(dataDetectorAdapter);
341 if (detectTypesSet != dataDetectorAdapter->textDetectTypesSet_) {
342 return;
343 }
344 dataDetectorAdapter->ParseAIResult(result, startPos);
345 dataDetectorAdapter->MarkDirtyNode();
346 },
347 "ArkUITextParseAIResult");
348 };
349
350 auto backgroundExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::BACKGROUND);
351 backgroundExecutor.PostTask(
352 [info, textFunc] {
353 TAG_LOGI(AceLogTag::ACE_TEXT, "DataDetectorAdapter::InitTextDetect, start AI detect, length: %{public}zu",
354 info.text.size());
355 DataDetectorMgr::GetInstance().DataDetect(info, textFunc);
356 },
357 "ArkUITextInitDataDetect");
358 }
359
HandleTextUrlDetect()360 void DataDetectorAdapter::HandleTextUrlDetect()
361 {
362 auto context = PipelineContext::GetCurrentContextSafelyWithCheck();
363 CHECK_NULL_VOID(context);
364 int32_t instanceID = context->GetInstanceId();
365 auto textFunc = [weak = WeakClaim(this),
366 instanceID, detectTypesSet = textDetectTypesSet_](const std::vector<UrlEntity>& urlEntities) {
367 ContainerScope scope(instanceID);
368 auto context = PipelineContext::GetCurrentContextSafelyWithCheck();
369 CHECK_NULL_VOID(context);
370 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
371 uiTaskExecutor.PostTask(
372 [urlEntities, weak, instanceID, detectTypesSet] {
373 ContainerScope scope(instanceID);
374 auto dataDetectorAdapter = weak.Upgrade();
375 CHECK_NULL_VOID(dataDetectorAdapter);
376 if (detectTypesSet != dataDetectorAdapter->textDetectTypesSet_) {
377 return;
378 }
379 dataDetectorAdapter->HandleUrlResult(urlEntities);
380 dataDetectorAdapter->MarkDirtyNode();
381 },
382 "ArkUITextUrlParseResult");
383 };
384
385 auto backgroundExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::BACKGROUND);
386 backgroundExecutor.PostTask(
387 [text = UtfUtils::Str16DebugToStr8(textForAI_), func = std::move(textFunc)] {
388 TAG_LOGI(AceLogTag::ACE_TEXT, "Start url entity detect using AI");
389 func(DataUrlAnalyzerMgr::GetInstance().AnalyzeUrls(text));
390 },
391 "ArkUITextInitUrlDetect");
392 }
393
HandleUrlResult(std::vector<UrlEntity> urlEntities)394 void DataDetectorAdapter::HandleUrlResult(std::vector<UrlEntity> urlEntities)
395 {
396 for (const auto& entity : urlEntities) {
397 auto iter = aiSpanMap_.find(entity.charOffset);
398 if (iter != aiSpanMap_.end() && iter->second.content.length() >= entity.text.length()) {
399 // both entities start at the same position, leaving the longer one
400 continue;
401 }
402 AISpan aiSpan;
403 aiSpan.start = entity.charOffset;
404 aiSpan.end = aiSpan.start + static_cast<int32_t>(UtfUtils::Str8DebugToStr16(entity.text).length());
405 aiSpan.content = entity.text;
406 aiSpan.type = TextDataDetectType::URL;
407 aiSpanMap_[aiSpan.start] = aiSpan;
408 }
409 aiDetectFlag_ |= URL_DETECT_FINISH;
410 if (aiDetectFlag_ == ALL_DETECT_FINISH) {
411 FireFinalResult();
412 }
413 }
414
ParseAIResult(const TextDataDetectResult & result,int32_t startPos)415 void DataDetectorAdapter::ParseAIResult(const TextDataDetectResult& result, int32_t startPos)
416 {
417 TAG_LOGI(AceLogTag::ACE_TEXT, "DataDetectorAdapter::ParseAIResult, ResultLength: %{public}zu, id: %{public}i",
418 result.entity.size(), GetHost() ? GetHost()->GetId() : -1);
419 auto entityJson = JsonUtil::ParseJsonString(result.entity);
420 CHECK_NULL_VOID(entityJson);
421 for (const auto& type : TEXT_DETECT_MAP) {
422 auto jsonValue = entityJson->GetValue(type.second);
423 ParseAIJson(jsonValue, type.first, startPos);
424 }
425
426 if (startPos + AI_TEXT_MAX_LENGTH >= static_cast<int32_t>(textForAI_.length())) {
427 SetTextDetectResult(result);
428 aiDetectFlag_ |= OTHER_DETECT_FINISH;
429 if (aiDetectFlag_ == ALL_DETECT_FINISH) {
430 FireFinalResult();
431 }
432 }
433 }
434
FireFinalResult()435 void DataDetectorAdapter::FireFinalResult()
436 {
437 aiDetectInitialized_ = true;
438 auto entityJsonArray = JsonUtil::CreateArray(true);
439 // process with overlapping entities, leaving only the earlier ones
440 int32_t preEnd = 0;
441 auto aiSpanIterator = aiSpanMap_.begin();
442 while (aiSpanIterator != aiSpanMap_.end()) {
443 auto aiSpan = aiSpanIterator->second;
444 if (aiSpan.start < preEnd) {
445 aiSpanIterator = aiSpanMap_.erase(aiSpanIterator);
446 } else {
447 preEnd = aiSpan.end;
448 ++aiSpanIterator;
449 auto aiSpanJson = JsonUtil::Create(true);
450 aiSpanJson->Put("start", aiSpan.start);
451 aiSpanJson->Put("end", aiSpan.end);
452 aiSpanJson->Put("entityContent", aiSpan.content.c_str());
453 aiSpanJson->Put("entityType", TEXT_DETECT_MAP.at(aiSpan.type).c_str());
454 entityJsonArray->Put(aiSpanJson);
455 }
456 }
457 auto resultJson = JsonUtil::Create(true);
458 resultJson->Put("entity", entityJsonArray);
459 resultJson->Put("code", textDetectTypes_.empty() && hasUrlType_ ? 0: textDetectResult_.code);
460 FireOnResult(resultJson->ToString());
461 aiDetectFlag_ = 0;
462 }
463
ParseAIJson(const std::unique_ptr<JsonValue> & jsonValue,TextDataDetectType type,int32_t startPos)464 void DataDetectorAdapter::ParseAIJson(
465 const std::unique_ptr<JsonValue>& jsonValue, TextDataDetectType type, int32_t startPos)
466 {
467 if (!jsonValue || !jsonValue->IsArray()) {
468 TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI result");
469 return;
470 }
471
472 for (int32_t i = 0; i < jsonValue->GetArraySize(); ++i) {
473 auto item = jsonValue->GetArrayItem(i);
474 auto charOffset = item->GetInt("charOffset");
475 auto oriText = item->GetString("oriText");
476 auto wOriText = UtfUtils::Str8ToStr16(oriText);
477 auto startTimestamp = item->GetInt64("startTimestamp", -1);
478 int32_t end = startPos + charOffset + static_cast<int32_t>(wOriText.length());
479 if (charOffset < 0 || startPos + charOffset >= static_cast<int32_t>(textForAI_.length()) ||
480 end >= startPos + AI_TEXT_MAX_LENGTH || oriText.empty()) {
481 TAG_LOGW(AceLogTag::ACE_TEXT, "The result of AI is wrong");
482 continue;
483 }
484 if (wOriText != textForAI_.substr(startPos + charOffset, static_cast<int32_t>(wOriText.length()))) {
485 TAG_LOGW(AceLogTag::ACE_TEXT, "The charOffset is wrong");
486 continue;
487 }
488 int32_t start = startPos + charOffset;
489 auto iter = aiSpanMap_.find(start);
490 if (iter != aiSpanMap_.end() && iter->second.content.length() > oriText.length()) {
491 // both entities start at the same position, leaving the longer one
492 continue;
493 }
494
495 TimeStamp currentDetectorTimeStamp = std::chrono::high_resolution_clock::now();
496 std::chrono::duration<float, std::ratio<1, SECONDS_TO_MILLISECONDS>> costTime =
497 currentDetectorTimeStamp - startDetectorTimeStamp_;
498 item->Put("costTime", costTime.count());
499 item->Put("resultCode", textDetectResult_.code);
500 entityJson_[start] = item->ToString();
501 TAG_LOGI(AceLogTag::ACE_TEXT, "size of the entity json is: %{public}zu", entityJson_[start].size());
502
503 AISpan aiSpan;
504 aiSpan.start = start;
505 aiSpan.end = end;
506 aiSpan.content = oriText;
507 aiSpan.type = type;
508 if (startTimestamp != -1) {
509 aiSpan.params["startTimestamp"] = std::to_string(startTimestamp);
510 }
511 aiSpanMap_[aiSpan.start] = aiSpan;
512 }
513 }
514
GetDetectDelayTask(const std::map<int32_t,AISpan> & aiSpanMap)515 std::function<void()> DataDetectorAdapter::GetDetectDelayTask(const std::map<int32_t, AISpan>& aiSpanMap)
516 {
517 return [aiSpanMap, weak = WeakClaim(this)]() {
518 auto dataDetectorAdapter = weak.Upgrade();
519 CHECK_NULL_VOID(dataDetectorAdapter && !dataDetectorAdapter->textForAI_.empty());
520 TAG_LOGI(AceLogTag::ACE_TEXT, "DataDetectorAdapter, delayed whole task executed, id: %{public}i",
521 dataDetectorAdapter->GetHost() ? dataDetectorAdapter->GetHost()->GetId() : -1);
522 dataDetectorAdapter->lastTextForAI_ = dataDetectorAdapter->textForAI_;
523 size_t detectTextIdx = 0;
524 auto aiSpanMapIt = aiSpanMap.begin();
525 int32_t startPos = 0;
526 bool hasSame = false;
527 auto wTextForAI = dataDetectorAdapter->textForAI_;
528 auto wTextForAILength = static_cast<int32_t>(wTextForAI.length());
529 dataDetectorAdapter->PreprocessTextDetect();
530 do {
531 std::string detectText = UtfUtils::Str16DebugToStr8(
532 wTextForAI.substr(startPos, std::min(AI_TEXT_MAX_LENGTH, wTextForAILength - startPos)));
533 bool isSameDetectText = detectTextIdx < dataDetectorAdapter->detectTexts_.size() &&
534 detectText == dataDetectorAdapter->detectTexts_[detectTextIdx];
535 while (!aiSpanMap.empty() && aiSpanMapIt != aiSpanMap.end() && aiSpanMapIt->first >= 0 &&
536 aiSpanMapIt->first < std::min(wTextForAILength, startPos + AI_TEXT_MAX_LENGTH - AI_TEXT_GAP)) {
537 auto aiContent = aiSpanMapIt->second.content;
538 auto wAIContent = StringUtils::ToWstring(aiContent);
539 if (isSameDetectText || aiContent == UtfUtils::Str16DebugToStr8(wTextForAI.substr(aiSpanMapIt->first,
540 std::min(static_cast<int32_t>(wAIContent.length()), wTextForAILength - aiSpanMapIt->first)))) {
541 dataDetectorAdapter->aiSpanMap_[aiSpanMapIt->first] = aiSpanMapIt->second;
542 hasSame = true;
543 }
544 ++aiSpanMapIt;
545 }
546 if (!isSameDetectText) {
547 dataDetectorAdapter->InitTextDetect(startPos, detectText);
548 if (detectTextIdx < dataDetectorAdapter->detectTexts_.size()) {
549 dataDetectorAdapter->detectTexts_[detectTextIdx] = detectText;
550 } else {
551 dataDetectorAdapter->detectTexts_.emplace_back(detectText);
552 }
553 }
554 ++detectTextIdx;
555 startPos += AI_TEXT_MAX_LENGTH - AI_TEXT_GAP;
556 } while (startPos + AI_TEXT_GAP < wTextForAILength && (!dataDetectorAdapter->textDetectTypes_.empty()));
557 if (dataDetectorAdapter->hasUrlType_) {
558 dataDetectorAdapter->HandleTextUrlDetect();
559 }
560 if (hasSame) {
561 dataDetectorAdapter->MarkDirtyNode();
562 }
563 };
564 }
565
StartAITask()566 void DataDetectorAdapter::StartAITask()
567 {
568 if (textForAI_.empty() || (!typeChanged_ && lastTextForAI_ == textForAI_)) {
569 MarkDirtyNode();
570 return;
571 }
572 std::map<int32_t, AISpan> aiSpanMapCopy;
573 if (!typeChanged_) {
574 aiSpanMapCopy = aiSpanMap_;
575 }
576 detectTexts_.clear();
577 aiSpanMap_.clear();
578 typeChanged_ = false;
579 startDetectorTimeStamp_ = std::chrono::high_resolution_clock::now();
580 auto context = PipelineContext::GetCurrentContextSafely();
581 CHECK_NULL_VOID(context);
582 auto taskExecutor = context->GetTaskExecutor();
583 CHECK_NULL_VOID(taskExecutor);
584 aiDetectDelayTask_.Cancel();
585 aiDetectDelayTask_.Reset(GetDetectDelayTask(aiSpanMapCopy));
586 TAG_LOGI(AceLogTag::ACE_TEXT, "DataDetectorAdapter::StartAITask, post whole task, id: %{public}i",
587 GetHost() ? GetHost()->GetId() : -1);
588 taskExecutor->PostDelayedTask(
589 aiDetectDelayTask_, TaskExecutor::TaskType::UI, AI_DELAY_TIME, "ArkUITextStartAIDetect");
590 }
591
MarkDirtyNode() const592 void DataDetectorAdapter::MarkDirtyNode() const
593 {
594 auto host = GetHost();
595 CHECK_NULL_VOID(host);
596 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
597 auto layoutProperty = host->GetLayoutProperty();
598 CHECK_NULL_VOID(layoutProperty);
599 layoutProperty->OnPropertyChangeMeasure();
600 }
601 } // namespace OHOS::Ace
602