• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 #define MLOG_TAG "MediaAssetsChangeRequestNapi"
17 
18 #include "media_assets_change_request_napi.h"
19 
20 #include <unordered_set>
21 
22 #include "file_asset_napi.h"
23 #include "media_column.h"
24 #include "medialibrary_client_errno.h"
25 #include "medialibrary_errno.h"
26 #include "medialibrary_napi_log.h"
27 #include "medialibrary_tracer.h"
28 #include "userfile_client.h"
29 #include "userfile_manager_types.h"
30 
31 using namespace std;
32 
33 namespace OHOS::Media {
34 static const string MEDIA_ASSETS_CHANGE_REQUEST_CLASS = "MediaAssetsChangeRequest";
35 thread_local napi_ref MediaAssetsChangeRequestNapi::constructor_ = nullptr;
36 
37 constexpr int32_t YES = 1;
38 constexpr int32_t NO = 0;
39 constexpr int32_t USER_COMMENT_MAX_LEN = 420;
40 
Init(napi_env env,napi_value exports)41 napi_value MediaAssetsChangeRequestNapi::Init(napi_env env, napi_value exports)
42 {
43     NapiClassInfo info = { .name = MEDIA_ASSETS_CHANGE_REQUEST_CLASS,
44         .ref = &constructor_,
45         .constructor = Constructor,
46         .props = {
47             DECLARE_NAPI_FUNCTION("setFavorite", JSSetFavorite),
48             DECLARE_NAPI_FUNCTION("setHidden", JSSetHidden),
49             DECLARE_NAPI_FUNCTION("setUserComment", JSSetUserComment),
50             DECLARE_NAPI_FUNCTION("setIsRecentShow", JSSetIsRecentShow),
51         } };
52     MediaLibraryNapiUtils::NapiDefineClass(env, exports, info);
53     return exports;
54 }
55 
GetAssetArray(napi_env env,napi_value arg,vector<shared_ptr<FileAsset>> & fileAssets)56 static napi_value GetAssetArray(napi_env env, napi_value arg, vector<shared_ptr<FileAsset>>& fileAssets)
57 {
58     bool isArray = false;
59     uint32_t len = 0;
60     CHECK_ARGS(env, napi_is_array(env, arg, &isArray), JS_INNER_FAIL);
61     CHECK_COND_WITH_MESSAGE(env, isArray, "Failed to check array type");
62     CHECK_ARGS(env, napi_get_array_length(env, arg, &len), JS_INNER_FAIL);
63     CHECK_COND_WITH_MESSAGE(env, len > 0, "Failed to check array length");
64     for (uint32_t i = 0; i < len; i++) {
65         napi_value asset = nullptr;
66         CHECK_ARGS(env, napi_get_element(env, arg, i, &asset), JS_INNER_FAIL);
67         CHECK_COND_WITH_MESSAGE(env, asset != nullptr, "Failed to get asset element");
68 
69         FileAssetNapi* fileAssetNapi = nullptr;
70         CHECK_ARGS(env, napi_unwrap(env, asset, reinterpret_cast<void**>(&fileAssetNapi)), JS_INNER_FAIL);
71         CHECK_COND_WITH_MESSAGE(env, fileAssetNapi != nullptr, "Failed to get FileAssetNapi object");
72 
73         auto fileAssetPtr = fileAssetNapi->GetFileAssetInstance();
74         CHECK_COND_WITH_MESSAGE(env, fileAssetPtr != nullptr, "fileAsset is null");
75         CHECK_COND_WITH_MESSAGE(env,
76             fileAssetPtr->GetResultNapiType() == ResultNapiType::TYPE_PHOTOACCESS_HELPER &&
77                 (fileAssetPtr->GetMediaType() == MEDIA_TYPE_IMAGE || fileAssetPtr->GetMediaType() == MEDIA_TYPE_VIDEO),
78             "Unsupported type of fileAsset");
79         fileAssets.push_back(fileAssetPtr);
80     }
81     RETURN_NAPI_TRUE(env);
82 }
83 
Constructor(napi_env env,napi_callback_info info)84 napi_value MediaAssetsChangeRequestNapi::Constructor(napi_env env, napi_callback_info info)
85 {
86     if (!MediaLibraryNapiUtils::IsSystemApp()) {
87         NapiError::ThrowError(env, E_CHECK_SYSTEMAPP_FAIL, "The constructor can be called only by system apps");
88         return nullptr;
89     }
90 
91     napi_value newTarget = nullptr;
92     CHECK_ARGS(env, napi_get_new_target(env, info, &newTarget), JS_INNER_FAIL);
93     CHECK_COND_RET(newTarget != nullptr, nullptr, "Failed to check new.target");
94 
95     size_t argc = ARGS_ONE;
96     napi_value argv[ARGS_ONE] = { 0 };
97     napi_value thisVar = nullptr;
98     CHECK_ARGS(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr), JS_INNER_FAIL);
99     CHECK_COND_WITH_MESSAGE(env, argc == ARGS_ONE, "Number of args is invalid");
100 
101     vector<shared_ptr<FileAsset>> fileAssets;
102     CHECK_COND_WITH_MESSAGE(env, GetAssetArray(env, argv[PARAM0], fileAssets), "Failed to parse args");
103     unique_ptr<MediaAssetsChangeRequestNapi> obj = make_unique<MediaAssetsChangeRequestNapi>();
104     CHECK_COND(env, obj != nullptr, JS_INNER_FAIL);
105     obj->fileAssets_ = fileAssets;
106     CHECK_ARGS(env,
107         napi_wrap(env, thisVar, reinterpret_cast<void*>(obj.get()), MediaAssetsChangeRequestNapi::Destructor, nullptr,
108             nullptr),
109         JS_INNER_FAIL);
110     obj.release();
111     return thisVar;
112 }
113 
Destructor(napi_env env,void * nativeObject,void * finalizeHint)114 void MediaAssetsChangeRequestNapi::Destructor(napi_env env, void* nativeObject, void* finalizeHint)
115 {
116     auto* assetsChangeRequest = reinterpret_cast<MediaAssetsChangeRequestNapi*>(nativeObject);
117     if (assetsChangeRequest != nullptr) {
118         delete assetsChangeRequest;
119         assetsChangeRequest = nullptr;
120     }
121 }
122 
GetFileAssetUriArray() const123 vector<string> MediaAssetsChangeRequestNapi::GetFileAssetUriArray() const
124 {
125     vector<string> uriArray;
126     uriArray.reserve(fileAssets_.size());
127     for (const auto& fileAsset : fileAssets_) {
128         uriArray.push_back(fileAsset->GetUri());
129     }
130     return uriArray;
131 }
132 
GetFavoriteStatus() const133 bool MediaAssetsChangeRequestNapi::GetFavoriteStatus() const
134 {
135     return isFavorite_;
136 }
137 
GetHiddenStatus() const138 bool MediaAssetsChangeRequestNapi::GetHiddenStatus() const
139 {
140     return isHidden_;
141 }
142 
GetUpdatedUserComment() const143 string MediaAssetsChangeRequestNapi::GetUpdatedUserComment() const
144 {
145     return userComment_;
146 }
147 
GetRecentShowStatus() const148 bool MediaAssetsChangeRequestNapi::GetRecentShowStatus() const
149 {
150     return isRecentShow_;
151 }
152 
CheckChangeOperations(napi_env env)153 bool MediaAssetsChangeRequestNapi::CheckChangeOperations(napi_env env)
154 {
155     if (assetsChangeOperations_.empty()) {
156         NapiError::ThrowError(env, OHOS_INVALID_PARAM_CODE, "None request to apply");
157         return false;
158     }
159 
160     if (fileAssets_.empty()) {
161         NapiError::ThrowError(env, OHOS_INVALID_PARAM_CODE, "fileAssets is empty");
162         return false;
163     }
164 
165     for (const auto& fileAsset : fileAssets_) {
166         if (fileAsset == nullptr || fileAsset->GetId() <= 0 || fileAsset->GetUri().empty()) {
167             NapiError::ThrowError(env, OHOS_INVALID_PARAM_CODE, "Invalid fileAsset to apply");
168             return false;
169         }
170     }
171 
172     return true;
173 }
174 
JSSetFavorite(napi_env env,napi_callback_info info)175 napi_value MediaAssetsChangeRequestNapi::JSSetFavorite(napi_env env, napi_callback_info info)
176 {
177     if (!MediaLibraryNapiUtils::IsSystemApp()) {
178         NapiError::ThrowError(env, E_CHECK_SYSTEMAPP_FAIL, "This interface can be called only by system apps");
179         return nullptr;
180     }
181 
182     auto asyncContext = make_unique<MediaAssetsChangeRequestAsyncContext>();
183     bool isFavorite;
184     CHECK_COND_WITH_MESSAGE(env,
185         MediaLibraryNapiUtils::ParseArgsBoolCallBack(env, info, asyncContext, isFavorite) == napi_ok,
186         "Failed to parse args");
187     CHECK_COND_WITH_MESSAGE(env, asyncContext->argc == ARGS_ONE, "Number of args is invalid");
188 
189     auto changeRequest = asyncContext->objectInfo;
190     changeRequest->isFavorite_ = isFavorite;
191     for (const auto& fileAsset : changeRequest->fileAssets_) {
192         fileAsset->SetFavorite(isFavorite);
193     }
194     changeRequest->assetsChangeOperations_.push_back(AssetsChangeOperation::BATCH_SET_FAVORITE);
195     RETURN_NAPI_UNDEFINED(env);
196 }
197 
JSSetHidden(napi_env env,napi_callback_info info)198 napi_value MediaAssetsChangeRequestNapi::JSSetHidden(napi_env env, napi_callback_info info)
199 {
200     if (!MediaLibraryNapiUtils::IsSystemApp()) {
201         NapiError::ThrowError(env, E_CHECK_SYSTEMAPP_FAIL, "This interface can be called only by system apps");
202         return nullptr;
203     }
204 
205     auto asyncContext = make_unique<MediaAssetsChangeRequestAsyncContext>();
206     bool isHidden;
207     CHECK_COND_WITH_MESSAGE(env,
208         MediaLibraryNapiUtils::ParseArgsBoolCallBack(env, info, asyncContext, isHidden) == napi_ok,
209         "Failed to parse args");
210     CHECK_COND_WITH_MESSAGE(env, asyncContext->argc == ARGS_ONE, "Number of args is invalid");
211 
212     auto changeRequest = asyncContext->objectInfo;
213     changeRequest->isHidden_ = isHidden;
214     for (const auto& fileAsset : changeRequest->fileAssets_) {
215         fileAsset->SetHidden(isHidden);
216     }
217     changeRequest->assetsChangeOperations_.push_back(AssetsChangeOperation::BATCH_SET_HIDDEN);
218     RETURN_NAPI_UNDEFINED(env);
219 }
220 
JSSetUserComment(napi_env env,napi_callback_info info)221 napi_value MediaAssetsChangeRequestNapi::JSSetUserComment(napi_env env, napi_callback_info info)
222 {
223     if (!MediaLibraryNapiUtils::IsSystemApp()) {
224         NapiError::ThrowError(env, E_CHECK_SYSTEMAPP_FAIL, "This interface can be called only by system apps");
225         return nullptr;
226     }
227 
228     auto asyncContext = make_unique<MediaAssetsChangeRequestAsyncContext>();
229     string userComment;
230     CHECK_COND_WITH_MESSAGE(env,
231         MediaLibraryNapiUtils::ParseArgsStringCallback(env, info, asyncContext, userComment) == napi_ok,
232         "Failed to parse args");
233     CHECK_COND_WITH_MESSAGE(env, asyncContext->argc == ARGS_ONE, "Number of args is invalid");
234     CHECK_COND_WITH_MESSAGE(env, userComment.length() <= USER_COMMENT_MAX_LEN, "user comment too long");
235 
236     auto changeRequest = asyncContext->objectInfo;
237     changeRequest->userComment_ = userComment;
238     for (const auto& fileAsset : changeRequest->fileAssets_) {
239         fileAsset->SetUserComment(userComment);
240     }
241     changeRequest->assetsChangeOperations_.push_back(AssetsChangeOperation::BATCH_SET_USER_COMMENT);
242     RETURN_NAPI_UNDEFINED(env);
243 }
244 
JSSetIsRecentShow(napi_env env,napi_callback_info info)245 napi_value MediaAssetsChangeRequestNapi::JSSetIsRecentShow(napi_env env, napi_callback_info info)
246 {
247     if (!MediaLibraryNapiUtils::IsSystemApp()) {
248         NapiError::ThrowError(env, E_CHECK_SYSTEMAPP_FAIL, "This interface can be called only by system apps");
249         return nullptr;
250     }
251 
252     auto asyncContext = make_unique<MediaAssetsChangeRequestAsyncContext>();
253     bool isRecentShow;
254     CHECK_COND_WITH_MESSAGE(env,
255         MediaLibraryNapiUtils::ParseArgsBoolCallBack(env, info, asyncContext, isRecentShow) == napi_ok,
256         "Failed to parse args");
257     CHECK_COND_WITH_MESSAGE(env, asyncContext->argc == ARGS_ONE, "Number of args is invalid");
258 
259     auto changeRequest = asyncContext->objectInfo;
260     changeRequest->isRecentShow_ = isRecentShow;
261     for (const auto& fileAsset : changeRequest->fileAssets_) {
262         fileAsset->SetRecentShow(isRecentShow);
263     }
264     changeRequest->assetsChangeOperations_.push_back(AssetsChangeOperation::BATCH_SET_RECENT_SHOW);
265     RETURN_NAPI_UNDEFINED(env);
266 }
267 
SetAssetsPropertyExecute(MediaAssetsChangeRequestAsyncContext & context,const AssetsChangeOperation & changeOperation)268 static bool SetAssetsPropertyExecute(
269     MediaAssetsChangeRequestAsyncContext& context, const AssetsChangeOperation& changeOperation)
270 {
271     MediaLibraryTracer tracer;
272     tracer.Start("SetAssetsPropertyExecute");
273 
274     string uri;
275     DataShare::DataSharePredicates predicates;
276     DataShare::DataShareValuesBucket valuesBucket;
277     auto changeRequest = context.objectInfo;
278     predicates.In(PhotoColumn::MEDIA_ID, changeRequest->GetFileAssetUriArray());
279     switch (changeOperation) {
280         case AssetsChangeOperation::BATCH_SET_FAVORITE:
281             uri = PAH_BATCH_UPDATE_FAVORITE;
282             valuesBucket.Put(PhotoColumn::MEDIA_IS_FAV, changeRequest->GetFavoriteStatus() ? YES : NO);
283             NAPI_INFO_LOG("Batch set favorite: %{public}d", changeRequest->GetFavoriteStatus() ? YES : NO);
284             break;
285         case AssetsChangeOperation::BATCH_SET_HIDDEN:
286             uri = PAH_HIDE_PHOTOS;
287             valuesBucket.Put(PhotoColumn::MEDIA_HIDDEN, changeRequest->GetHiddenStatus() ? YES : NO);
288             break;
289         case AssetsChangeOperation::BATCH_SET_USER_COMMENT:
290             uri = PAH_BATCH_UPDATE_USER_COMMENT;
291             valuesBucket.Put(PhotoColumn::PHOTO_USER_COMMENT, changeRequest->GetUpdatedUserComment());
292             break;
293         case AssetsChangeOperation::BATCH_SET_RECENT_SHOW:
294             uri = PAH_BATCH_UPDATE_RECENT_SHOW;
295             valuesBucket.Put(PhotoColumn::PHOTO_IS_RECENT_SHOW, changeRequest->GetRecentShowStatus() ? YES : NO);
296             break;
297         default:
298             context.SaveError(E_FAIL);
299             NAPI_ERR_LOG("Unsupported assets change operation: %{public}d", changeOperation);
300             return false;
301     }
302 
303     NAPI_INFO_LOG("changeOperation:%{public}d, size:%{public}zu",
304         changeOperation, changeRequest->GetFileAssetUriArray().size());
305     MediaLibraryNapiUtils::UriAppendKeyValue(uri, API_VERSION, to_string(MEDIA_API_VERSION_V10));
306     Uri updateAssetsUri(uri);
307     int32_t changedRows = UserFileClient::Update(updateAssetsUri, predicates, valuesBucket);
308     if (changedRows < 0) {
309         context.SaveError(changedRows);
310         NAPI_ERR_LOG("Failed to set property, operation: %{public}d, err: %{public}d", changeOperation, changedRows);
311         return false;
312     }
313     return true;
314 }
315 
ApplyAssetsChangeRequestExecute(napi_env env,void * data)316 static void ApplyAssetsChangeRequestExecute(napi_env env, void* data)
317 {
318     MediaLibraryTracer tracer;
319     tracer.Start("ApplyAssetsChangeRequestExecute");
320 
321     auto* context = static_cast<MediaAssetsChangeRequestAsyncContext*>(data);
322     unordered_set<AssetsChangeOperation> appliedOperations;
323     for (const auto& changeOperation : context->assetsChangeOperations) {
324         // Keep the final result(s) of each operation, and commit only once.
325         if (appliedOperations.find(changeOperation) != appliedOperations.end()) {
326             continue;
327         }
328 
329         bool valid = SetAssetsPropertyExecute(*context, changeOperation);
330         if (!valid) {
331             NAPI_ERR_LOG("Failed to apply assets change request, operation: %{public}d", changeOperation);
332             return;
333         }
334         appliedOperations.insert(changeOperation);
335     }
336 }
337 
ApplyAssetsChangeRequestCompleteCallback(napi_env env,napi_status status,void * data)338 static void ApplyAssetsChangeRequestCompleteCallback(napi_env env, napi_status status, void* data)
339 {
340     MediaLibraryTracer tracer;
341     tracer.Start("ApplyAssetsChangeRequestCompleteCallback");
342 
343     auto* context = static_cast<MediaAssetsChangeRequestAsyncContext*>(data);
344     CHECK_NULL_PTR_RETURN_VOID(context, "Async context is null");
345     auto jsContext = make_unique<JSAsyncContextOutput>();
346     jsContext->status = false;
347     napi_get_undefined(env, &jsContext->data);
348     napi_get_undefined(env, &jsContext->error);
349     if (context->error == ERR_DEFAULT) {
350         jsContext->status = true;
351     } else {
352         context->HandleError(env, jsContext->error);
353     }
354 
355     if (context->work != nullptr) {
356         MediaLibraryNapiUtils::InvokeJSAsyncMethod(
357             env, context->deferred, context->callbackRef, context->work, *jsContext);
358     }
359     delete context;
360 }
361 
ApplyChanges(napi_env env,napi_callback_info info)362 napi_value MediaAssetsChangeRequestNapi::ApplyChanges(napi_env env, napi_callback_info info)
363 {
364     constexpr size_t minArgs = ARGS_ONE;
365     constexpr size_t maxArgs = ARGS_TWO;
366     auto asyncContext = make_unique<MediaAssetsChangeRequestAsyncContext>();
367     CHECK_COND_WITH_MESSAGE(env,
368         MediaLibraryNapiUtils::AsyncContextGetArgs(env, info, asyncContext, minArgs, maxArgs) == napi_ok,
369         "Failed to get args");
370     asyncContext->objectInfo = this;
371 
372     CHECK_COND_WITH_MESSAGE(env, CheckChangeOperations(env), "Failed to check assets change request operations");
373     asyncContext->assetsChangeOperations = assetsChangeOperations_;
374     assetsChangeOperations_.clear();
375     return MediaLibraryNapiUtils::NapiCreateAsyncWork(env, asyncContext, "ApplyMediaAssetsChangeRequest",
376         ApplyAssetsChangeRequestExecute, ApplyAssetsChangeRequestCompleteCallback);
377 }
378 } // namespace OHOS::Media