• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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