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 "core/common/ai/data_detector_mgr.h"
20 #include "core/pipeline_ng/pipeline_context.h"
21
22 namespace OHOS::Ace {
23
24 constexpr int32_t AI_TEXT_MAX_LENGTH = 500;
25 constexpr int32_t AI_TEXT_GAP = 100;
26 constexpr int32_t AI_DELAY_TIME = 100;
27 constexpr uint32_t SECONDS_TO_MILLISECONDS = 1000;
28
29 const std::unordered_map<TextDataDetectType, std::string> TEXT_DETECT_MAP = {
30 { TextDataDetectType::PHONE_NUMBER, "phoneNum" }, { TextDataDetectType::URL, "url" },
31 { TextDataDetectType::EMAIL, "email" }, { TextDataDetectType::ADDRESS, "location" },
32 { TextDataDetectType::DATE_TIME, "datetime" }
33 };
34 const std::unordered_map<std::string, TextDataDetectType> TEXT_DETECT_MAP_REVERSE = {
35 { "phoneNum", TextDataDetectType::PHONE_NUMBER }, { "url", TextDataDetectType::URL },
36 { "email", TextDataDetectType::EMAIL }, { "location", TextDataDetectType::ADDRESS },
37 { "datetime", TextDataDetectType::DATE_TIME }
38 };
39
GetAIEntityMenu()40 void DataDetectorAdapter::GetAIEntityMenu()
41 {
42 auto context = PipelineContext::GetCurrentContextSafely();
43 CHECK_NULL_VOID(context);
44 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
45 uiTaskExecutor.PostTask(
46 [weak = AceType::WeakClaim(this), instanceId = context->GetInstanceId()] {
47 ContainerScope scope(instanceId);
48 auto dataDetectorAdapter = weak.Upgrade();
49 CHECK_NULL_VOID(dataDetectorAdapter);
50 TAG_LOGI(AceLogTag::ACE_TEXT, "Get AI entity menu from ai_engine");
51 DataDetectorMgr::GetInstance().GetAIEntityMenu(dataDetectorAdapter->textDetectResult_);
52 },
53 "ArkUITextInitDataDetect");
54 }
55
ShowAIEntityMenu(const AISpan& aiSpan, const NG::RectF& aiRect, const RefPtr<NG::FrameNode>& targetNode, bool isShowCopy, bool isShowSelectText)56 bool DataDetectorAdapter::ShowAIEntityMenu(const AISpan& aiSpan, const NG::RectF& aiRect,
57 const RefPtr<NG::FrameNode>& targetNode, bool isShowCopy, bool isShowSelectText)
58 {
59 if (textDetectResult_.menuOptionAndAction.empty()) {
60 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
61 GetAIEntityMenu();
62 return false;
63 }
64
65 mainContainerId_ = Container::CurrentId();
66 std::vector<std::pair<std::string, std::function<void()>>> menuOptions;
67 auto menuOptionAndAction = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
68 if (menuOptionAndAction.empty()) {
69 return false;
70 }
71 if (!isShowSelectText) {
72 // delete the last option: selectText.
73 menuOptionAndAction.pop_back();
74 if (!isShowCopy) {
75 // delete the last option: copy.
76 menuOptionAndAction.pop_back();
77 }
78 }
79
80 for (auto menuOption : menuOptionAndAction) {
81 std::function<void()> onClickEvent = [aiSpan, menuOption, weak = AceType::WeakClaim(this),
82 targetNodeWeak = AceType::WeakClaim(AceType::RawPtr(targetNode))]() {
83 auto dataDetectorAdapter = weak.Upgrade();
84 CHECK_NULL_VOID(dataDetectorAdapter);
85 auto targetNode = targetNodeWeak.Upgrade();
86 CHECK_NULL_VOID(targetNode);
87 dataDetectorAdapter->OnClickAIMenuOption(aiSpan, menuOption, targetNode);
88 };
89 menuOptions.push_back(std::make_pair(menuOption.first, onClickEvent));
90 }
91 auto pipeline = NG::PipelineContext::GetCurrentContextSafely();
92 CHECK_NULL_RETURN(pipeline, false);
93 auto overlayManager = pipeline->GetOverlayManager();
94 CHECK_NULL_RETURN(overlayManager, false);
95 return overlayManager->ShowAIEntityMenu(menuOptions, aiRect, targetNode);
96 }
97
OnClickAIMenuOption(const AISpan& aiSpan, const std::pair<std::string, FuncVariant>& menuOption, const RefPtr<NG::FrameNode>& targetNode)98 void DataDetectorAdapter::OnClickAIMenuOption(const AISpan& aiSpan,
99 const std::pair<std::string, FuncVariant>& menuOption, const RefPtr<NG::FrameNode>& targetNode)
100 {
101 TAG_LOGI(AceLogTag::ACE_TEXT, "Click AI menu option: %{public}s", menuOption.first.c_str());
102 auto pipeline = NG::PipelineContext::GetCurrentContextSafely();
103 CHECK_NULL_VOID(pipeline);
104 auto overlayManager = pipeline->GetOverlayManager();
105 CHECK_NULL_VOID(overlayManager);
106 if (targetNode) {
107 overlayManager->CloseAIEntityMenu(targetNode->GetId());
108 }
109 Container::UpdateCurrent(mainContainerId_);
110
111 auto runtimeContext = Platform::AceContainer::GetRuntimeContext(pipeline->GetInstanceId());
112 CHECK_NULL_VOID(runtimeContext);
113 auto token = runtimeContext->GetToken();
114 auto bundleName = runtimeContext->GetBundleName();
115
116 hasClickedMenuOption_ = true;
117 if (onClickMenu_ && std::holds_alternative<std::function<std::string()>>(menuOption.second)) {
118 onClickMenu_(std::get<std::function<std::string()>>(menuOption.second)());
119 } else if (std::holds_alternative<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)) {
120 std::get<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)(token, aiSpan.content);
121 } else if (std::holds_alternative<std::function<void(int32_t, std::string)>>(menuOption.second)) {
122 std::get<std::function<void(int32_t, std::string)>>(menuOption.second)(mainContainerId_, aiSpan.content);
123 } else if (std::holds_alternative<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(
124 menuOption.second)) {
125 std::get<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(menuOption.second)(
126 mainContainerId_, textForAI_, bundleName, aiSpan.start, aiSpan.content);
127 } else {
128 TAG_LOGW(AceLogTag::ACE_TEXT, "No matching menu option");
129 }
130 hasClickedMenuOption_ = false;
131 }
132
ResponseBestMatchItem(const AISpan& aiSpan)133 void DataDetectorAdapter::ResponseBestMatchItem(const AISpan& aiSpan)
134 {
135 if (textDetectResult_.menuOptionAndAction.empty()) {
136 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
137 GetAIEntityMenu();
138 return;
139 }
140 auto menuOptions = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
141 if (menuOptions.empty()) {
142 TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty");
143 return;
144 }
145 OnClickAIMenuOption(aiSpan, menuOptions[0]);
146 }
147
SetTextDetectTypes(const std::string& types)148 void DataDetectorAdapter::SetTextDetectTypes(const std::string& types)
149 {
150 textDetectTypes_ = types;
151
152 std::set<std::string> newTypesSet;
153 std::istringstream iss(types);
154 std::string type;
155 while (std::getline(iss, type, ',')) {
156 newTypesSet.insert(type);
157 }
158 if (newTypesSet != textDetectTypesSet_) {
159 textDetectTypesSet_ = newTypesSet;
160 typeChanged_ = true;
161 aiDetectInitialized_ = false;
162 auto host = GetHost();
163 CHECK_NULL_VOID(host);
164 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
165 }
166 }
167
ParseOriText(const std::unique_ptr<JsonValue>& entityJson, std::string& text)168 bool DataDetectorAdapter::ParseOriText(const std::unique_ptr<JsonValue>& entityJson, std::string& text)
169 {
170 TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text entry");
171 auto runtimeContext = Platform::AceContainer::GetRuntimeContext(Container::CurrentId());
172 CHECK_NULL_RETURN(runtimeContext, false);
173 if (runtimeContext->GetBundleName() != entityJson->GetString("bundleName")) {
174 TAG_LOGW(AceLogTag::ACE_TEXT,
175 "Wrong bundleName, the context bundleName is: %{public}s, but your bundleName is: %{public}s",
176 runtimeContext->GetBundleName().c_str(), entityJson->GetString("bundleName").c_str());
177 return false;
178 }
179 auto aiSpanArray = entityJson->GetValue("entity");
180 if (aiSpanArray->IsNull() || !aiSpanArray->IsArray()) {
181 TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI entity");
182 return false;
183 }
184
185 aiSpanMap_.clear();
186 aiSpanRects_.clear();
187 detectTexts_.clear();
188 AISpan aiSpan;
189 for (int32_t i = 0; i < aiSpanArray->GetArraySize(); ++i) {
190 auto item = aiSpanArray->GetArrayItem(i);
191 aiSpan.content = item->GetString("entityContent");
192 aiSpan.type = TEXT_DETECT_MAP_REVERSE.at(item->GetString("entityType"));
193 aiSpan.start = item->GetInt("start");
194 aiSpan.end = item->GetInt("end");
195 aiSpanMap_[aiSpan.start] = aiSpan;
196 }
197 aiDetectInitialized_ = true;
198 text = entityJson->GetString("content");
199 textForAI_ = text;
200 lastTextForAI_ = textForAI_;
201 if (textDetectResult_.menuOptionAndAction.empty()) {
202 GetAIEntityMenu();
203 }
204
205 TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text successful");
206 return true;
207 }
208
InitTextDetect(int32_t startPos, std::string detectText)209 void DataDetectorAdapter::InitTextDetect(int32_t startPos, std::string detectText)
210 {
211 TextDataDetectInfo info;
212 info.text = detectText;
213 info.module = textDetectTypes_;
214
215 auto context = PipelineContext::GetCurrentContextSafely();
216 CHECK_NULL_VOID(context);
217 int32_t instanceID = context->GetInstanceId();
218 auto textFunc = [weak = WeakClaim(this), instanceID, startPos, info](const TextDataDetectResult result) {
219 ContainerScope scope(instanceID);
220 auto context = PipelineContext::GetCurrentContextSafely();
221 CHECK_NULL_VOID(context);
222 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
223 uiTaskExecutor.PostTask(
224 [result, weak, instanceID, startPos, info] {
225 ContainerScope scope(instanceID);
226 auto dataDetectorAdapter = weak.Upgrade();
227 CHECK_NULL_VOID(dataDetectorAdapter);
228 if (info.module != dataDetectorAdapter->textDetectTypes_) {
229 return;
230 }
231 dataDetectorAdapter->ParseAIResult(result, startPos);
232 auto host = dataDetectorAdapter->GetHost();
233 CHECK_NULL_VOID(host);
234 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
235 },
236 "ArkUITextParseAIResult");
237 };
238
239 auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::BACKGROUND);
240 uiTaskExecutor.PostTask(
241 [info, textFunc] {
242 TAG_LOGI(AceLogTag::ACE_TEXT, "Start entity detect using AI");
243 DataDetectorMgr::GetInstance().DataDetect(info, textFunc);
244 },
245 "ArkUITextInitDataDetect");
246 }
247
ParseAIResult(const TextDataDetectResult& result, int32_t startPos)248 void DataDetectorAdapter::ParseAIResult(const TextDataDetectResult& result, int32_t startPos)
249 {
250 auto entityJson = JsonUtil::ParseJsonString(result.entity);
251 CHECK_NULL_VOID(entityJson);
252 for (const auto& type : TEXT_DETECT_MAP) {
253 auto jsonValue = entityJson->GetValue(type.second);
254 ParseAIJson(jsonValue, type.first, startPos);
255 }
256
257 if (startPos + AI_TEXT_MAX_LENGTH >= static_cast<int32_t>(StringUtils::ToWstring(textForAI_).length())) {
258 aiDetectInitialized_ = true;
259 auto entityJsonArray = JsonUtil::CreateArray(true);
260 // process with overlapping entities, leaving only the earlier ones
261 int32_t preEnd = 0;
262 auto aiSpanIterator = aiSpanMap_.begin();
263 while (aiSpanIterator != aiSpanMap_.end()) {
264 auto aiSpan = aiSpanIterator->second;
265 if (aiSpan.start < preEnd) {
266 aiSpanIterator = aiSpanMap_.erase(aiSpanIterator);
267 } else {
268 preEnd = aiSpan.end;
269 ++aiSpanIterator;
270 auto aiSpanJson = JsonUtil::Create(true);
271 aiSpanJson->Put("start", aiSpan.start);
272 aiSpanJson->Put("end", aiSpan.end);
273 aiSpanJson->Put("entityContent", aiSpan.content.c_str());
274 aiSpanJson->Put("entityType", TEXT_DETECT_MAP.at(aiSpan.type).c_str());
275 entityJsonArray->Put(aiSpanJson);
276 }
277 }
278 auto resultJson = JsonUtil::Create(true);
279 resultJson->Put("entity", entityJsonArray);
280 resultJson->Put("code", result.code);
281 SetTextDetectResult(result);
282 FireOnResult(resultJson->ToString());
283 }
284 }
285
ParseAIJson( const std::unique_ptr<JsonValue>& jsonValue, TextDataDetectType type, int32_t startPos)286 void DataDetectorAdapter::ParseAIJson(
287 const std::unique_ptr<JsonValue>& jsonValue, TextDataDetectType type, int32_t startPos)
288 {
289 if (!jsonValue || !jsonValue->IsArray()) {
290 TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI result");
291 return;
292 }
293
294 for (int32_t i = 0; i < jsonValue->GetArraySize(); ++i) {
295 auto item = jsonValue->GetArrayItem(i);
296 auto charOffset = item->GetInt("charOffset");
297 auto oriText = item->GetString("oriText");
298 auto wTextForAI = StringUtils::ToWstring(textForAI_);
299 auto wOriText = StringUtils::ToWstring(oriText);
300 int32_t end = startPos + charOffset + static_cast<int32_t>(wOriText.length());
301 if (charOffset < 0 || startPos + charOffset >= static_cast<int32_t>(wTextForAI.length()) ||
302 end >= startPos + AI_TEXT_MAX_LENGTH || oriText.empty()) {
303 TAG_LOGW(AceLogTag::ACE_TEXT, "The result of AI is wrong");
304 continue;
305 }
306 if (oriText !=
307 StringUtils::ToString(wTextForAI.substr(startPos + charOffset, static_cast<int32_t>(wOriText.length())))) {
308 TAG_LOGW(AceLogTag::ACE_TEXT, "The charOffset is wrong");
309 continue;
310 }
311 int32_t start = startPos + charOffset;
312 auto iter = aiSpanMap_.find(start);
313 if (iter != aiSpanMap_.end() && iter->second.content.length() >= oriText.length()) {
314 // both entities start at the same position, leaving the longer one
315 continue;
316 }
317
318 TimeStamp currentDetectorTimeStamp = std::chrono::high_resolution_clock::now();
319 std::chrono::duration<float, std::ratio<1, SECONDS_TO_MILLISECONDS>> costTime =
320 currentDetectorTimeStamp - startDetectorTimeStamp_;
321 item->Put("costTime", costTime.count());
322 item->Put("resultCode", textDetectResult_.code);
323 entityJson_[start] = item->ToString();
324 TAG_LOGI(AceLogTag::ACE_TEXT, "The json of the entity is: %{private}s", entityJson_[start].c_str());
325
326 AISpan aiSpan;
327 aiSpan.start = start;
328 aiSpan.end = end;
329 aiSpan.content = oriText;
330 aiSpan.type = type;
331 aiSpanMap_[aiSpan.start] = aiSpan;
332 }
333 }
334
GetDetectDelayTask(const std::map<int32_t, AISpan>& aiSpanMap)335 std::function<void()> DataDetectorAdapter::GetDetectDelayTask(const std::map<int32_t, AISpan>& aiSpanMap)
336 {
337 return [aiSpanMap, weak = WeakClaim(this)]() {
338 auto dataDetectorAdapter = weak.Upgrade();
339 CHECK_NULL_VOID(dataDetectorAdapter);
340 if (dataDetectorAdapter->textForAI_.empty()) {
341 return;
342 }
343
344 size_t detectTextIdx = 0;
345 auto aiSpanMapIt = aiSpanMap.begin();
346 int32_t startPos = 0;
347 bool hasSame = false;
348 auto wTextForAI = StringUtils::ToWstring(dataDetectorAdapter->textForAI_);
349 auto wTextForAILength = static_cast<int32_t>(wTextForAI.length());
350 do {
351 std::string detectText = StringUtils::ToString(
352 wTextForAI.substr(startPos, std::min(AI_TEXT_MAX_LENGTH, wTextForAILength - startPos)));
353 bool isSameDetectText = detectTextIdx < dataDetectorAdapter->detectTexts_.size() &&
354 detectText == dataDetectorAdapter->detectTexts_[detectTextIdx];
355 while (!aiSpanMap.empty() && aiSpanMapIt != aiSpanMap.end() && aiSpanMapIt->first >= 0 &&
356 aiSpanMapIt->first < std::min(wTextForAILength, startPos + AI_TEXT_MAX_LENGTH - AI_TEXT_GAP)) {
357 auto aiContent = aiSpanMapIt->second.content;
358 auto wAIContent = StringUtils::ToWstring(aiContent);
359 if (isSameDetectText || aiContent == StringUtils::ToString(wTextForAI.substr(aiSpanMapIt->first,
360 std::min(static_cast<int32_t>(wAIContent.length()), wTextForAILength - aiSpanMapIt->first)))) {
361 dataDetectorAdapter->aiSpanMap_[aiSpanMapIt->first] = aiSpanMapIt->second;
362 hasSame = true;
363 }
364 ++aiSpanMapIt;
365 }
366 if (!isSameDetectText) {
367 dataDetectorAdapter->InitTextDetect(startPos, detectText);
368 if (detectTextIdx < dataDetectorAdapter->detectTexts_.size()) {
369 dataDetectorAdapter->detectTexts_[detectTextIdx] = detectText;
370 } else {
371 dataDetectorAdapter->detectTexts_.emplace_back(detectText);
372 }
373 }
374 ++detectTextIdx;
375 startPos += AI_TEXT_MAX_LENGTH - AI_TEXT_GAP;
376 } while (startPos + AI_TEXT_GAP < wTextForAILength);
377 if (hasSame) {
378 auto host = dataDetectorAdapter->GetHost();
379 CHECK_NULL_VOID(host);
380 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
381 }
382 };
383 }
384
StartAITask()385 void DataDetectorAdapter::StartAITask()
386 {
387 if (textForAI_.empty() || (!typeChanged_ && lastTextForAI_ == textForAI_)) {
388 auto host = GetHost();
389 CHECK_NULL_VOID(host);
390 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
391 return;
392 }
393 std::map<int32_t, AISpan> aiSpanMapCopy;
394 if (!typeChanged_) {
395 aiSpanMapCopy = aiSpanMap_;
396 } else {
397 detectTexts_.clear();
398 }
399 aiSpanMap_.clear();
400 typeChanged_ = false;
401 lastTextForAI_ = textForAI_;
402 startDetectorTimeStamp_ = std::chrono::high_resolution_clock::now();
403 auto context = PipelineContext::GetCurrentContextSafely();
404 CHECK_NULL_VOID(context);
405 auto taskExecutor = context->GetTaskExecutor();
406 CHECK_NULL_VOID(taskExecutor);
407 aiDetectDelayTask_.Cancel();
408 aiDetectDelayTask_.Reset(GetDetectDelayTask(aiSpanMapCopy));
409 taskExecutor->PostDelayedTask(
410 aiDetectDelayTask_, TaskExecutor::TaskType::UI, AI_DELAY_TIME, "ArkUITextStartAIDetect");
411 }
412 } // namespace OHOS::Ace
413