1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.PersistableBundle;
26 import android.telecom.PhoneAccount;
27 import android.telecom.PhoneAccountHandle;
28 import android.telecom.TelecomManager;
29 import android.telephony.CarrierConfigManager;
30 import android.telephony.Rlog;
31 import android.telephony.SmsMessage;
32 import android.text.TextUtils;
33 
34 import java.time.Instant;
35 import java.time.LocalDateTime;
36 import java.time.ZoneId;
37 import java.time.format.DateTimeFormatter;
38 import java.util.Arrays;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 import java.util.regex.PatternSyntaxException;
42 
43 /**
44  * The SMS filter for parsing SMS from carrier to notify users about the missed incoming call.
45  */
46 public class MissedIncomingCallSmsFilter {
47     private static final String TAG = MissedIncomingCallSmsFilter.class.getSimpleName();
48 
49     private static final boolean VDBG = false;    // STOPSHIP if true
50 
51     private static final String SMS_YEAR_TAG = "year";
52 
53     private static final String SMS_MONTH_TAG = "month";
54 
55     private static final String SMS_DAY_TAG = "day";
56 
57     private static final String SMS_HOUR_TAG = "hour";
58 
59     private static final String SMS_MINUTE_TAG = "minute";
60 
61     private static final String SMS_CALLER_ID_TAG = "callerId";
62 
63     private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT =
64             new ComponentName("com.android.phone",
65                     "com.android.services.telephony.TelephonyConnectionService");
66 
67     private final Phone mPhone;
68 
69     private PersistableBundle mCarrierConfig;
70 
71     /**
72      * Constructor
73      *
74      * @param phone The phone instance
75      */
MissedIncomingCallSmsFilter(Phone phone)76     public MissedIncomingCallSmsFilter(Phone phone) {
77         mPhone = phone;
78 
79         CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext()
80                 .getSystemService(Context.CARRIER_CONFIG_SERVICE);
81         if (configManager != null) {
82             mCarrierConfig = configManager.getConfigForSubId(mPhone.getSubId());
83         }
84     }
85 
86     /**
87      * Check if the message is missed incoming call SMS, which is sent from the carrier to notify
88      * the user about the missed incoming call earlier.
89      *
90      * @param pdus SMS pdu binary
91      * @param format Either {@link SmsConstants#FORMAT_3GPP} or {@link SmsConstants#FORMAT_3GPP2}
92      * @return {@code true} if this is an SMS for notifying the user about missed incoming call.
93      */
filter(byte[][] pdus, String format)94     public boolean filter(byte[][] pdus, String format) {
95         // The missed incoming call SMS must be one page only, and if not we should ignore it.
96         if (pdus.length != 1) {
97             return false;
98         }
99 
100         if (mCarrierConfig != null) {
101             String[] originators = mCarrierConfig.getStringArray(CarrierConfigManager
102                     .KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY);
103             if (originators != null) {
104                 SmsMessage message = SmsMessage.createFromPdu(pdus[0], format);
105                 if (message != null
106                         && !TextUtils.isEmpty(message.getOriginatingAddress())
107                         && Arrays.asList(originators).contains(message.getOriginatingAddress())) {
108                     return processSms(message);
109                 }
110             }
111         }
112         return false;
113     }
114 
115     /**
116      * Get the Epoch time.
117      *
118      * @param year Year in string format. If this param is null or empty, a guessed year will be
119      * used. Some carriers do not provide this information in the SMS.
120      * @param month Month in string format.
121      * @param day Day in string format.
122      * @param hour Hour in string format.
123      * @param minute Minute in string format.
124      * @return The Epoch time in milliseconds.
125      */
getEpochTime(String year, String month, String day, String hour, String minute)126     private long getEpochTime(String year, String month, String day, String hour, String minute) {
127         LocalDateTime now = LocalDateTime.now();
128         if (TextUtils.isEmpty(year)) {
129             // If year is not provided, guess the year from current time.
130             year = Integer.toString(now.getYear());
131         }
132 
133         LocalDateTime time;
134         // Check if the guessed year is reasonable. If it's the future, then the year must be
135         // the previous year. For example, the missed call's month and day is 12/31, but current
136         // date is 1/1/2020, then the year of missed call must be 2019.
137         do {
138             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
139             time = LocalDateTime.parse(year + month + day + hour + minute, formatter);
140             year = Integer.toString(Integer.parseInt(year) - 1);
141         } while (time.isAfter(now));
142 
143         Instant instant = time.atZone(ZoneId.systemDefault()).toInstant();
144         return instant.toEpochMilli();
145     }
146 
147     /**
148      * Process the SMS message
149      *
150      * @param message SMS message
151      *
152      * @return {@code true} if the SMS message has been processed as a missed incoming call SMS.
153      */
processSms(@onNull SmsMessage message)154     private boolean processSms(@NonNull SmsMessage message) {
155         long missedCallTime = 0;
156         String callerId = null;
157 
158         String[] smsPatterns = mCarrierConfig.getStringArray(CarrierConfigManager
159                 .KEY_MISSED_INCOMING_CALL_SMS_PATTERN_STRING_ARRAY);
160         if (smsPatterns == null || smsPatterns.length == 0) {
161             Rlog.w(TAG, "Missed incoming call SMS pattern is not configured!");
162             return false;
163         }
164 
165         for (String smsPattern : smsPatterns) {
166             Pattern pattern;
167             try {
168                 pattern = Pattern.compile(smsPattern, Pattern.DOTALL | Pattern.UNIX_LINES);
169             } catch (PatternSyntaxException e) {
170                 Rlog.w(TAG, "Configuration error. Unexpected missed incoming call sms "
171                         + "pattern: " + smsPattern + ", e=" + e);
172                 continue;
173             }
174 
175             Matcher matcher = pattern.matcher(message.getMessageBody());
176             String year = null, month = null, day = null, hour = null, minute = null;
177             if (matcher.find()) {
178                 try {
179                     month = matcher.group(SMS_MONTH_TAG);
180                     day = matcher.group(SMS_DAY_TAG);
181                     hour = matcher.group(SMS_HOUR_TAG);
182                     minute = matcher.group(SMS_MINUTE_TAG);
183                     if (VDBG) {
184                         Rlog.v(TAG, "month=" + month + ", day=" + day + ", hour=" + hour
185                                 + ", minute=" + minute);
186                     }
187                 } catch (IllegalArgumentException e) {
188                     if (VDBG) {
189                         Rlog.v(TAG, "One of the critical date field is missing. Using the "
190                                 + "current time for missed incoming call.");
191                     }
192                     missedCallTime = System.currentTimeMillis();
193                 }
194 
195                 // Year is an optional field.
196                 try {
197                     year = matcher.group(SMS_YEAR_TAG);
198                 } catch (IllegalArgumentException e) {
199                     if (VDBG) Rlog.v(TAG, "Year is missing.");
200                 }
201 
202                 try {
203                     if (missedCallTime == 0) {
204                         missedCallTime = getEpochTime(year, month, day, hour, minute);
205                         if (missedCallTime == 0) {
206                             Rlog.e(TAG, "Can't get the time. Use the current time.");
207                             missedCallTime = System.currentTimeMillis();
208                         }
209                     }
210 
211                     if (VDBG) Rlog.v(TAG, "missedCallTime=" + missedCallTime);
212                 } catch (Exception e) {
213                     Rlog.e(TAG, "Can't get the time for missed incoming call");
214                 }
215 
216                 try {
217                     callerId = matcher.group(SMS_CALLER_ID_TAG);
218                     if (VDBG) Rlog.v(TAG, "caller id=" + callerId);
219                 } catch (IllegalArgumentException e) {
220                     Rlog.d(TAG, "Caller id is not provided or can't be parsed.");
221                 }
222                 createMissedIncomingCallEvent(missedCallTime, callerId);
223                 return true;
224             }
225         }
226 
227         Rlog.d(TAG, "SMS did not match any missed incoming call SMS pattern.");
228         return false;
229     }
230 
231     // Create phone account. The logic is copied from PhoneUtils.makePstnPhoneAccountHandle.
makePstnPhoneAccountHandle(Phone phone)232     private static PhoneAccountHandle makePstnPhoneAccountHandle(Phone phone) {
233         return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
234                 String.valueOf(phone.getFullIccSerialNumber()));
235     }
236 
237     /**
238      * Create the missed incoming call through TelecomManager.
239      *
240      * @param missedCallTime the time of missed incoming call in. This is the EPOCH time in
241      * milliseconds.
242      * @param callerId The caller id of the missed incoming call.
243      */
createMissedIncomingCallEvent(long missedCallTime, @Nullable String callerId)244     private void createMissedIncomingCallEvent(long missedCallTime, @Nullable String callerId) {
245         TelecomManager tm = (TelecomManager) mPhone.getContext()
246                 .getSystemService(Context.TELECOM_SERVICE);
247 
248         if (tm != null) {
249             Bundle bundle = new Bundle();
250 
251             if (callerId != null) {
252                 final Uri phoneUri = Uri.fromParts(
253                         PhoneAccount.SCHEME_TEL, callerId, null);
254                 bundle.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, phoneUri);
255             }
256 
257             // Need to use the Epoch time instead of the elapsed time because it's possible
258             // the missed incoming call occurred before the phone boots up.
259             bundle.putLong(TelecomManager.EXTRA_CALL_CREATED_EPOCH_TIME_MILLIS, missedCallTime);
260             tm.addNewIncomingCall(makePstnPhoneAccountHandle(mPhone), bundle);
261         }
262     }
263 }
264