1 /*
2  * Copyright (c) 2021-2022 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 <algorithm>
17 #include <map>
18 #include <mutex>
19 #include <set>
20 #include <vector>
21 
22 #include "napi/native_api.h"
23 #include "napi/native_common.h"
24 #include "napi/native_node_api.h"
25 
26 #include "base/utils/utils.h"
27 #include "bridge/common/media_query/media_queryer.h"
28 #include "bridge/common/utils/engine_helper.h"
29 #include "bridge/js_frontend/engine/common/js_engine.h"
30 #include "core/common/container.h"
31 
32 namespace OHOS::Ace::Napi {
33 namespace {
34 constexpr size_t STR_BUFFER_SIZE = 1024;
35 constexpr int32_t TWO_ARGS = 2;
36 }
37 
38 using namespace OHOS::Ace::Framework;
39 struct MediaQueryResult {
40     bool matches_ = false;
41     std::string media_;
42 
MediaQueryResultOHOS::Ace::Napi::MediaQueryResult43     MediaQueryResult(bool match, const std::string& media) : matches_(match), media_(media) {}
44     virtual ~MediaQueryResult() = default;
NapiSerializerOHOS::Ace::Napi::MediaQueryResult45     virtual void NapiSerializer(napi_env& env, napi_value& result)
46     {
47         /* construct a MediaQueryListener object */
48         napi_create_object(env, &result);
49         napi_handle_scope scope = nullptr;
50         napi_open_handle_scope(env, &scope);
51         if (scope == nullptr) {
52             return;
53         }
54 
55         napi_value matchesVal = nullptr;
56         napi_get_boolean(env, matches_, &matchesVal);
57         napi_set_named_property(env, result, "matches", matchesVal);
58 
59         napi_value mediaVal = nullptr;
60         napi_create_string_utf8(env, media_.c_str(), media_.size(), &mediaVal);
61         napi_set_named_property(env, result, "media", mediaVal);
62         napi_close_handle_scope(env, scope);
63     }
64 };
65 
66 class MediaQueryListener : public MediaQueryResult {
67 public:
MediaQueryListener(bool match, const std::string& media)68     MediaQueryListener(bool match, const std::string& media) : MediaQueryResult(match, media) {}
69     ~MediaQueryListener() override
70     {
71         {
72             std::lock_guard<std::mutex> lock(mutex_);
73             TAG_LOGI(AceLogTag::ACE_MEDIA_QUERY, "clean:%{public}s", media_.c_str());
74             CleanListenerSet();
75         }
76 
77         if (env_ == nullptr) {
78             return;
79         }
80         for (auto& item : cbList_) {
81             napi_delete_reference(env_, item);
82         }
83     }
84 
NapiCallback(JsEngine* jsEngine)85     static void NapiCallback(JsEngine* jsEngine)
86     {
87         OnNapiCallback(jsEngine);
88     }
89 
OnNapiCallback(JsEngine* jsEngine)90     static void OnNapiCallback(JsEngine* jsEngine)
91     {
92         std::set<std::unique_ptr<MediaQueryListener>> delayDeleteListenerSets;
93         std::set<napi_ref> delayDeleteCallbacks;
94         std::vector<MediaQueryListener*> copyListeners;
95         {
96             std::lock_guard<std::mutex> lock(mutex_);
97             auto& currentListeners = listenerSets_[AceType::WeakClaim(jsEngine)];
98             copyListeners.insert(copyListeners.end(), currentListeners.begin(), currentListeners.end());
99         }
100         struct Leave {
101             ~Leave()
102             {
103                 if (delayDeleteEnv_) {
104                     for (auto& cbRef : *delayDeleteCallbacks_) {
105                         napi_delete_reference(delayDeleteEnv_, cbRef);
106                     }
107                 }
108                 delayDeleteEnv_ = nullptr;
109                 delayDeleteCallbacks_ = nullptr;
110                 delayDeleteListenerSets_ = nullptr;
111             }
112         } leave;
113 
114         delayDeleteCallbacks_ = &delayDeleteCallbacks;
115         delayDeleteListenerSets_ = &delayDeleteListenerSets;
116 
117         TriggerAllCallbacks(copyListeners);
118     }
119 
TriggerAllCallbacks(std::vector<MediaQueryListener*>& copyListeners)120     static void TriggerAllCallbacks(std::vector<MediaQueryListener*>& copyListeners)
121     {
122         MediaQueryer queryer;
123         for (auto& listener : copyListeners) {
124             auto json = MediaQueryInfo::GetMediaQueryJsonInfo();
125             listener->matches_ = queryer.MatchCondition(listener->media_, json);
126             std::set<napi_ref> delayDeleteCallbacks;
127             std::vector<napi_ref> copyCallbacks;
128             {
129                 std::lock_guard<std::mutex> lock(mutex_);
130                 auto& currentCallbacks = listener->cbList_;
131                 copyCallbacks.insert(copyCallbacks.end(), currentCallbacks.begin(), currentCallbacks.end());
132             }
133 
134             for (const auto& cbRef : copyCallbacks) {
135                 if (delayDeleteCallbacks_->find(cbRef) != delayDeleteCallbacks_->end()) {
136                     continue;
137                 }
138                 TAG_LOGI(AceLogTag::ACE_MEDIA_QUERY, "trigger:%{public}s matches:%{public}d",
139                     listener->media_.c_str(), listener->matches_);
140                 napi_handle_scope scope = nullptr;
141                 napi_open_handle_scope(listener->env_, &scope);
142                 if (scope == nullptr) {
143                     return;
144                 }
145 
146                 napi_value cb = nullptr;
147                 napi_get_reference_value(listener->env_, cbRef, &cb);
148 
149                 napi_value resultArg = nullptr;
150                 listener->MediaQueryResult::NapiSerializer(listener->env_, resultArg);
151 
152                 napi_value result = nullptr;
153                 napi_call_function(listener->env_, nullptr, cb, 1, &resultArg, &result);
154                 napi_close_handle_scope(listener->env_, scope);
155             }
156         }
157     }
158 
On(napi_env env, napi_callback_info info)159     static napi_value On(napi_env env, napi_callback_info info)
160     {
161         auto jsEngine = EngineHelper::GetCurrentEngineSafely();
162         if (!jsEngine) {
163             return nullptr;
164         }
165         jsEngine->RegisterMediaUpdateCallback(NapiCallback);
166 
167         napi_handle_scope scope = nullptr;
168         napi_open_handle_scope(env, &scope);
169         if (scope == nullptr) {
170             return nullptr;
171         }
172         napi_value thisVar = nullptr;
173         napi_value cb = nullptr;
174         size_t argc = ParseArgs(env, info, thisVar, cb);
175         if (!(argc == TWO_ARGS && thisVar != nullptr && cb != nullptr)) {
176             napi_close_handle_scope(env, scope);
177             return nullptr;
178         }
179         MediaQueryListener* listener = GetListener(env, thisVar);
180         if (!listener) {
181             napi_close_handle_scope(env, scope);
182             return nullptr;
183         }
184         auto iter = listener->FindCbList(cb);
185         if (iter != listener->cbList_.end()) {
186             napi_close_handle_scope(env, scope);
187             return nullptr;
188         }
189         napi_ref ref = nullptr;
190         napi_create_reference(env, cb, 1, &ref);
191         listener->cbList_.emplace_back(ref);
192         TAG_LOGI(AceLogTag::ACE_MEDIA_QUERY, "on:%{public}s num=%{public}d", listener->media_.c_str(),
193             static_cast<int>(listener->cbList_.size()));
194         napi_close_handle_scope(env, scope);
195 
196 #if defined(PREVIEW)
197         NapiCallback(AceType::RawPtr(jsEngine));
198 #endif
199 
200         return nullptr;
201     }
202 
Off(napi_env env, napi_callback_info info)203     static napi_value Off(napi_env env, napi_callback_info info)
204     {
205         napi_value thisVar = nullptr;
206         napi_value cb = nullptr;
207         size_t argc = ParseArgs(env, info, thisVar, cb);
208         MediaQueryListener* listener = GetListener(env, thisVar);
209         if (!listener) {
210             return nullptr;
211         }
212         if (argc == 1) {
213             if (delayDeleteCallbacks_) {
214                 delayDeleteEnv_ = env;
215                 for (auto& item : listener->cbList_) {
216                     (*delayDeleteCallbacks_).emplace(item);
217                 }
218             } else {
219                 for (auto& item : listener->cbList_) {
220                     napi_delete_reference(listener->env_, item);
221                 }
222             }
223             listener->cbList_.clear();
224         } else {
225             NAPI_ASSERT(env, (argc == 2 && listener != nullptr && cb != nullptr), "Invalid arguments");
226             auto iter = listener->FindCbList(cb);
227             if (iter != listener->cbList_.end()) {
228                 if (delayDeleteCallbacks_) {
229                     delayDeleteEnv_ = env;
230                     (*delayDeleteCallbacks_).emplace(*iter);
231                 } else {
232                     napi_delete_reference(listener->env_, *iter);
233                 }
234                 listener->cbList_.erase(iter);
235             }
236         }
237         TAG_LOGI(AceLogTag::ACE_MEDIA_QUERY, "off:%{public}s num=%{public}d", listener->media_.c_str(),
238             static_cast<int>(listener->cbList_.size()));
239         return nullptr;
240     }
241 
FindCbList(napi_value cb)242     std::list<napi_ref>::iterator FindCbList(napi_value cb)
243     {
244         return std::find_if(cbList_.begin(), cbList_.end(), [env = env_, cb](const napi_ref& item) -> bool {
245             bool result = false;
246             napi_value refItem;
247             napi_get_reference_value(env, item, &refItem);
248             napi_strict_equals(env, refItem, cb, &result);
249             return result;
250         });
251     }
252 
253     void NapiSerializer(napi_env& env, napi_value& result) override
254     {
255         MediaQueryResult::NapiSerializer(env, result);
256 
257         napi_wrap(
258             env, result, this,
259             [](napi_env env, void* data, void* hint) {
260                 MediaQueryListener* listener = static_cast<MediaQueryListener*>(data);
261                 if (delayDeleteListenerSets_) {
262                     delayDeleteListenerSets_->emplace(listener);
263                 } else {
264                     delete listener;
265                 }
266             },
267             nullptr, nullptr);
268 
269         /* insert callback functions */
270         const char* funName = "on";
271         napi_value funcValue = nullptr;
272         napi_create_function(env, funName, NAPI_AUTO_LENGTH, On, nullptr, &funcValue);
273         napi_set_named_property(env, result, funName, funcValue);
274 
275         funName = "off";
276         napi_create_function(env, funName, NAPI_AUTO_LENGTH, Off, nullptr, &funcValue);
277         napi_set_named_property(env, result, funName, funcValue);
278     }
279 
280 private:
CleanListenerSet()281     void CleanListenerSet()
282     {
283         auto iter = listenerSets_.begin();
284         while (iter != listenerSets_.end()) {
285             iter->second.erase(this);
286             if (iter->second.empty()) {
287                 auto jsEngineWeak = iter->first.Upgrade();
288                 if (jsEngineWeak) {
289                     jsEngineWeak->UnregisterMediaUpdateCallback();
290                 }
291                 iter = listenerSets_.erase(iter);
292             } else {
293                 iter++;
294             }
295         }
296     }
297 
Initialize(napi_env env, napi_value thisVar)298     void Initialize(napi_env env, napi_value thisVar)
299     {
300         napi_handle_scope scope = nullptr;
301         napi_open_handle_scope(env, &scope);
302         if (scope == nullptr) {
303             return;
304         }
305         if (env_ == nullptr) {
306             env_ = env;
307         }
308         napi_close_handle_scope(env, scope);
309         auto jsEngine = EngineHelper::GetCurrentEngineSafely();
310         if (!jsEngine) {
311             return;
312         }
313         {
314             std::lock_guard<std::mutex> lock(mutex_);
315             listenerSets_[jsEngine].emplace(this);
316         }
317     }
318 
GetListener(napi_env env, napi_value thisVar)319     static MediaQueryListener* GetListener(napi_env env, napi_value thisVar)
320     {
321         MediaQueryListener* listener = nullptr;
322         napi_unwrap(env, thisVar, (void**)&listener);
323         CHECK_NULL_RETURN(listener, nullptr);
324         listener->Initialize(env, thisVar);
325         return listener;
326     }
327 
ParseArgs(napi_env& env, napi_callback_info& info, napi_value& thisVar, napi_value& cb)328     static size_t ParseArgs(napi_env& env, napi_callback_info& info, napi_value& thisVar, napi_value& cb)
329     {
330         const size_t argNum = 2;
331         size_t argc = argNum;
332         napi_value argv[argNum] = { 0 };
333         void* data = nullptr;
334         napi_get_cb_info(env, info, &argc, argv, &thisVar, &data);
335         NAPI_ASSERT_BASE(env, argc > 0, "too few parameter", 0);
336 
337         napi_valuetype napiType;
338         NAPI_CALL_BASE(env, napi_typeof(env, argv[0], &napiType), 0);
339         NAPI_ASSERT_BASE(env, napiType == napi_string, "parameter 1 should be string", 0);
340         char type[STR_BUFFER_SIZE] = { 0 };
341         size_t len = 0;
342         napi_get_value_string_utf8(env, argv[0], type, STR_BUFFER_SIZE, &len);
343         NAPI_ASSERT_BASE(env, len < STR_BUFFER_SIZE, "condition string too long", 0);
344         NAPI_ASSERT_BASE(env, strcmp("change", type) == 0, "type mismatch('change')", 0);
345 
346         if (argc <= 1) {
347             return argc;
348         }
349 
350         NAPI_CALL_BASE(env, napi_typeof(env, argv[1], &napiType), 0);
351         NAPI_ASSERT_BASE(env, napiType == napi_function, "type mismatch for parameter 2", 0);
352         cb = argv[1];
353         return argc;
354     }
355 
356     napi_env env_ = nullptr;
357     std::list<napi_ref> cbList_;
358     static std::set<std::unique_ptr<MediaQueryListener>>* delayDeleteListenerSets_;
359     static napi_env delayDeleteEnv_;
360     static std::set<napi_ref>* delayDeleteCallbacks_;
361     static std::map<WeakPtr<JsEngine>, std::set<MediaQueryListener*>> listenerSets_;
362     static std::mutex mutex_;
363 };
364 std::set<std::unique_ptr<MediaQueryListener>>* MediaQueryListener::delayDeleteListenerSets_;
365 napi_env MediaQueryListener::delayDeleteEnv_ = nullptr;
366 std::set<napi_ref>* MediaQueryListener::delayDeleteCallbacks_;
367 std::map<WeakPtr<JsEngine>, std::set<MediaQueryListener*>> MediaQueryListener::listenerSets_;
368 std::mutex MediaQueryListener::mutex_;
369 
JSMatchMediaSync(napi_env env, napi_callback_info info)370 static napi_value JSMatchMediaSync(napi_env env, napi_callback_info info)
371 {
372     /* Get arguments */
373     size_t argc = 1;
374     napi_value argv = nullptr;
375     napi_value thisVar = nullptr;
376     void* data = nullptr;
377     NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &argv, &thisVar, &data));
378     NAPI_ASSERT(env, argc == 1, "requires 1 parameter");
379 
380     /* Checkout arguments */
381     napi_valuetype type;
382     NAPI_CALL(env, napi_typeof(env, argv, &type));
383     NAPI_ASSERT(env, type == napi_string, "type mismatch");
384     char condition[STR_BUFFER_SIZE] = { 0 };
385     size_t len = 0;
386     napi_get_value_string_utf8(env, argv, condition, STR_BUFFER_SIZE, &len);
387     NAPI_ASSERT(env, len < STR_BUFFER_SIZE, "condition string too long");
388 
389     /* construct object for query */
390     std::string conditionStr(condition, len);
391     auto mediaFeature = MediaQueryInfo::GetMediaQueryJsonInfo();
392     MediaQueryer queryer;
393     bool matchResult = queryer.MatchCondition(conditionStr, mediaFeature);
394     MediaQueryListener* listener = new MediaQueryListener(matchResult, conditionStr);
395     napi_value result = nullptr;
396     listener->NapiSerializer(env, result);
397     return result;
398 }
399 
Export(napi_env env, napi_value exports)400 static napi_value Export(napi_env env, napi_value exports)
401 {
402     napi_property_descriptor properties[] = { DECLARE_NAPI_FUNCTION("matchMediaSync", JSMatchMediaSync) };
403 
404     NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties));
405     return exports;
406 }
407 
408 static napi_module media_query_module = {
409     .nm_version = 1,
410     .nm_flags = 0,
411     .nm_filename = nullptr,
412     .nm_register_func = Export,
413     .nm_modname = "mediaquery", // relative to the dynamic library's name
414     .nm_priv = ((void*)0),
415     .reserved = { 0 },
416 };
417 
RegisterMediaQuery()418 extern "C" __attribute__((constructor)) void RegisterMediaQuery()
419 {
420     napi_module_register(&media_query_module);
421 }
422 
423 } // namespace OHOS::Ace::Napi
424