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