/**
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BusinessError } from '@ohos.base';
import LogUtils from '../common/utils/LogUtils';
import dataShare from '@ohos.data.dataShare';
import common from '@ohos.app.ability.common';
import rdb from '@ohos.data.relationalStore';
import { ContactInfo } from '../common/data/ContactInfo';
import { ValuesBucket } from '@ohos.data.ValuesBucket';
import { Contacts } from '../common/data/Contacts';
import relationalStore from '@ohos.data.relationalStore';
import HandleUniqueKeyService, { MergeHandleCount } from './HandleUniqueKeyService';
import BirthdayManager from './BirthdayManager';
import ContactAggregator from '../aggregation/ContactAggregator';
import { contextConstant } from '@kit.AbilityKit';
import { image } from '@kit.ImageKit';
import DbConstants, { DIRTY_ENUM } from '../common/DbConstants';
import CallLogSingleConstant from '../common/contract/CallLogSingleConstant';
import GroupIdUtil from '../common/utils/GroupIdUtil';
import { contactRdbHelper } from '../common/helperUtils/ContactsRdbHelperForCloudSync';
import RawContactsColumns from '../common/data/RawContactsColumns';
import MergeRawContact from '../common/data/MergeRawContact';
import { DeleteRawContact } from '../common/contract/DeleteRawContact';
import ContactsColumns from '../common/contract/ContactsColumns';
import DataColumns from '../common/data/DataColumns';
import { getFormatNumber } from '../common/utils/NumberFormatUtil';
import { callLogRdbHelper } from '../common/helperUtils/CallLogRdbHelperForCloudSync';
import { Data } from '../common/data/Data';
const TAG = 'ContactsDataService';
const TIMEOUT_AGGREGATION_ALL_ONCE = 200;
const MIN_RESOLUTION = 409600;
export class DisPlayNameResult {
public displayName: string = '';
public updateFlag: number = 0;
}
interface UpdateInfo {
updateBucket: ValuesBucket,
predicates: rdb.RdbPredicates
}
export interface ExtraInfo {
deletePhoneList: string[],
addPhoneList: string[],
deleteFormatPhoneList: string[],
addFormatPhoneList: string[],
}
export interface PhotoFileInfo {
filePath: string,
isCloud?: boolean,
blobData: Uint8Array,
blobSource?: number,
resolution?: number,
fileSize?: number,
}
// 无名氏首字母 ...
export const ANONYMOUS_SORT_FIRST_LETTER = '...';
// 无名氏排序数字,无名氏信息需要排到字母的后边
// 如果是字母最大是z,对应90,升序排列,无名氏需要在90后边
// 排序字段存99
export const ANONYMOUS_SORT = '99';
export default class ContactsDataService {
private static instance: ContactsDataService;
private groupDir: string = '';
private dataShareHelper: dataShare.DataShareHelper | undefined = undefined;
private isMergeContactEnd: boolean = true;
private checkingCursor: boolean = false;
private mergeWorkerId: number = 0;
public context: common.ServiceExtensionContext | undefined = undefined;
public QUERY_RAW_CONTACT_PAGE_NUM: number = 50;
public dataShareHelperContactSyncSwitchUri?: Promise<dataShare.DataShareHelper> | dataShare.DataShareHelper;
public callLogDataShareHelper: dataShare.DataShareHelper | undefined = undefined;
// 单次克隆,相同form_id仅删除一次
private formIdSet: Set<string> = new Set();
// 是否处理退账号删除联系人,退账号删除联系人操作不被功耗管控
private isHandleLogoutDelContactFlag = false;
public getIsHandleLogoutDelContactFlag() {
return this.isHandleLogoutDelContactFlag;
}
private constructor() {
}
init(context: common.ServiceExtensionContext) {
this.context = context;
BirthdayManager.getInstance().init(this.context);
this.getGroupDir(context);
}
getGroupDir(context: Context) {
let ctx = context;
ctx.area = contextConstant.AreaMode.EL2;
ctx.getGroupDir(GroupIdUtil.groupId).then((data) => {
this.groupDir = data;
}).catch((error: BusinessError) => {
LogUtils.w(TAG, 'getGroupDir error:%s' + error?.message + ', stack: ' + error?.stack);
});
}
public static getInstance(): ContactsDataService {
if (ContactsDataService.instance == null) {
ContactsDataService.instance = new ContactsDataService();
}
return ContactsDataService.instance;
}
public isMergingContact(): boolean {
LogUtils.i(TAG, `mergeContact check,isMergeContactEnd:${this.isMergeContactEnd}, mergeWorkerId:${this.mergeWorkerId}`);
return this.isMergeContactEnd == false || this.mergeWorkerId != 0;
}
public setMergingContact(isMerging: boolean) {
LogUtils.i(TAG, `mergeContact setMergingContact:${isMerging}`);
if (isMerging) {
this.isMergeContactEnd = false;
this.mergeWorkerId = Date.now();
} else {
this.isMergeContactEnd = true;
this.mergeWorkerId = 0;
}
}
/**
* 本地表dirty为1,刷新至云表
* Query the contact from the raw-cloud.
* If the uuid not exist, it means that it is a insert addition.
* If the uuid exists, it means that it is an update operation.
* The insert request processing is to generate a vcard string,
* insert it into the cloud table,and update the uuid and dirty of the raw-cloud table.
* The update request is processed as follows: regenerate the vcard string,
* update the data of the cloud table, and update the raw-cloud table dirty
*/
public async contactsDataChange(context: common.Context): Promise<void> {
LogUtils.i(TAG, 'contactsDataChange enter');
// 修改数据上云前,需要检查空uniqueKey,空的重新生成
await HandleUniqueKeyService.getInstance().waitForCalcUniqueKeyEnd(this.context!);
}
public async getCallLogDataAbilityHelper(context?: common.Context) {
// 由于ArkTS类型推断,此处在Promise中调用callback获取dataShareHelper实例
if (this.callLogDataShareHelper === undefined) {
this.callLogDataShareHelper = await new Promise<dataShare.DataShareHelper>((resolve) => {
dataShare.createDataShareHelper(context || this.context!, CallLogSingleConstant.CALL_LOG_URI,
(err, callLogDataShareHelper) => {
resolve(callLogDataShareHelper);
});
})
}
return this.callLogDataShareHelper;
}
public async next2nextDeleteContactFormId(formId: string) {
const fromIdArr = formId.split('//');
for (const oldFromId of fromIdArr) {
const numberMatch = oldFromId.match(/\d+/);
if (numberMatch && !this.formIdSet.has(numberMatch[0])) {
await this.deleteContactFormId(this.extractNumberBeforeAsterisk(numberMatch[0]));
this.formIdSet.add(numberMatch[0])
}
}
}
// 检查文件比例
async checkFileScale(filePath: string, tag?: string) {
const tagStr = TAG + ` bigPhotoFromFile checkFileScale tag: ${tag} `;
// 默认使用海报显示大头像
let blobSource = 3;
let imageInfo: image.ImageInfo | null = null;
try {
// 根据filePath获取
imageInfo = await this.getImageInfoFromFile(filePath);
if (imageInfo) {
let height = imageInfo.size.height;
let width = imageInfo.size.width;
if (height * width <= MIN_RESOLUTION) {
blobSource = 1;
}
}
return blobSource;
} catch (e) {
LogUtils.e(tagStr, `err,msg:${e.message}`);
return blobSource;
}
}
async getImageInfoFromFile(filePath: string): Promise<image.ImageInfo | null> {
let imageSource: image.ImageSource | null = null;
let imageInfo: image.ImageInfo | null = null;
try {
imageSource = image.createImageSource(filePath);
if (imageSource !== undefined) {
imageInfo = await imageSource.getImageInfo();
} else {
LogUtils.w(TAG, 'getImageInfoFromFile createImageSource filePath result empty');
}
} catch (error) {
LogUtils.e(TAG, `getImageInfoFromFile err,msg:${error.message}`);
} finally {
imageSource?.release().catch((error: BusinessError) => {
LogUtils.e(TAG, `getImageInfoFromFile release resource error: ${error?.message}`);
});
}
return imageInfo;
}
public async getDataAbilityHelper() {
// 由于ArkTS类型推断,此处在Promise中调用callback获取dataShareHelper实例
if (this.dataShareHelper === undefined) {
this.dataShareHelper = await new Promise<dataShare.DataShareHelper>((resolve) => {
dataShare.createDataShareHelper(this.context!, Contacts.CONTENT_URI, (err, dataShareHelper) => {
resolve(dataShareHelper);
});
})
}
return this.dataShareHelper;
}
public async closeDataAbilityHelper() {
await this.dataShareHelper?.close();
this.dataShareHelper = undefined;
}
public async closeCalllogDataAbilityHelper() {
await this.callLogDataShareHelper?.close();
this.callLogDataShareHelper = undefined;
}
public getName(contactInfo: ContactInfo): DisPlayNameResult {
let result: DisPlayNameResult = new DisPlayNameResult();
let displayName: string = '';
let updateFlag: number = 0;
if (contactInfo.displayName != undefined && contactInfo.displayName.length > 0) {
displayName = contactInfo.displayName;
updateFlag = 0;
} else if (contactInfo.nicknameList != null && contactInfo.nicknameList.length > 0) {
displayName = contactInfo.nicknameList[0]?.nickName as string;
updateFlag = 1;
} else if (contactInfo.company != null && contactInfo.company != '') {
displayName = contactInfo.company;
updateFlag = 1;
} else if (contactInfo.position != null && contactInfo.position != '') {
displayName = contactInfo.position;
updateFlag = 1;
} else if (contactInfo.phoneList != null && contactInfo.phoneList.length > 0) {
displayName = contactInfo.phoneList[0].phoneNumber as string;
updateFlag = 1;
} else if (contactInfo.emails != null && contactInfo.emails.length > 0) {
displayName = contactInfo.emails[0].email as string;
updateFlag = 1;
}
result.displayName = displayName;
result.updateFlag = updateFlag;
return result;
}
// 升级后触发一次全量智能合并
public async checkAggregationContact() {
if (this.checkingCursor) {
setTimeout(async () => {
LogUtils.w(TAG, 'checkAggregationContact: timeout retry.');
await this.checkAggregationContact();
}, TIMEOUT_AGGREGATION_ALL_ONCE);
}
if (!this.checkingCursor) {
await ContactAggregator.getInstance().aggregateAll(this.context!, true);
} else {
LogUtils.w(TAG, 'checkAggregationContact: is syncing, dont do aggregation.');
}
}
// 重复克隆删除卡片的formId
public async deleteContactFormId(formID: string) {
LogUtils.i(TAG, 'deleteContactFormId start!');
let ruleSet: relationalStore.ResultSet | null | undefined = null;
try {
const initDbSuc = await contactRdbHelper.initDB(this.context!);
if (!initDbSuc) {
throw new Error(`deleteContactFormId initDb fail`);
}
const beginTransSuc = await contactRdbHelper.beginTransaction();
if (!beginTransSuc) {
throw new Error(`deleteContactFormId beginTrans fail`);
}
const delPredicates = new rdb.RdbPredicates(DbConstants.CLONE_NEXT2NEXT_CONTACTS_TABLE_NAME);
delPredicates.contains('form_id', formID);
ruleSet = await contactRdbHelper.queryData(DbConstants.CLONE_NEXT2NEXT_CONTACTS_TABLE_NAME,
delPredicates, ['form_id', 'id']);
if (!ruleSet || !ruleSet.goToFirstRow()) {
LogUtils.e(TAG, 'deleteContactFormId getAllContactData not found.');
contactRdbHelper.rollBack();
return
}
do {
let originFormID = ruleSet?.getString(ruleSet.getColumnIndex('form_id'));
let delUpdateID = ruleSet?.getString(ruleSet.getColumnIndex('id'))
let parts: string[] | undefined = originFormID?.split('//');
let filteredParts: string[] | undefined = parts?.filter(part =>!part.startsWith(formID));
let result: string | undefined = filteredParts?.join('//');
LogUtils.i(TAG,
'deleteContactFormId originFormID: ' + originFormID + ' delUpdateID: ' + delUpdateID + ' result: ' + result);
let updateBucket: ValuesBucket = {
'form_id': result + ''
}
const delUpdatePredicates = new rdb.RdbPredicates(DbConstants.CLONE_NEXT2NEXT_CONTACTS_TABLE_NAME);
delUpdatePredicates.equalTo('id', delUpdateID);
let delRet = await contactRdbHelper.updateData(updateBucket, delUpdatePredicates);
LogUtils.i(TAG, 'deleteContactFormId delete success: ' + delRet);
if (delRet < 0) {
throw new Error(`deleteContactFormId fail, ret:${delRet}`);
}
} while (ruleSet.goToNextRow());
await contactRdbHelper.commit();
} catch (e) {
LogUtils.e(TAG, `deleteContactFormId fail err,msg:${e.message},stack:${e.stack}`);
await contactRdbHelper.rollBack();
} finally {
if (ruleSet && !ruleSet.isClosed) {
ruleSet.close();
}
}
}
public extractNumberBeforeAsterisk(input: string): string {
const asteriskIndex = input.indexOf('*');
if (asteriskIndex !== -1) {
return input.substring(0, asteriskIndex);
} else {
return input;
}
}
/**
* 本地恢复联系人
* @param localRestoreRawIdStr 恢复联系人的id
*/
public async localRestoreContact(localRestoreRawIdStr: string) {
const localRestoreRawIds = localRestoreRawIdStr?.split(',').map((item: string) => Number(item));
if (!localRestoreRawIds.length) {
LogUtils.e(TAG, `localRestoreContact err args:${localRestoreRawIdStr}`);
return;
}
const predicates = new rdb.RdbPredicates(DbConstants.NEW_RAW_CONTACT_TABLE_NAME);
predicates.in(RawContactsColumns.ID, localRestoreRawIds);
// 查询恢复联系人raw信息
const restoreRawDataList = await this.getRawContactArr(predicates);
await this.restoreContact(restoreRawDataList, true, -1);
}
private async getRawContactArr(predicates: rdb.RdbPredicates): Promise<MergeRawContact[]> {
const rawContactArr: MergeRawContact[] = [];
let ruleSet: relationalStore.ResultSet | null | undefined = null;
try {
ruleSet = await contactRdbHelper.queryData(DbConstants.NEW_RAW_CONTACT_TABLE_NAME, predicates,
MergeRawContact.columns);
if (!ruleSet || !ruleSet.goToFirstRow()) {
return rawContactArr;
}
do {
const mergeRawContact: MergeRawContact = MergeRawContact.fromRuleSet(ruleSet);
rawContactArr.push(mergeRawContact);
} while (ruleSet.goToNextRow());
} finally {
if (ruleSet && !ruleSet.isClosed) {
ruleSet.close();
}
}
return rawContactArr;
}
/**
* 恢复联系人:(1)应用执行从回收站恢复,c++测删除回收站表数据,后续处理执行到这里(2)云同步下行恢复联系人
* @param restoreRawDataList
* @param isLocalRestore 是否本地恢复联系人
* @param maxCursor
*/
public async restoreContact(restoreRawDataList: MergeRawContact[], isLocalRestore: boolean, maxCursor: number) {
const restoreRawIds: number[] = [];
const restoreContactIds: number[] = [];
const restoreContacts: MergeRawContact[] = [];
const displayNameList: string[] = [];
const logTag = isLocalRestore ? 'localRestoreContact' : 'cloudRestoreContact';
for (const rawContact of restoreRawDataList) {
// 否则进行恢复
restoreRawIds.push(rawContact.rawId);
restoreContactIds.push(rawContact.contactId);
restoreContacts.push(rawContact);
displayNameList.push(rawContact.displayName);
}
let restoreSuccessFlag = false;
try {
const beginTransSuc = await contactRdbHelper.beginTransaction();
if (!beginTransSuc) {
LogUtils.e(logTag, `beginTransaction fail, return`);
return;
}
if (restoreRawIds.length) {
// 更新raw表is_deleted字段
const predicates = new rdb.RdbPredicates(DbConstants.NEW_RAW_CONTACT_TABLE_NAME);
predicates.in(RawContactsColumns.ID, restoreRawIds);
const updateBucket: ValuesBucket = {};
updateBucket[RawContactsColumns.IS_DELETED] = 0;
const updateRet = await contactRdbHelper.updateData(updateBucket, predicates);
LogUtils.w(logTag, `update raw table is_deleted Ret: ${updateRet}, restoreRawIds:${restoreRawIds}`);
// 删除最近删除表中对应数据
// TODO 本地删除, c++侧也删除了delete raw contact表,可能c++侧删除了,js删除操作失败,联系人没了
const delPredicates = new rdb.RdbPredicates(DbConstants.NEW_DELETE_RAW_CONTACT_TABLE_NAME);
delPredicates.in(DeleteRawContact.RAW_CONTACT_ID, restoreRawIds);
const delRet = await contactRdbHelper.deleteData(delPredicates);
LogUtils.w(logTag, `del delete table Ret: ${delRet}, restoreRawIdList:${restoreRawIds}`);
}
// 恢复的联系人,更新时间戳信息
if (restoreContactIds.length > 0) {
const predicates = new rdb.RdbPredicates(DbConstants.NEW_CONTACTS_TABLE_NAME);
predicates.in(ContactsColumns.NEW_ID, restoreContactIds);
const updateBucket: ValuesBucket = {};
updateBucket[ContactsColumns.CONTACT_LAST_UPDATED_TIMESTAMP] = Date.now();
const updateRet = await contactRdbHelper.updateData(updateBucket, predicates);
LogUtils.w(logTag, `restoreUpdateTimestamp Ret: ${updateRet}, restoreNum:${restoreContactIds.length}`);
}
restoreSuccessFlag = true;
await contactRdbHelper.commit();
if (restoreContacts.length) {
await this.refreshCalllogWhenRestoreContacts(restoreContacts, isLocalRestore);
}
// 添加生日到日历
if (restoreRawIds.length) {
for (let index = 0; index < restoreRawIds.length; index++) {
await BirthdayManager.getInstance().handleSingleBirthday(restoreRawIds[index].toString(), true);
}
}
} catch (e) {
await contactRdbHelper.rollBack();
LogUtils.e(logTag, `catch err:${e.message},stack:${e.messstackage}`);
}
// 触发智能合并
if (displayNameList && displayNameList.length > 0) {
if (isLocalRestore) {
await ContactAggregator.getInstance().aggregateAll(this.context!, true, undefined, displayNameList);
}
}
}
public async refreshCalllogWhenRestoreContacts(restoreContacts: MergeRawContact[], needNotify: boolean) {
try {
for (const rawContact of restoreContacts) {
const predicates = new rdb.RdbPredicates(DbConstants.NEW_DATA_TABLE_NAME);
predicates.equalTo(DataColumns.RAW_CONTACT_ID, rawContact.rawId);
predicates.and();
predicates.equalTo(DataColumns.TYPE_ID, DbConstants.FOUNDATION_TYPE_ID_MAP.PHONE);
const extraInfo: ExtraInfo = {
deletePhoneList: [],
deleteFormatPhoneList: [],
addPhoneList: [],
addFormatPhoneList: [],
};
let ruleSet: relationalStore.ResultSet | null | undefined = null;
try {
ruleSet = await contactRdbHelper.queryData(DbConstants.NEW_DATA_TABLE_NAME, predicates,
[DataColumns.DETAIL_INFO, DataColumns.FORMAT_PHONE_NUMBER]);
if (!ruleSet || !ruleSet.goToFirstRow()) {
continue;
}
do {
const detailInfo = ruleSet.getString(0);
const formatPhoneNumber = ruleSet.getString(1);
if (formatPhoneNumber) {
extraInfo.addFormatPhoneList.push(formatPhoneNumber);
} else if (detailInfo) {
const formatPhoneNumber = getFormatNumber(detailInfo);
if (formatPhoneNumber) {
extraInfo.addFormatPhoneList.push(formatPhoneNumber);
} else {
extraInfo.addPhoneList.push(detailInfo);
}
}
} while (ruleSet.goToNextRow());
} finally {
if (ruleSet && !ruleSet.isClosed) {
ruleSet.close();
}
}
await this.refreshCallLog(rawContact.contactId, rawContact.displayName, extraInfo, false);
}
if (needNotify) {
const dataShareHelper: dataShare.DataShareHelper | undefined = await this.getDataAbilityHelper();
await dataShareHelper?.notifyChange(Data.CONTENT_URI);
const callLogDataShareHelper: dataShare.DataShareHelper | undefined = await this
.getCallLogDataAbilityHelper();
await callLogDataShareHelper?.notifyChange(CallLogSingleConstant.CALL_LOG_URI);
}
} catch (e) {
LogUtils.e(TAG, `refreshCalllogWhenRestoreContacts err:${e.message},stack:${e.messstackage}`);
}
}
/**
* 对于软删除的,
* 对于恢复的联系人
* 对于新增、更新的联系人
* 为啥不能通过变动的号码来定向刷新(用户可能有2个相同号码,删了一个还有一个)
*/
public async refreshCallLog(contactId: number, displayName: string,
extraInfo: ExtraInfo, isDelete: boolean, context?: common.Context) {
try {
await callLogRdbHelper.initDB(context ? context : this.context!);
if (isDelete) {
await callLogRdbHelper.updateCallLogWhenDeleteContact(contactId);
return;
}
if (extraInfo.deletePhoneList?.length) {
await callLogRdbHelper.updateCallLogWhenContactDeletePhone(contactId, displayName,
extraInfo.deletePhoneList, false);
}
if (extraInfo.deleteFormatPhoneList?.length) {
await callLogRdbHelper.updateCallLogWhenContactDeletePhone(contactId, displayName,
extraInfo.deleteFormatPhoneList, true);
}
// 新增电话号码,处理通话记录
if (extraInfo.addPhoneList?.length) {
await callLogRdbHelper.updateCallLogWhenContactAddPhone(contactId, displayName,
extraInfo.addPhoneList, false);
}
if (extraInfo.addFormatPhoneList?.length) {
await callLogRdbHelper.updateCallLogWhenContactAddPhone(contactId, displayName,
extraInfo.addFormatPhoneList, true);
}
} catch (e) {
LogUtils.e(TAG, `refreshCallLog err,msg:${e.message || e.code}`);
}
}
}