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 16import BackupExtensionAbility, {BundleVersion} from '@ohos.application.BackupExtensionAbility'; 17import fs from '@ohos.file.fs'; 18// @ts-ignore 19import mediabackup from '@ohos.multimedia.mediabackup'; 20 21const TAG = 'MediaBackupExtAbility'; 22 23const galleryAppName = 'com.huawei.photos'; 24const mediaAppName = 'com.android.providers.media.module'; 25 26const UPGRADE_RESTORE : number = 0; 27const DUAL_FRAME_CLONE_RESTORE : number = 1; 28const CLONE_RESTORE : number = 2; 29const I_PHONE_CLONE_RESTORE : number = 3; 30const OTHERS_PHONE_CLONE_RESTORE : number = 4; 31 32const UPGRADE_NAME = '0.0.0.0'; 33const I_PHONE_FRAME_CLONE_NAME = '99.99.99.997'; 34const OTHERS_PHONE_FRAME_CLONE_NAME = '99.99.99.998'; 35const DUAL_FRAME_CLONE_NAME = '99.99.99.999'; 36const STAT_KEY_RESULT_INFO = 'resultInfo'; 37const STAT_KEY_TYPE = 'type'; 38const STAT_KEY_ERROR_CODE = 'errorCode'; 39const STAT_KEY_ERROR_INFO = 'errorInfo'; 40const STAT_KEY_INFOS = 'infos'; 41const STAT_KEY_BACKUP_INFO = 'backupInfo'; 42const STAT_KEY_SUCCESS_COUNT = 'successCount'; 43const STAT_KEY_DUPLICATE_COUNT = 'duplicateCount'; 44const STAT_KEY_FAILED_COUNT = 'failedCount'; 45const STAT_KEY_DETAILS = 'details'; 46const STAT_KEY_NUMBER = 'number'; 47const STAT_KEY_PROGRESS_INFO = 'progressInfo'; 48const STAT_KEY_NAME = 'name'; 49const STAT_KEY_PROCESSED = 'processed'; 50const STAT_KEY_TOTAL = 'total'; 51const STAT_KEY_IS_PERCENTAGE = 'isPercentage'; 52const STAT_VALUE_ERROR_INFO = 'ErrorInfo'; 53const STAT_VALUE_COUNT_INFO = 'CountInfo'; 54const STAT_TYPE_PHOTO = 'photo'; 55const STAT_TYPE_VIDEO = 'video'; 56const STAT_TYPE_AUDIO = 'audio'; 57const STAT_TYPE_PHOTO_VIDEO = 'photo&video'; 58const STAT_TYPE_UPDATE = 'update'; 59const STAT_TYPE_OTHER = 'other'; 60const STAT_TYPE_ONGOING = 'ongoing'; 61const STAT_TYPES = [STAT_TYPE_PHOTO, STAT_TYPE_VIDEO, STAT_TYPE_AUDIO]; 62const RESULT_INFO_NUM = 2; 63const JS_TYPE_STRING = 'string'; 64const JS_TYPE_BOOLEAN = 'boolean'; 65const DEFAULT_RESTORE_EX_INFO = { 66 'resultInfo': 67 [ 68 { 69 'type': STAT_VALUE_ERROR_INFO, 70 'errorCode': '13500099', 71 'errorInfo': 'Get restoreEx info failed' 72 }, 73 { 74 'type': STAT_VALUE_COUNT_INFO, 75 'infos': 76 [ 77 { 78 'backupInfo': STAT_TYPE_PHOTO, 79 'successCount': 0, 80 'duplicateCount': 0, 81 'failedCount': 0, 82 'details': null 83 }, 84 { 85 'backupInfo': STAT_TYPE_VIDEO, 86 'successCount': 0, 87 'duplicateCount': 0, 88 'failedCount': 0, 89 'details': null 90 }, 91 { 92 'backupInfo': STAT_TYPE_AUDIO, 93 'successCount': 0, 94 'duplicateCount': 0, 95 'failedCount': 0, 96 'details': null 97 } 98 ] 99 } 100 ] 101}; 102const DEFAULT_BACKUP_INFO = [ 103 { 104 'backupInfo': STAT_TYPE_PHOTO, 105 'number': 0 106 }, 107 { 108 'backupInfo': STAT_TYPE_VIDEO, 109 'number': 0 110 }, 111 { 112 'backupInfo': STAT_TYPE_AUDIO, 113 'number': 0 114 } 115]; 116const DEFAULT_PROGRESS_INFO = { 117 'progressInfo': [ 118 { 119 'name': STAT_TYPE_PHOTO_VIDEO, 120 'processed': 0, 121 'total': 0, 122 'isPercentage': false 123 }, 124 { 125 'name': STAT_TYPE_AUDIO, 126 'processed': 0, 127 'total': 0, 128 'isPercentage': false 129 }, 130 { 131 'name': STAT_TYPE_UPDATE, 132 'processed': 0, 133 'total': 0, 134 'isPercentage': false 135 }, 136 { 137 'name': STAT_TYPE_OTHER, 138 'processed': 0, 139 'total': 0, 140 'isPercentage': false 141 }, 142 { 143 'name': STAT_TYPE_ONGOING, 144 'processed': 0, 145 'total': 0, 146 'isPercentage': false 147 }] 148}; 149 150export default class MediaBackupExtAbility extends BackupExtensionAbility { 151 async onBackup() : Promise<void> { 152 console.log(TAG, 'onBackup ok.'); 153 console.time(TAG + ' BACKUP'); 154 await mediabackup.startBackup(CLONE_RESTORE, galleryAppName, mediaAppName); 155 console.timeEnd(TAG + ' BACKUP'); 156 } 157 158 async onRestore(bundleVersion : BundleVersion) : Promise<void> { 159 console.log(TAG, `onRestore ok ${JSON.stringify(bundleVersion)}`); 160 console.time(TAG + ' RESTORE'); 161 const backupDir = this.context.backupDir + 'restore'; 162 let sceneCode: number = this.getSceneCode(bundleVersion); 163 await mediabackup.startRestore(this.context, sceneCode, galleryAppName, mediaAppName, backupDir); 164 console.timeEnd(TAG + ' RESTORE'); 165 } 166 167 async onRestoreEx(bundleVersion: BundleVersion, bundleInfo: string): Promise<string> { 168 console.log(TAG, `onRestoreEx ok ${JSON.stringify(bundleVersion)}, ${JSON.stringify(bundleInfo)}`); 169 console.time(TAG + ' RESTORE EX'); 170 const backupDir = this.context.backupDir + 'restore'; 171 let sceneCode: number = this.getSceneCode(bundleVersion); 172 let restoreExResult: string = await mediabackup.startRestoreEx(this.context, sceneCode, galleryAppName, mediaAppName, backupDir, 173 bundleInfo); 174 let restoreExInfo: string = await this.getRestoreExInfo(sceneCode, restoreExResult); 175 console.log(TAG, `GET restoreExInfo: ${restoreExInfo}`); 176 console.timeEnd(TAG + ' RESTORE EX'); 177 return restoreExInfo; 178 } 179 180 getBackupInfo(): string { 181 console.log(TAG, 'getBackupInfo ok'); 182 let tmpBackupInfo: string = mediabackup.getBackupInfo(CLONE_RESTORE); 183 let backupInfo: string; 184 if (!this.isBackupInfoValid(tmpBackupInfo)) { 185 console.error(TAG, 'backupInfo is invalid, return default'); 186 backupInfo = JSON.stringify(DEFAULT_BACKUP_INFO); 187 } else { 188 backupInfo = tmpBackupInfo; 189 } 190 console.log(TAG, `GET backupInfo: ${backupInfo}`); 191 return backupInfo; 192 } 193 194 onProcess(): string { 195 console.log(TAG, 'onProcess ok'); 196 let progressInfo: string = mediabackup.getProgressInfo(); 197 if (progressInfo.length === 0 || !this.isProgressInfoValid(progressInfo)) { 198 console.error(TAG, 'progressInfo is empty or invalid, return default'); 199 progressInfo = JSON.stringify(DEFAULT_PROGRESS_INFO); 200 } 201 console.log(TAG, `GET progressInfo: ${progressInfo}`); 202 return progressInfo; 203 } 204 205 private async getRestoreExInfo(sceneCode: number, restoreExResult: string): Promise<string> { 206 if (!this.isRestoreExResultValid(restoreExResult)) { 207 console.error(TAG, 'restoreEx result is invalid, use default'); 208 return JSON.stringify(DEFAULT_RESTORE_EX_INFO); 209 } 210 if (sceneCode !== UPGRADE_RESTORE) { 211 return restoreExResult; 212 } 213 try { 214 let jsonObject = JSON.parse(restoreExResult); 215 for (let info of jsonObject.resultInfo) { 216 if (info.type !== STAT_VALUE_COUNT_INFO) { 217 continue; 218 } 219 for (let subCountInfo of info.infos) { 220 let type = subCountInfo.backupInfo; 221 let detailsPath = subCountInfo.details; 222 subCountInfo.details = await this.getDetails(type, detailsPath); 223 } 224 } 225 return JSON.stringify(jsonObject); 226 } catch (err) { 227 console.error(TAG, `getRestoreExInfo error message: ${err.message}, code: ${err.code}`); 228 return JSON.stringify(DEFAULT_RESTORE_EX_INFO); 229 } 230 } 231 232 private async getDetails(type: string, detailsPath: string): Promise<null | number> { 233 if (detailsPath.length === 0) { 234 console.log(TAG, `${type} has no failed files`); 235 return null; 236 } 237 let file = await fs.open(detailsPath); 238 console.log(TAG, `${type} details fd: ${file.fd}`); 239 return file.fd; 240 } 241 242 private isRestoreExResultValid(restoreExResult: string): boolean { 243 try { 244 let jsonObject = JSON.parse(restoreExResult); 245 if (!this.hasKey(jsonObject, STAT_KEY_RESULT_INFO)) { 246 return false; 247 } 248 let resultInfo = jsonObject[STAT_KEY_RESULT_INFO]; 249 if (resultInfo.length !== RESULT_INFO_NUM) { 250 console.error(TAG, `resultInfo num ${resultInfo.length} != ${RESULT_INFO_NUM}`); 251 return false; 252 } 253 let errorInfo = resultInfo[0]; 254 let countInfo = resultInfo[1]; 255 return this.isErrorInfoValid(errorInfo) && this.isCountInfoValid(countInfo); 256 } catch (err) { 257 console.error(TAG, `isRestoreExResultValid error message: ${err.message}, code: ${err.code}`); 258 return false; 259 } 260 } 261 262 private isErrorInfoValid(errorInfo: JSON): boolean { 263 if (!this.hasKey(errorInfo, STAT_KEY_TYPE) || !this.hasKey(errorInfo, STAT_KEY_ERROR_CODE) || 264 !this.hasKey(errorInfo, STAT_KEY_ERROR_INFO)) { 265 return false; 266 } 267 if (errorInfo[STAT_KEY_TYPE] !== STAT_VALUE_ERROR_INFO) { 268 console.error(TAG, `errorInfo ${errorInfo[STAT_KEY_TYPE]} != ${STAT_VALUE_ERROR_INFO}`); 269 return false; 270 } 271 if (!this.checkType(typeof errorInfo[STAT_KEY_ERROR_CODE], JS_TYPE_STRING) || 272 !this.checkType(typeof errorInfo[STAT_KEY_ERROR_INFO], JS_TYPE_STRING)) { 273 return false; 274 } 275 return true; 276 } 277 278 private isCountInfoValid(countInfo: JSON): boolean { 279 if (!this.hasKey(countInfo, STAT_KEY_TYPE) || !this.hasKey(countInfo, STAT_KEY_INFOS)) { 280 return false; 281 } 282 if (countInfo[STAT_KEY_TYPE] !== STAT_VALUE_COUNT_INFO) { 283 console.error(TAG, `countInfo ${countInfo[STAT_KEY_TYPE]} != ${STAT_VALUE_COUNT_INFO}`); 284 return false; 285 } 286 let subCountInfos = countInfo[STAT_KEY_INFOS]; 287 if (subCountInfos.length !== STAT_TYPES.length) { 288 console.error(TAG, `countInfo ${subCountInfos.length} != ${STAT_TYPES.length}`); 289 return false; 290 } 291 let hasPhoto = false; 292 let hasVideo = false; 293 let hasAudio = false; 294 for (let subCountInfo of subCountInfos) { 295 if (!this.isSubCountInfoValid(subCountInfo)) { 296 return false; 297 } 298 hasPhoto = hasPhoto || subCountInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_PHOTO; 299 hasVideo = hasVideo || subCountInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_VIDEO; 300 hasAudio = hasAudio || subCountInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_AUDIO; 301 } 302 return hasPhoto && hasVideo && hasAudio; 303 } 304 305 private isSubCountInfoValid(subCountInfo: JSON): boolean { 306 if (!this.hasKey(subCountInfo, STAT_KEY_BACKUP_INFO) || !this.hasKey(subCountInfo, STAT_KEY_SUCCESS_COUNT) || 307 !this.hasKey(subCountInfo, STAT_KEY_DUPLICATE_COUNT) || !this.hasKey(subCountInfo, STAT_KEY_FAILED_COUNT) || 308 !this.hasKey(subCountInfo, STAT_KEY_DETAILS)) { 309 return false; 310 } 311 if (!STAT_TYPES.includes(subCountInfo[STAT_KEY_BACKUP_INFO])) { 312 console.error(TAG, `SubCountInfo ${subCountInfo[STAT_KEY_BACKUP_INFO]} not in ${JSON.stringify(STAT_TYPES)}`); 313 return false; 314 } 315 return !isNaN(subCountInfo[STAT_KEY_SUCCESS_COUNT]) && !isNaN(subCountInfo[STAT_KEY_DUPLICATE_COUNT]) && 316 !isNaN(subCountInfo[STAT_KEY_FAILED_COUNT]); 317 } 318 319 private isBackupInfoValid(backupInfo: string): boolean { 320 try { 321 let jsonObject = JSON.parse(backupInfo); 322 if (jsonObject.length !== STAT_TYPES.length) { 323 console.error(TAG, `backupInfo num ${jsonObject.length} != ${STAT_TYPES.length}`); 324 return false; 325 } 326 let hasPhoto = false; 327 let hasVideo = false; 328 let hasAudio = false; 329 for (let subBackupInfo of jsonObject) { 330 if (!this.isSubBackupInfoValid(subBackupInfo)) { 331 return false; 332 } 333 hasPhoto = hasPhoto || subBackupInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_PHOTO; 334 hasVideo = hasVideo || subBackupInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_VIDEO; 335 hasAudio = hasAudio || subBackupInfo[STAT_KEY_BACKUP_INFO] === STAT_TYPE_AUDIO; 336 } 337 return hasPhoto && hasVideo && hasAudio; 338 } catch (err) { 339 console.error(TAG, `isBackupInfoValid error message: ${err.message}, code: ${err.code}`); 340 return false; 341 } 342 } 343 344 private isSubBackupInfoValid(subBackupInfo: JSON): boolean { 345 if (!this.hasKey(subBackupInfo, STAT_KEY_BACKUP_INFO) || !this.hasKey(subBackupInfo, STAT_KEY_NUMBER)) { 346 return false; 347 } 348 if (!STAT_TYPES.includes(subBackupInfo[STAT_KEY_BACKUP_INFO])) { 349 console.error(TAG, `SubBackupInfo ${subBackupInfo[STAT_KEY_BACKUP_INFO]} not in ${JSON.stringify(STAT_TYPES)}`); 350 return false; 351 } 352 return !isNaN(subBackupInfo[STAT_KEY_NUMBER]); 353 } 354 355 private hasKey(jsonObject: JSON, key: string): boolean { 356 if (!(key in jsonObject)) { 357 console.error(TAG, `hasKey ${key} not found`); 358 return false; 359 } 360 return true; 361 } 362 363 private checkType(varType: string, expectedType: string): boolean { 364 if (varType !== expectedType) { 365 console.error(TAG, `checkType ${varType} != ${expectedType}`); 366 return false; 367 } 368 return true; 369 } 370 371 private isProgressInfoValid(progressInfo: string): boolean { 372 try { 373 let jsonObject = JSON.parse(progressInfo); 374 if (!this.hasKey(jsonObject, STAT_KEY_PROGRESS_INFO)) { 375 return false; 376 } 377 let subProcessInfos = jsonObject[STAT_KEY_PROGRESS_INFO]; 378 for (let subProcessInfo of subProcessInfos) { 379 if (!this.isSubProcessInfoValid(subProcessInfo)) { 380 return false; 381 } 382 } 383 return true; 384 } catch (err) { 385 console.error(TAG, `isProgressInfoValid error message: ${err.message}, code: ${err.code}`); 386 return false; 387 } 388 } 389 390 private isSubProcessInfoValid(subProcessInfo: JSON): boolean { 391 if (!this.hasKey(subProcessInfo, STAT_KEY_NAME) || !this.hasKey(subProcessInfo, STAT_KEY_PROCESSED) || 392 !this.hasKey(subProcessInfo, STAT_KEY_TOTAL) || !this.hasKey(subProcessInfo, STAT_KEY_IS_PERCENTAGE)) { 393 return false; 394 } 395 return !isNaN(subProcessInfo[STAT_KEY_PROCESSED]) && !isNaN(subProcessInfo[STAT_KEY_TOTAL]) && 396 this.checkType(typeof subProcessInfo[STAT_KEY_IS_PERCENTAGE], JS_TYPE_BOOLEAN); 397 } 398 399 private getSceneCode(bundleVersion: BundleVersion): number { 400 if (bundleVersion.name.startsWith(UPGRADE_NAME)) { 401 return UPGRADE_RESTORE; 402 } 403 if (bundleVersion.name === DUAL_FRAME_CLONE_NAME && bundleVersion.code === 0) { 404 return DUAL_FRAME_CLONE_RESTORE; 405 } 406 if (bundleVersion.name === OTHERS_PHONE_FRAME_CLONE_NAME && bundleVersion.code === 0) { 407 return OTHERS_PHONE_CLONE_RESTORE; 408 } 409 if (bundleVersion.name === I_PHONE_FRAME_CLONE_NAME && bundleVersion.code === 0) { 410 return I_PHONE_CLONE_RESTORE; 411 } 412 return CLONE_RESTORE; 413 } 414} 415