1/**
2 * Copyright (c) 2022 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 ICallLogService from './ICallLogService';
17import { CallLog } from './entity/CallLog';
18import { CallType } from './entity/CallLog';
19import { ArrayUtil } from '../../../../../common/src/main/ets/util/ArrayUtil';
20import MergedCallLog from './entity/MergedCallLog';
21import { StringUtil } from '../../../../../common/src/main/ets/util/StringUtil';
22import { MergeRule } from './CallLogSetting';
23import StringFormatUtil from '../../../../../entry/src/main/ets/util/StringFormatUtil';
24
25export class CallLogService implements ICallLogService {
26  private static instance: CallLogService;
27  private mergeRule: MergeRule;
28  private context: Context;
29
30  private constructor() {
31  }
32
33  /*
34   * init if Call From serviceAbility globalThis.context is Null
35   *@param ctx Context used for dataShare
36   */
37  init(ctx: Context) {
38    this.context = ctx;
39  }
40
41  public static getInstance(): CallLogService {
42    if (!CallLogService.instance) {
43      CallLogService.instance = new CallLogService();
44    }
45    return CallLogService.instance;
46  }
47
48  setMergeRule(mergeRule: MergeRule) {
49    this.mergeRule = mergeRule;
50  }
51
52  mergeCallLogs(callLogs: CallLog[]) {
53    if (this.getMergeRule() === MergeRule.CONTACT) {
54      return this.mergeByContact(callLogs);
55    } else {
56      return this.mergeByTime(callLogs);
57    }
58  }
59
60  mergeMissedCalls(callLogs: CallLog[]) {
61    let mergeRule = this.getMergeRule();
62    let callLogList: CallLog[] = [];
63    let missedList: CallLog[] = [];
64    for (let callLog of callLogs) {
65      callLogList.push(callLog);
66      if (callLog.callType == CallType.MISSED ||
67      callLog.callType == CallType.REJECTED) {
68        //Filtering Missed Call Data
69        missedList.push(callLog);
70        let timeList = [];
71        // Filtering by Contact Missed Calls and Redialing
72        if (mergeRule === MergeRule.CONTACT) {
73          for (let k = 0; k < missedList.length; k++) {
74            let missedPhone = missedList[k].phoneNumber;
75            for (let i = 0; i < callLogList.length; i++) {
76              let allSpecialPhone = callLogList[i].phoneNumber;
77              if (missedPhone == allSpecialPhone) {
78                let timeNumber = callLogList[i].createTime;
79                let obj = {
80                  'id': i,
81                  'timeObj': timeNumber,
82                };
83                timeList.push(obj);
84                let max = timeList[0].timeObj;
85                for (let j = 0; j < timeList.length; j++) {
86                  if (timeList[j].timeObj > max) {
87                    max = timeList[j].timeObj;
88                    let n = timeList[j].id;
89                    if (callLogList[n].callType == CallType.OUT) {
90                      missedList.splice(k, 1);
91                    }
92                  } else {
93                    let m = timeList[0].id;
94                    if (callLogList[m].callType == CallType.OUT) {
95                      missedList.splice(k, 1);
96                    }
97                  }
98                }
99              }
100            }
101          }
102        }
103      }
104    }
105    if (mergeRule === MergeRule.CONTACT) {
106      return this.mergeByContact(missedList);
107    } else {
108      return this.mergeByTime(missedList);
109    }
110  }
111
112  /**
113   * In the case of merging by time, the post-processing of the call record
114   * service data is optimized based on the original call record data.
115   * @param callLogList
116   * @return
117   */
118  private mergeByTime(callLogList: CallLog[]) {
119    let resultList = [];
120    if (ArrayUtil.isEmpty(callLogList)) {
121      return resultList;
122    }
123    // Call records are cached from the first record.
124    let tempElement = new MergedCallLog(callLogList[0]);
125    // Indicates the creation time of the latest record.
126    // After call records are merged, the time is displayed.
127    let tempCallTime = callLogList[0].createTime;
128    // Type of the call record that retains the latest record.
129    // This type is displayed after the call record is combined.
130    let tempCallType = callLogList[0].callType;
131    let num = 1;
132    let ids = [];
133    ids.push(callLogList[0].id);
134    for (let i = 1; i < callLogList.length; i++) {
135      let element = callLogList[i];
136      // Whether the cached field needs to be combined with the current field
137      if (this.callLogMergeCheck(tempElement, element)) {
138        num++;
139        // Put the latest record ID into the merged array.
140        ids.push(element.id);
141      } else {
142        //If the latest data is inconsistent with the cached data,
143        // replace the num and ids data in the cached data and
144        // save the cached data to the result set.
145        tempElement.count = num;
146        tempElement.ids = ids;
147        // Displays the creation time of the latest saved record.
148        tempElement.createTime = this.formatTime(tempCallTime);
149        tempElement.callType = tempCallType;
150        resultList.push(tempElement);
151        /* Reset num and ids to the latest count and record,
152        and reset tempCallTime to the latest creation time of the next record.*/
153        num = 1;
154        ids = [];
155        tempCallTime = element.createTime;
156        tempCallType = element.callType;
157        ids.push(element.id);
158      }
159      tempElement = new MergedCallLog(element);
160    }
161    /* Put the last piece of cached data into the result set*/
162    if (tempElement != null) {
163      tempElement.count = num;
164      tempElement.ids = ids;
165      tempElement.createTime = this.formatTime(tempCallTime);
166      tempElement.callType = tempCallType;
167      resultList.push(tempElement);
168    }
169    return resultList;
170  }
171
172  /**
173   * In the case of merging by contact, the post-processing
174   * of the call record service data is optimized based on the original call record data.
175   *
176   * @param {Array} callLogList
177   * @return {Array} callLogList
178   */
179  private mergeByContact(callLogs: CallLog[]) {
180    let resultList = [];
181    if (ArrayUtil.isEmpty(callLogs)) {
182      return resultList;
183    }
184    let contactTempMap = new Map();
185    let phoneNumberMap = new Map();
186    for (let i = 0; i < callLogs.length; i++) {
187      let element = new MergedCallLog(callLogs[i]);
188      element.createTime = this.formatTime(callLogs[i].createTime);
189      // In the case of merging by contact, the combined record entry is fixed to 1.
190      element.count = 1;
191      // In the case of merging by contact, the IDs of
192      // the merging record are fixed to the ID of the record.
193      element.ids = [callLogs[i].id];
194      // Call records without contacts are combined by phone number.
195      if (StringUtil.isEmpty(element.quickSearchKey)) {
196        if (!phoneNumberMap.has(element.phoneNumber)) {
197          resultList.push(element);
198          phoneNumberMap.set(element.phoneNumber, callLogs[i].phoneNumber);
199        }
200      } else { // Call records with contacts are merged by contact.
201        let isContactKey = contactTempMap.has(element.quickSearchKey);
202        if (!isContactKey) {
203          resultList.push(element);
204          contactTempMap.set(element.quickSearchKey, callLogs[i].quickSearchKey);
205        }
206      }
207    }
208    return resultList;
209  }
210
211  /**
212   * Obtain the call time.
213   *
214   * @param date Call record creation timestamp
215   * @return {object} Talk time
216   */
217  private formatTime(date) {
218    let result;
219    // If the value is not a number, the value is not parsed.
220    if (isNaN(date)) {
221      return date;
222    }
223    let timestamp = parseInt(date) * 1000;
224    let callTime = new Date(timestamp);
225    let now = new Date();
226    if (callTime.getTime() > now.getTime()) {
227      result = callTime.getFullYear() + '/' + (callTime.getMonth() + 1) + '/' + callTime.getDate();
228    } else if (callTime.getFullYear() == now.getFullYear()) {
229      if (callTime.getMonth() == now.getMonth()) {
230        // Same month of the same year
231        let timeDiff = parseInt(((now.getTime() - callTime.getTime()) / 60000).toString());
232        let dayDiff = now.getDate() - callTime.getDate();
233        let hour = callTime.getHours().toString();
234        let minutes = callTime.getMinutes() < 10 ? '0' + callTime.getMinutes() : callTime.getMinutes().toString();
235        if (dayDiff == 0) {
236          // 同天
237          if (timeDiff == 0) {
238            result = $r("app.string.justNow");
239          } else if (timeDiff < 60) {
240            result = $r("app.string.minutesAgo", timeDiff);
241          } else {
242            let timeDetail: any = {};
243            if (parseInt(StringFormatUtil.judgeSysTime(this.context)) == 12) {
244              timeDetail.time = StringFormatUtil.getDayMessage(hour, minutes);
245            } else {
246              timeDetail.time = $r("app.string.time_normal", hour, minutes);
247            }
248            result = timeDetail.time
249          }
250        } else if (dayDiff == 1) {
251          result = $r("app.string.yesterday");
252        } else {
253          result = (callTime.getMonth() + 1) + '/' + callTime.getDate(); // 'MM/dd'
254        }
255      } else {
256        result = (callTime.getMonth() + 1) + '/' + callTime.getDate();
257      }
258    } else {
259      // 'yyyy/MM/dd'
260      result = callTime.getFullYear() + '/' + (callTime.getMonth() + 1) + '/' + callTime.getDate();
261    }
262    return result;
263  }
264
265  /**
266   * Checks whether two call records need to be combined when the call records are combined by time.
267   * If the call records need to be combined, true is returned. Otherwise, false is returned.
268   *
269   * @param oldElement Call records before merging
270   * @param newElement Combined call records
271   * @return
272   */
273  private callLogMergeCheck(oldElement: MergedCallLog, newElement: CallLog) {
274    /* Merge Rules:
275       1. The phone numbers are combined only when the phone numbers are the same.
276        2. If the number is the same and the call type is 1, 2, 3, or 5,
277        the call is combined. Types 1, 2, 3, and 5 are not combined.
278    */
279    if (oldElement.phoneNumber.trim() == newElement.phoneNumber.trim()) {
280      if (oldElement.callType == CallType.IN || oldElement.callType == CallType.OUT) {
281        if (newElement.callType == CallType.IN || newElement.callType == CallType.OUT) {
282          return true;
283        }
284        return false;
285      }
286      if (newElement.callType == CallType.MISSED || newElement.callType == CallType.REJECTED) {
287        return true;
288      }
289    }
290    return false;
291  }
292
293  private getMergeRule() {
294    return this.mergeRule;
295  }
296}