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