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