1/*
2 * Copyright (c) 2023 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 <set>
17#include <string>
18#include <unistd.h>
19#include <uv.h>
20#include "json.hpp"
21#include "common_utilities_hpp.h"
22#include "ui_event_observer_napi.h"
23
24namespace OHOS::uitest {
25    using namespace nlohmann;
26    using namespace std;
27
28    /** Cached reference of UIEventObserver and JsCallbackFunction, key is the unique id.*/
29    static map<string, napi_ref> g_jsRefs;
30    static size_t g_incJsCbId = 0;
31
32    UiEventObserverNapi &UiEventObserverNapi::Get()
33    {
34        static UiEventObserverNapi instance;
35        return instance;
36    }
37
38    void UiEventObserverNapi::PreprocessCallOnce(napi_env env, ApiCallInfo &call, napi_value jsThis,
39                                                 napi_value* argv, ApiCallErr &err)
40    {
41        DCHECK(env != nullptr);
42        DCHECK(jsThis != nullptr);
43        auto &paramList = call.paramList_;
44        // pop js_cb_function and save at local side
45        if (paramList.size() < 1 || paramList.at(0).type() != detail::value_t::string) {
46            LOG_E("Missing event type argument");
47            err = ApiCallErr(ERR_INVALID_INPUT, "Missing event type argument");
48            return;
49        }
50        auto event = paramList.at(0).get<string>();
51        napi_valuetype type = napi_undefined;
52        if (paramList.size() > 1) {
53            NAPI_CALL_RETURN_VOID(env, napi_typeof(env, argv[1], &type));
54        }
55        if (type != napi_function) {
56            LOG_E("Invalid callback function argument");
57            err = ApiCallErr(ERR_INVALID_INPUT, "Invalid callback function argument");
58            return;
59        }
60        if (g_jsRefs.find(call.callerObjRef_) == g_jsRefs.end()) {
61            // hold the jsObserver to avoid it be recycled, it's needed in performing callback
62            napi_ref ref = nullptr;
63            NAPI_CALL_RETURN_VOID(env, napi_create_reference(env, jsThis, 1, &ref));
64            LOG_D("Hold reference of %{public}s", call.callerObjRef_.c_str());
65            g_jsRefs.insert({ call.callerObjRef_, ref });
66        }
67        auto jsCallback = argv[1];
68        const auto jsCbId = string("js_callback#") + to_string(++g_incJsCbId);
69        // hold the const  to avoid it be recycled, it's needed in performing callback
70        napi_ref ref = nullptr;
71        NAPI_CALL_RETURN_VOID(env, napi_create_reference(env, jsCallback, 1, &ref));
72        LOG_I("CbId = %{public}s, CbRef = %{public}p", jsCbId.c_str(), ref);
73        LOG_D("Hold reference of %{public}s", jsCbId.c_str());
74        g_jsRefs.insert({ jsCbId, ref });
75        // pass jsCbId instread of the function body
76        paramList.at(1) = jsCbId; // observer.once(type, cllbackId)
77    }
78
79    struct EventCallbackContext {
80        napi_env env;
81        string observerId;
82        string callbackId;
83        napi_ref observerRef;
84        napi_ref callbackRef;
85        bool releaseObserver; // if or not release observer object after performing this callback
86        bool releaseCallback; // if or not release callback function after performing this callback
87        nlohmann::json elmentInfo;
88    };
89
90    static void InitCallbackContext(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out, EventCallbackContext &ctx)
91    {
92        LOG_I("Handler api callback: %{public}s", in.apiId_.c_str());
93        if (in.apiId_ != "UIEventObserver.once") {
94            out.exception_ = ApiCallErr(ERR_INTERNAL, "Api dose not support callback: " + in.apiId_);
95            LOG_E("%{public}s", out.exception_.message_.c_str());
96            return;
97        }
98        DCHECK(env != nullptr);
99        DCHECK(in.paramList_.size() > INDEX_ZERO && in.paramList_.at(INDEX_ZERO).type() == detail::value_t::object);
100        DCHECK(in.paramList_.size() > INDEX_ONE && in.paramList_.at(INDEX_ONE).type() == detail::value_t::string);
101        DCHECK(in.paramList_.size() > INDEX_TWO && in.paramList_.at(INDEX_TWO).type() == detail::value_t::boolean);
102        DCHECK(in.paramList_.size() > INDEX_THREE && in.paramList_.at(INDEX_THREE).type() == detail::value_t::boolean);
103        auto &observerId = in.callerObjRef_;
104        auto &elementInfo = in.paramList_.at(INDEX_ZERO);
105        auto callbackId = in.paramList_.at(INDEX_ONE).get<string>();
106        LOG_I("Begin to callback UiEvent: observer=%{public}s, callback=%{public}s",
107              observerId.c_str(), callbackId.c_str());
108        auto findObserver = g_jsRefs.find(observerId);
109        auto findCallback = g_jsRefs.find(callbackId);
110        if (findObserver == g_jsRefs.end()) {
111            out.exception_ = ApiCallErr(INTERNAL_ERROR, "UIEventObserver is not referenced: " + observerId);
112            LOG_E("%{public}s", out.exception_.message_.c_str());
113            return;
114        }
115        if (findCallback == g_jsRefs.end()) {
116            out.exception_ = ApiCallErr(INTERNAL_ERROR, "JsCallbackFunction is not referenced: " + callbackId);
117            LOG_E("%{public}s", out.exception_.message_.c_str());
118            return;
119        }
120        ctx.env = env;
121        ctx.observerId = observerId;
122        ctx.callbackId = callbackId;
123        ctx.observerRef = findObserver->second;
124        ctx.callbackRef = findCallback->second;
125        ctx.elmentInfo = move(elementInfo);
126        ctx.releaseObserver = in.paramList_.at(INDEX_TWO).get<bool>();
127        ctx.releaseCallback = in.paramList_.at(INDEX_THREE).get<bool>();
128    }
129
130    /**Handle api callback from server end.*/
131    void UiEventObserverNapi::HandleEventCallback(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out)
132    {
133        auto context = new EventCallbackContext();
134        InitCallbackContext(env, in, out, *context);
135        if (out.exception_.code_ != NO_ERROR) {
136            delete context;
137            LOG_W("InitCallbackContext failed, cannot perform callback");
138            return;
139        }
140        uv_loop_s *loop = nullptr;
141        NAPI_CALL_RETURN_VOID(env, napi_get_uv_event_loop(env, &loop));
142        auto work = new uv_work_t;
143        work->data = context;
144        (void)uv_queue_work(loop, work, [](uv_work_t *) {}, [](uv_work_t *work, int status) {
145            auto ctx = reinterpret_cast<EventCallbackContext *>(work->data);
146            napi_value global = nullptr;
147            napi_value jsonProp = nullptr;
148            napi_value parseFunc = nullptr;
149            napi_get_global(ctx->env, &global);
150            napi_get_named_property(ctx->env, global, "JSON", &jsonProp);
151            napi_get_named_property(ctx->env, jsonProp, "parse", &parseFunc);
152            napi_value argv[1] = { nullptr };
153            napi_create_string_utf8(ctx->env, ctx->elmentInfo.dump().c_str(), NAPI_AUTO_LENGTH, argv);
154            napi_call_function(ctx->env, jsonProp, parseFunc, 1, argv, argv);
155            napi_value jsCallback = nullptr;
156            napi_get_reference_value(ctx->env, ctx->callbackRef, &jsCallback);
157            napi_call_function(ctx->env, nullptr, jsCallback, 1, argv, argv);
158            // accessing callback from js-caller, do check and warn
159            auto hasError = false;
160            napi_is_exception_pending(ctx->env, &hasError);
161            if (hasError) {
162                LOG_W("Exception raised during call js_cb_function!");
163            }
164            if (ctx->releaseObserver) {
165                LOG_D("Unref jsObserver: %{public}s", ctx->observerId.c_str());
166                napi_delete_reference(ctx->env, ctx->observerRef);
167                g_jsRefs.erase(ctx->observerId);
168            }
169            if (ctx->releaseCallback) {
170                LOG_D("Unref jsCallback: %{public}s", ctx->callbackId.c_str());
171                napi_delete_reference(ctx->env, ctx->callbackRef);
172                g_jsRefs.erase(ctx->callbackId);
173            }
174            delete work;
175            delete ctx;
176        });
177    }
178}
179