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