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