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