• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.mms.service;
18 
19 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE;
20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_SEND_REQ;
21 
22 import android.annotation.Nullable;
23 import android.app.PendingIntent;
24 import android.app.Service;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.SharedPreferences;
31 import android.database.sqlite.SQLiteException;
32 import android.net.Uri;
33 import android.os.Binder;
34 import android.os.Bundle;
35 import android.os.IBinder;
36 import android.os.ParcelFileDescriptor;
37 import android.os.Process;
38 import android.os.RemoteException;
39 import android.os.UserHandle;
40 import android.provider.Settings;
41 import android.provider.Telephony;
42 import android.security.NetworkSecurityPolicy;
43 import android.service.carrier.CarrierMessagingService;
44 import android.telephony.SmsManager;
45 import android.telephony.SubscriptionInfo;
46 import android.telephony.SubscriptionManager;
47 import android.telephony.TelephonyManager;
48 import android.telephony.data.ApnSetting;
49 import android.text.TextUtils;
50 import android.util.EventLog;
51 import android.util.SparseArray;
52 
53 import com.android.internal.telephony.IMms;
54 import com.android.mms.service.metrics.MmsMetricsCollector;
55 import com.android.mms.service.metrics.MmsStats;
56 
57 import com.google.android.mms.MmsException;
58 import com.google.android.mms.pdu.DeliveryInd;
59 import com.google.android.mms.pdu.GenericPdu;
60 import com.google.android.mms.pdu.NotificationInd;
61 import com.google.android.mms.pdu.PduParser;
62 import com.google.android.mms.pdu.PduPersister;
63 import com.google.android.mms.pdu.ReadOrigInd;
64 import com.google.android.mms.pdu.RetrieveConf;
65 import com.google.android.mms.pdu.SendReq;
66 import com.google.android.mms.util.SqliteWrapper;
67 
68 import java.io.IOException;
69 import java.util.ArrayDeque;
70 import java.util.ArrayList;
71 import java.util.Arrays;
72 import java.util.Collections;
73 import java.util.Comparator;
74 import java.util.List;
75 import java.util.Queue;
76 import java.util.concurrent.Callable;
77 import java.util.concurrent.ExecutorService;
78 import java.util.concurrent.Executors;
79 import java.util.concurrent.Future;
80 import java.util.concurrent.TimeUnit;
81 import java.util.stream.Collectors;
82 import java.util.stream.Stream;
83 
84 /**
85  * System service to process MMS API requests
86  */
87 public class MmsService extends Service implements MmsRequest.RequestManager {
88     public static final int QUEUE_INDEX_SEND = 0;
89     public static final int QUEUE_INDEX_DOWNLOAD = 1;
90 
91     private static final String SHARED_PREFERENCES_NAME = "mmspref";
92     private static final String PREF_AUTO_PERSISTING = "autopersisting";
93 
94     // Maximum time to spend waiting to read data from a content provider before failing with error.
95     private static final int TASK_TIMEOUT_MS = 30 * 1000;
96     // Maximum size of MMS service supports - used on occassions when MMS messages are processed
97     // in a carrier independent manner (for example for imports and drafts) and the carrier
98     // specific size limit should not be used (as it could be lower on some carriers).
99     private static final int MAX_MMS_FILE_SIZE = 8 * 1024 * 1024;
100 
101     // The default number of threads allowed to run MMS requests in each queue
102     public static final int THREAD_POOL_SIZE = 4;
103 
104     /** Represents the received SMS message for importing. */
105     public static final int SMS_TYPE_INCOMING = 0;
106     /** Represents the sent SMS message for importing. */
107     public static final int SMS_TYPE_OUTGOING = 1;
108     /** Message status property: whether the message has been seen. */
109     public static final String MESSAGE_STATUS_SEEN = "seen";
110     /** Message status property: whether the message has been read. */
111     public static final String MESSAGE_STATUS_READ = "read";
112 
113     // Pending requests that are waiting for the SIM to be available
114     // If a different SIM is currently used by previous requests, the following
115     // requests will stay in this queue until that SIM finishes its current requests in
116     // RequestQueue.
117     // Requests are not reordered. So, e.g. if current SIM is SIM1, a request for SIM2 will be
118     // blocked in the queue. And a later request for SIM1 will be appended to the queue, ordered
119     // after the request for SIM2, instead of being put into the running queue.
120     // TODO: persist this in case MmsService crashes
121     private final Queue<MmsRequest> mPendingSimRequestQueue = new ArrayDeque<>();
122 
123     // Thread pool for transferring PDU with MMS apps
124     private final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool();
125 
126     // A cache of MmsNetworkManager for SIMs
127     private final SparseArray<MmsNetworkManager> mNetworkManagerCache = new SparseArray<>();
128 
129     // The default TelephonyManager and a cache of TelephonyManagers for individual subscriptions
130     private TelephonyManager mDefaultTelephonyManager;
131     private final SparseArray<TelephonyManager> mTelephonyManagerCache = new SparseArray<>();
132 
133     // The current SIM ID for the running requests. Only one SIM can send/download MMS at a time.
134     private int mCurrentSubId;
135     // The current running MmsRequest count.
136     private int mRunningRequestCount;
137 
138     // Running request queues, one thread pool per queue
139     // 0: send queue
140     // 1: download queue
141     private final ExecutorService[] mRunningRequestExecutors = new ExecutorService[2];
142 
143     private static MmsMetricsCollector mMmsMetricsCollector;
144 
getNetworkManager(int subId)145     private MmsNetworkManager getNetworkManager(int subId) {
146         synchronized (mNetworkManagerCache) {
147             MmsNetworkManager manager = mNetworkManagerCache.get(subId);
148             if (manager == null) {
149                 manager = new MmsNetworkManager(this, subId);
150                 mNetworkManagerCache.put(subId, manager);
151             }
152             return manager;
153         }
154     }
155 
getTelephonyManager(int subId)156     private TelephonyManager getTelephonyManager(int subId) {
157         synchronized (mTelephonyManagerCache) {
158             if (mDefaultTelephonyManager == null) {
159                 mDefaultTelephonyManager = (TelephonyManager) this.getSystemService(
160                         Context.TELEPHONY_SERVICE);
161             }
162 
163             TelephonyManager subSpecificTelephonyManager = mTelephonyManagerCache.get(subId);
164             if (subSpecificTelephonyManager == null) {
165                 subSpecificTelephonyManager = mDefaultTelephonyManager.createForSubscriptionId(
166                         subId);
167                 mTelephonyManagerCache.put(subId, subSpecificTelephonyManager);
168             }
169             return subSpecificTelephonyManager;
170         }
171     }
172 
enforceSystemUid()173     private void enforceSystemUid() {
174         if (Binder.getCallingUid() != Process.SYSTEM_UID) {
175             throw new SecurityException("Only system can call this service");
176         }
177     }
178 
179     @Nullable
getCarrierMessagingServicePackageIfExists(int subId)180     private String getCarrierMessagingServicePackageIfExists(int subId) {
181         Intent intent = new Intent(CarrierMessagingService.SERVICE_INTERFACE);
182         TelephonyManager telephonyManager = getTelephonyManager(subId);
183         List<String> carrierPackages = telephonyManager.getCarrierPackageNamesForIntent(intent);
184 
185         if (carrierPackages == null || carrierPackages.size() != 1) {
186             return null;
187         } else {
188             return carrierPackages.get(0);
189         }
190     }
191 
loadMmsConfig(int subId)192     private Bundle loadMmsConfig(int subId) {
193         final Bundle config = MmsConfigManager.getInstance().getMmsConfigBySubId(subId);
194         if (config != null) {
195             // TODO: Make MmsConfigManager authoritative for user agent and don't consult
196             // TelephonyManager.
197             final TelephonyManager telephonyManager = getTelephonyManager(subId);
198             final String userAgent = telephonyManager.getMmsUserAgent();
199             if (!TextUtils.isEmpty(userAgent)) {
200                 config.putString(SmsManager.MMS_CONFIG_USER_AGENT, userAgent);
201             }
202             final String userAgentProfileUrl = telephonyManager.getMmsUAProfUrl();
203             if (!TextUtils.isEmpty(userAgentProfileUrl)) {
204                 config.putString(SmsManager.MMS_CONFIG_UA_PROF_URL, userAgentProfileUrl);
205             }
206         }
207         return config;
208     }
209 
210     private IMms.Stub mStub = new IMms.Stub() {
211         @Override
212         public void sendMessage(int subId, String callingPkg, Uri contentUri,
213                 String locationUrl, Bundle configOverrides, PendingIntent sentIntent,
214                 long messageId, String attributionTag) {
215             LogUtil.d("sendMessage " + formatCrossStackMessageId(messageId));
216             enforceSystemUid();
217 
218             MmsStats mmsStats = new MmsStats(MmsService.this,
219                     mMmsMetricsCollector.getAtomsStorage(), subId, getTelephonyManager(subId),
220                     callingPkg, false);
221 
222             // Make sure the subId is correct
223             if (!SubscriptionManager.isValidSubscriptionId(subId)) {
224                 LogUtil.e("Invalid subId " + subId);
225                 handleError(sentIntent, SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID, mmsStats);
226                 return;
227             }
228             if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
229                 subId = SubscriptionManager.getDefaultSmsSubscriptionId();
230                 mmsStats.updateSubId(subId, getTelephonyManager(subId));
231             }
232 
233             // Make sure the subId is active
234             if (!isActiveSubId(subId)) {
235                 handleError(sentIntent, SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION, mmsStats);
236                 return;
237             }
238 
239             // Load MMS config
240             Bundle mmsConfig = loadMmsConfig(subId);
241             if (mmsConfig == null) {
242                 LogUtil.e("MMS config is not loaded yet for subId " + subId);
243                 handleError(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
244                 return;
245             }
246 
247             // Apply overrides
248             if (configOverrides != null) {
249                 mmsConfig.putAll(configOverrides);
250             }
251 
252             // Make sure MMS is enabled
253             if (!mmsConfig.getBoolean(SmsManager.MMS_CONFIG_MMS_ENABLED)) {
254                 LogUtil.e("MMS is not enabled for subId " + subId);
255                 handleError(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
256                 return;
257             }
258 
259             final SendRequest request = new SendRequest(MmsService.this, subId, contentUri,
260                     locationUrl, sentIntent, callingPkg, mmsConfig, MmsService.this,
261                     messageId, mmsStats);
262 
263             final String carrierMessagingServicePackage =
264                     getCarrierMessagingServicePackageIfExists(subId);
265 
266             if (carrierMessagingServicePackage != null) {
267                 LogUtil.d(request.toString(), "sending message by carrier app "
268                         + formatCrossStackMessageId(messageId));
269                 request.trySendingByCarrierApp(MmsService.this, carrierMessagingServicePackage);
270                 return;
271             }
272 
273             // Make sure subId has MMS data. We intentionally do this after attempting to send via a
274             // carrier messaging service as the carrier messaging service may want to handle this in
275             // a different way and may not be restricted by whether data is enabled for an APN on a
276             // given subscription.
277             if (!getTelephonyManager(subId).isDataEnabledForApn(ApnSetting.TYPE_MMS)) {
278                 // ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS is set for only SendReq case, since
279                 // AcknowledgeInd and NotifyRespInd are parts of downloading sequence.
280                 // TODO: Should consider ReadRecInd(Read Report)?
281                 sendSettingsIntentForFailedMms(!isRawPduSendReq(contentUri), subId);
282                 handleError(sentIntent, SmsManager.MMS_ERROR_NO_DATA_NETWORK, mmsStats);
283                 return;
284             }
285 
286             addSimRequest(request);
287         }
288 
289         @Override
290         public void downloadMessage(int subId, String callingPkg, String locationUrl,
291                 Uri contentUri, Bundle configOverrides, PendingIntent downloadedIntent,
292                 long messageId, String attributionTag) {
293             // If the subId is no longer active it could be caused by an MVNO using multiple
294             // subIds, so we should try to download anyway.
295             // TODO: Fail fast when downloading will fail (i.e. SIM swapped)
296             LogUtil.d("downloadMessage: " + MmsHttpClient.redactUrlForNonVerbose(locationUrl) +
297                     ", " + formatCrossStackMessageId(messageId));
298 
299             enforceSystemUid();
300 
301             MmsStats mmsStats = new MmsStats(MmsService.this,
302                     mMmsMetricsCollector.getAtomsStorage(), subId, getTelephonyManager(subId),
303                     callingPkg, true);
304 
305             // Make sure the subId is correct
306             if (!SubscriptionManager.isValidSubscriptionId(subId)) {
307                 LogUtil.e("Invalid subId " + subId);
308                 handleError(downloadedIntent, SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID,
309                         mmsStats);
310                 return;
311             }
312             if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
313                 subId = SubscriptionManager.getDefaultSmsSubscriptionId();
314                 mmsStats.updateSubId(subId, getTelephonyManager(subId));
315             }
316 
317             if (!isActiveSubId(subId)) {
318                 List<SubscriptionInfo> activeSubList = getActiveSubscriptionsInGroup(subId);
319                 if (activeSubList.isEmpty()) {
320                     handleError(downloadedIntent, SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION,
321                             mmsStats);
322                     return;
323                 }
324 
325                 subId = activeSubList.get(0).getSubscriptionId();
326                 int defaultSmsSubId = SubscriptionManager.getDefaultSmsSubscriptionId();
327                 // If we have default sms subscription, prefer to use that. Otherwise, use first
328                 // subscription
329                 for (SubscriptionInfo subInfo : activeSubList) {
330                     if (subInfo.getSubscriptionId() == defaultSmsSubId) {
331                         subId = subInfo.getSubscriptionId();
332                     }
333                 }
334             }
335             mmsStats.updateSubId(subId, getTelephonyManager(subId));
336 
337             // Load MMS config
338             Bundle mmsConfig = loadMmsConfig(subId);
339             if (mmsConfig == null) {
340                 LogUtil.e("MMS config is not loaded yet for subId " + subId);
341                 handleError(downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
342                 return;
343             }
344 
345             // Apply overrides
346             if (configOverrides != null) {
347                 mmsConfig.putAll(configOverrides);
348             }
349 
350             // Make sure MMS is enabled
351             if (!mmsConfig.getBoolean(SmsManager.MMS_CONFIG_MMS_ENABLED)) {
352                 LogUtil.e("MMS is not enabled for subId " + subId);
353                 handleError(downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
354                 return;
355             }
356 
357             final DownloadRequest request = new DownloadRequest(MmsService.this, subId, locationUrl,
358                     contentUri, downloadedIntent, callingPkg, mmsConfig, MmsService.this,
359                     messageId, mmsStats);
360 
361             final String carrierMessagingServicePackage =
362                     getCarrierMessagingServicePackageIfExists(subId);
363 
364             if (carrierMessagingServicePackage != null) {
365                 LogUtil.d(request.toString(), "downloading message by carrier app "
366                         + formatCrossStackMessageId(messageId));
367                 request.tryDownloadingByCarrierApp(MmsService.this, carrierMessagingServicePackage);
368                 return;
369             }
370 
371             // Make sure subId has MMS data
372             if (!getTelephonyManager(subId).isDataEnabledForApn(ApnSetting.TYPE_MMS)) {
373                 sendSettingsIntentForFailedMms(/*isIncoming=*/ true, subId);
374                 handleError(downloadedIntent, SmsManager.MMS_ERROR_DATA_DISABLED, mmsStats);
375                 return;
376             }
377 
378             addSimRequest(request);
379         }
380 
381         private List<SubscriptionInfo> getActiveSubscriptionsInGroup(int subId) {
382             SubscriptionManager subManager =
383                     (SubscriptionManager) getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
384 
385             if (subManager == null) {
386                 return Collections.emptyList();
387             }
388 
389             List<SubscriptionInfo> subList = subManager.getAvailableSubscriptionInfoList();
390 
391             if (subList == null) {
392                 return Collections.emptyList();
393             }
394 
395             SubscriptionInfo subscriptionInfo = null;
396             for (SubscriptionInfo subInfo : subList) {
397                 if (subInfo.getSubscriptionId() == subId) {
398                     subscriptionInfo = subInfo;
399                     break;
400                 }
401             }
402 
403             if (subscriptionInfo == null) {
404                 return Collections.emptyList();
405             }
406 
407             if (subscriptionInfo.getGroupUuid() == null) {
408                 return Collections.emptyList();
409             }
410 
411             List<SubscriptionInfo> subscriptionInGroupList =
412                     subManager.getSubscriptionsInGroup(subscriptionInfo.getGroupUuid());
413 
414             // the list is sorted by isOpportunistic and isOpportunistic == false will have higher
415             // priority
416             return subscriptionInGroupList.stream()
417                     .filter(info ->
418                             info.getSimSlotIndex() != SubscriptionManager.INVALID_SIM_SLOT_INDEX)
419                     .sorted(Comparator.comparing(SubscriptionInfo::isOpportunistic))
420                     .collect(Collectors.toList());
421         }
422 
423         @Override
424         public Uri importTextMessage(String callingPkg, String address, int type, String text,
425                 long timestampMillis, boolean seen, boolean read) {
426             LogUtil.d("importTextMessage");
427             enforceSystemUid();
428             return importSms(address, type, text, timestampMillis, seen, read, callingPkg);
429         }
430 
431         @Override
432         public Uri importMultimediaMessage(String callingPkg, Uri contentUri,
433                 String messageId, long timestampSecs, boolean seen, boolean read) {
434             LogUtil.d("importMultimediaMessage");
435             enforceSystemUid();
436             return importMms(contentUri, messageId, timestampSecs, seen, read, callingPkg);
437         }
438 
439         @Override
440         public boolean deleteStoredMessage(String callingPkg, Uri messageUri)
441                 throws RemoteException {
442             LogUtil.d("deleteStoredMessage " + messageUri);
443             enforceSystemUid();
444             if (!isSmsMmsContentUri(messageUri)) {
445                 LogUtil.e("deleteStoredMessage: invalid message URI: " + messageUri.toString());
446                 return false;
447             }
448             // Clear the calling identity and query the database using the phone user id
449             // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
450             // between the calling uid and the package uid
451             final long identity = Binder.clearCallingIdentity();
452             try {
453                 if (getContentResolver().delete(
454                         messageUri, null/*where*/, null/*selectionArgs*/) != 1) {
455                     LogUtil.e("deleteStoredMessage: failed to delete");
456                     return false;
457                 }
458             } catch (SQLiteException e) {
459                 LogUtil.e("deleteStoredMessage: failed to delete", e);
460             } finally {
461                 Binder.restoreCallingIdentity(identity);
462             }
463             return true;
464         }
465 
466         @Override
467         public boolean deleteStoredConversation(String callingPkg, long conversationId)
468                 throws RemoteException {
469             LogUtil.d("deleteStoredConversation " + conversationId);
470             enforceSystemUid();
471             if (conversationId == -1) {
472                 LogUtil.e("deleteStoredConversation: invalid thread id");
473                 return false;
474             }
475             final Uri uri = ContentUris.withAppendedId(
476                     Telephony.Threads.CONTENT_URI, conversationId);
477             // Clear the calling identity and query the database using the phone user id
478             // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
479             // between the calling uid and the package uid
480             final long identity = Binder.clearCallingIdentity();
481             try {
482                 if (getContentResolver().delete(uri, null, null) != 1) {
483                     LogUtil.e("deleteStoredConversation: failed to delete");
484                     return false;
485                 }
486             } catch (SQLiteException e) {
487                 LogUtil.e("deleteStoredConversation: failed to delete", e);
488             } finally {
489                 Binder.restoreCallingIdentity(identity);
490             }
491             return true;
492         }
493 
494         @Override
495         public boolean updateStoredMessageStatus(String callingPkg, Uri messageUri,
496                 ContentValues statusValues) throws RemoteException {
497             LogUtil.d("updateStoredMessageStatus " + messageUri);
498             enforceSystemUid();
499             return updateMessageStatus(messageUri, statusValues);
500         }
501 
502         @Override
503         public boolean archiveStoredConversation(String callingPkg, long conversationId,
504                 boolean archived) throws RemoteException {
505             LogUtil.d("archiveStoredConversation " + conversationId + " " + archived);
506             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
507                 EventLog.writeEvent(0x534e4554, "180419673", Binder.getCallingUid(), "");
508             }
509             enforceSystemUid();
510             if (conversationId == -1) {
511                 LogUtil.e("archiveStoredConversation: invalid thread id");
512                 return false;
513             }
514             return archiveConversation(conversationId, archived);
515         }
516 
517         @Override
518         public Uri addTextMessageDraft(String callingPkg, String address, String text)
519                 throws RemoteException {
520             LogUtil.d("addTextMessageDraft");
521             enforceSystemUid();
522             return addSmsDraft(address, text, callingPkg);
523         }
524 
525         @Override
526         public Uri addMultimediaMessageDraft(String callingPkg, Uri contentUri)
527                 throws RemoteException {
528             LogUtil.d("addMultimediaMessageDraft");
529             enforceSystemUid();
530             return addMmsDraft(contentUri, callingPkg);
531         }
532 
533         @Override
534         public void sendStoredMessage(int subId, String callingPkg, Uri messageUri,
535                 Bundle configOverrides, PendingIntent sentIntent) throws RemoteException {
536             throw new UnsupportedOperationException();
537         }
538 
539         @Override
540         public void setAutoPersisting(String callingPkg, boolean enabled) throws RemoteException {
541             LogUtil.d("setAutoPersisting " + enabled);
542             enforceSystemUid();
543             final SharedPreferences preferences = getSharedPreferences(
544                     SHARED_PREFERENCES_NAME, MODE_PRIVATE);
545             final SharedPreferences.Editor editor = preferences.edit();
546             editor.putBoolean(PREF_AUTO_PERSISTING, enabled);
547             editor.apply();
548         }
549 
550         @Override
551         public boolean getAutoPersisting() throws RemoteException {
552             LogUtil.d("getAutoPersisting");
553             return getAutoPersistingPref();
554         }
555 
556         /*
557          * @return true if the subId is active.
558          */
559         private boolean isActiveSubId(int subId) {
560             return ((SubscriptionManager) getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE))
561                 .isActiveSubscriptionId(subId);
562         }
563 
564         /**
565          * Calls the pending intent with one of these result codes:
566          * <code>MMS_ERROR_CONFIGURATION_ERROR</code>
567          * <code>MMS_ERROR_NO_DATA_NETWORK</code>.
568          */
569         private void sendErrorInPendingIntent(@Nullable PendingIntent intent, int resultCode) {
570             LogUtil.d("sendErrorInPendingIntent - no data network");
571             if (intent != null) {
572                 try {
573                     intent.send(resultCode);
574                 } catch (PendingIntent.CanceledException ex) {
575                 }
576             }
577         }
578 
579         private boolean isRawPduSendReq(Uri contentUri) {
580             // X-Mms-Message-Type is at the beginning of the message headers always. 1st byte is
581             // MMS-filed-name and 2nd byte is MMS-value for X-Mms-Message-Type field.
582             // See OMA-TS-MMS_ENC-V1_3-20110913-A, 7. Binary Encoding of ProtocolData Units
583             byte[] pduData = new byte[2];
584             int bytesRead = readPduBytesFromContentUri(contentUri, pduData);
585 
586             // Return true for MESSAGE_TYPE_SEND_REQ only. Otherwise false even wrong PDU case.
587             if (bytesRead == 2
588                     && (pduData[0] & 0xFF) == MESSAGE_TYPE
589                     && (pduData[1] & 0xFF) == MESSAGE_TYPE_SEND_REQ) {
590                 return true;
591             }
592             return false;
593         }
594 
595         private void handleError(@Nullable PendingIntent pendingIntent, int resultCode,
596                 MmsStats mmsStats) {
597             sendErrorInPendingIntent(pendingIntent, resultCode);
598             mmsStats.addAtomToStorage(resultCode);
599         }
600     };
601 
602     @Override
addSimRequest(MmsRequest request)603     public void addSimRequest(MmsRequest request) {
604         if (request == null) {
605             LogUtil.e("Add running or pending: empty request");
606             return;
607         }
608         LogUtil.d("Current running=" + mRunningRequestCount + ", "
609                 + "current subId=" + mCurrentSubId + ", "
610                 + "pending=" + mPendingSimRequestQueue.size());
611         synchronized (this) {
612             if (mPendingSimRequestQueue.size() > 0 ||
613                     (mRunningRequestCount > 0 && request.getSubId() != mCurrentSubId)) {
614                 LogUtil.d("Add request to pending queue."
615                         + " Request subId=" + request.getSubId() + ","
616                         + " current subId=" + mCurrentSubId);
617                 mPendingSimRequestQueue.add(request);
618                 if (mRunningRequestCount <= 0) {
619                     LogUtil.e("Nothing's running but queue's not empty");
620                     // Nothing is running but we are accumulating on pending queue.
621                     // This should not happen. But just in case...
622                     movePendingSimRequestsToRunningSynchronized();
623                 }
624             } else {
625                 LogUtil.d("Add request to running queue."
626                         + " Request subId=" + request.getSubId() + ","
627                         + " current subId=" + mCurrentSubId);
628                 addToRunningRequestQueueSynchronized(request);
629             }
630         }
631     }
632 
sendSettingsIntentForFailedMms(boolean isIncoming, int subId)633     private void sendSettingsIntentForFailedMms(boolean isIncoming, int subId) {
634         LogUtil.w("Subscription with id: " + subId
635                 + " cannot " + (isIncoming ? "download" : "send")
636                 + " MMS, data connection is not available");
637         Intent intent = new Intent(Settings.ACTION_ENABLE_MMS_DATA_REQUEST);
638 
639         intent.putExtra(Settings.EXTRA_ENABLE_MMS_DATA_REQUEST_REASON,
640                 isIncoming ? Settings.ENABLE_MMS_DATA_REQUEST_REASON_INCOMING_MMS
641                         : Settings.ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS);
642 
643         intent.putExtra(Settings.EXTRA_SUB_ID, subId);
644 
645         this.sendBroadcastAsUser(intent, UserHandle.SYSTEM,
646                 android.Manifest.permission.NETWORK_SETTINGS);
647     }
648 
addToRunningRequestQueueSynchronized(final MmsRequest request)649     private void addToRunningRequestQueueSynchronized(final MmsRequest request) {
650         LogUtil.d("Add request to running queue for subId " + request.getSubId());
651         // Update current state of running requests
652         final int queue = request.getQueueType();
653         if (queue < 0 || queue >= mRunningRequestExecutors.length) {
654             LogUtil.e("Invalid request queue index for running request");
655             return;
656         }
657         mRunningRequestCount++;
658         mCurrentSubId = request.getSubId();
659         // Send to the corresponding request queue for execution
660         mRunningRequestExecutors[queue].execute(new Runnable() {
661             @Override
662             public void run() {
663                 try {
664                     request.execute(MmsService.this, getNetworkManager(request.getSubId()));
665                 } finally {
666                     synchronized (MmsService.this) {
667                         mRunningRequestCount--;
668                         LogUtil.d("addToRunningRequestQueueSynchronized mRunningRequestCount="
669                                 + mRunningRequestCount);
670                         if (mRunningRequestCount <= 0) {
671                             movePendingSimRequestsToRunningSynchronized();
672                         }
673                     }
674                 }
675             }
676         });
677     }
678 
movePendingSimRequestsToRunningSynchronized()679     private void movePendingSimRequestsToRunningSynchronized() {
680         LogUtil.d("Move pending requests to running queue mPendingSimRequestQueue.size="
681                 + mPendingSimRequestQueue.size());
682         mCurrentSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
683         while (mPendingSimRequestQueue.size() > 0) {
684             final MmsRequest request = mPendingSimRequestQueue.peek();
685             if (request != null) {
686                 if (!SubscriptionManager.isValidSubscriptionId(mCurrentSubId)
687                         || mCurrentSubId == request.getSubId()) {
688                     // First or subsequent requests with same SIM ID
689                     mPendingSimRequestQueue.remove();
690                     LogUtil.d("Move pending request to running queue."
691                             + " Request subId=" + request.getSubId() + ","
692                             + " current subId=" + mCurrentSubId);
693                     addToRunningRequestQueueSynchronized(request);
694                 } else {
695                     // Stop if we see a different SIM ID
696                     LogUtil.d("Pending request not moved to running queue, different subId."
697                             + " Request subId=" + request.getSubId() + ","
698                             + " current subId=" + mCurrentSubId);
699                     break;
700                 }
701             } else {
702                 LogUtil.e("Schedule pending: found empty request");
703                 mPendingSimRequestQueue.remove();
704             }
705         }
706     }
707 
708     @Override
onBind(Intent intent)709     public IBinder onBind(Intent intent) {
710         return mStub;
711     }
712 
asBinder()713     public final IBinder asBinder() {
714         return mStub;
715     }
716 
717     @Override
onCreate()718     public void onCreate() {
719         super.onCreate();
720         LogUtil.d("onCreate");
721         // Load mms_config
722         MmsConfigManager.getInstance().init(this);
723 
724         NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true);
725 
726         // Registers statsd pullers
727         mMmsMetricsCollector = new MmsMetricsCollector(this);
728 
729         // Initialize running request state
730         for (int i = 0; i < mRunningRequestExecutors.length; i++) {
731             mRunningRequestExecutors[i] = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
732         }
733         synchronized (this) {
734             mCurrentSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
735             mRunningRequestCount = 0;
736         }
737     }
738 
739     @Override
onDestroy()740     public void onDestroy() {
741         super.onDestroy();
742         LogUtil.d("onDestroy");
743         for (ExecutorService executor : mRunningRequestExecutors) {
744             executor.shutdown();
745         }
746     }
747 
importSms(String address, int type, String text, long timestampMillis, boolean seen, boolean read, String creator)748     private Uri importSms(String address, int type, String text, long timestampMillis,
749             boolean seen, boolean read, String creator) {
750         Uri insertUri = null;
751         switch (type) {
752             case SMS_TYPE_INCOMING:
753                 insertUri = Telephony.Sms.Inbox.CONTENT_URI;
754 
755                 break;
756             case SMS_TYPE_OUTGOING:
757                 insertUri = Telephony.Sms.Sent.CONTENT_URI;
758                 break;
759         }
760         if (insertUri == null) {
761             LogUtil.e("importTextMessage: invalid message type for importing: " + type);
762             return null;
763         }
764         final ContentValues values = new ContentValues(6);
765         values.put(Telephony.Sms.ADDRESS, address);
766         values.put(Telephony.Sms.DATE, timestampMillis);
767         values.put(Telephony.Sms.SEEN, seen ? 1 : 0);
768         values.put(Telephony.Sms.READ, read ? 1 : 0);
769         values.put(Telephony.Sms.BODY, text);
770         if (!TextUtils.isEmpty(creator)) {
771             values.put(Telephony.Mms.CREATOR, creator);
772         }
773         // Clear the calling identity and query the database using the phone user id
774         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
775         // between the calling uid and the package uid
776         final long identity = Binder.clearCallingIdentity();
777         try {
778             return getContentResolver().insert(insertUri, values);
779         } catch (SQLiteException e) {
780             LogUtil.e("importTextMessage: failed to persist imported text message", e);
781         } finally {
782             Binder.restoreCallingIdentity(identity);
783         }
784         return null;
785     }
786 
importMms(Uri contentUri, String messageId, long timestampSecs, boolean seen, boolean read, String creator)787     private Uri importMms(Uri contentUri, String messageId, long timestampSecs,
788             boolean seen, boolean read, String creator) {
789         byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE);
790         if (pduData == null || pduData.length < 1) {
791             LogUtil.e("importMessage: empty PDU");
792             return null;
793         }
794         // Clear the calling identity and query the database using the phone user id
795         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
796         // between the calling uid and the package uid
797         final long identity = Binder.clearCallingIdentity();
798         try {
799             final GenericPdu pdu = parsePduForAnyCarrier(pduData);
800             if (pdu == null) {
801                 LogUtil.e("importMessage: can't parse input PDU");
802                 return null;
803             }
804             Uri insertUri = null;
805             if (pdu instanceof SendReq) {
806                 insertUri = Telephony.Mms.Sent.CONTENT_URI;
807             } else if (pdu instanceof RetrieveConf ||
808                     pdu instanceof NotificationInd ||
809                     pdu instanceof DeliveryInd ||
810                     pdu instanceof ReadOrigInd) {
811                 insertUri = Telephony.Mms.Inbox.CONTENT_URI;
812             }
813             if (insertUri == null) {
814                 LogUtil.e("importMessage; invalid MMS type: " + pdu.getClass().getCanonicalName());
815                 return null;
816             }
817             final PduPersister persister = PduPersister.getPduPersister(this);
818             final Uri uri = persister.persist(
819                     pdu,
820                     insertUri,
821                     true/*createThreadId*/,
822                     true/*groupMmsEnabled*/,
823                     null/*preOpenedFiles*/);
824             if (uri == null) {
825                 LogUtil.e("importMessage: failed to persist message");
826                 return null;
827             }
828             final ContentValues values = new ContentValues(5);
829             if (!TextUtils.isEmpty(messageId)) {
830                 values.put(Telephony.Mms.MESSAGE_ID, messageId);
831             }
832             if (timestampSecs != -1) {
833                 values.put(Telephony.Mms.DATE, timestampSecs);
834             }
835             values.put(Telephony.Mms.READ, seen ? 1 : 0);
836             values.put(Telephony.Mms.SEEN, read ? 1 : 0);
837             if (!TextUtils.isEmpty(creator)) {
838                 values.put(Telephony.Mms.CREATOR, creator);
839             }
840             if (SqliteWrapper.update(this, getContentResolver(), uri, values,
841                     null/*where*/, null/*selectionArg*/) != 1) {
842                 LogUtil.e("importMessage: failed to update message");
843             }
844             return uri;
845         } catch (RuntimeException e) {
846             LogUtil.e("importMessage: failed to parse input PDU", e);
847         } catch (MmsException e) {
848             LogUtil.e("importMessage: failed to persist message", e);
849         } finally {
850             Binder.restoreCallingIdentity(identity);
851         }
852         return null;
853     }
854 
isSmsMmsContentUri(Uri uri)855     private static boolean isSmsMmsContentUri(Uri uri) {
856         final String uriString = uri.toString();
857         if (!uriString.startsWith("content://sms/") && !uriString.startsWith("content://mms/")) {
858             return false;
859         }
860         if (ContentUris.parseId(uri) == -1) {
861             return false;
862         }
863         return true;
864     }
865 
updateMessageStatus(Uri messageUri, ContentValues statusValues)866     private boolean updateMessageStatus(Uri messageUri, ContentValues statusValues) {
867         if (!isSmsMmsContentUri(messageUri)) {
868             LogUtil.e("updateMessageStatus: invalid messageUri: " + messageUri.toString());
869             return false;
870         }
871         if (statusValues == null) {
872             LogUtil.w("updateMessageStatus: empty values to update");
873             return false;
874         }
875         final ContentValues values = new ContentValues();
876         if (statusValues.containsKey(MESSAGE_STATUS_READ)) {
877             final Integer val = statusValues.getAsInteger(MESSAGE_STATUS_READ);
878             if (val != null) {
879                 // MMS uses the same column name
880                 values.put(Telephony.Sms.READ, val);
881             }
882         } else if (statusValues.containsKey(MESSAGE_STATUS_SEEN)) {
883             final Integer val = statusValues.getAsInteger(MESSAGE_STATUS_SEEN);
884             if (val != null) {
885                 // MMS uses the same column name
886                 values.put(Telephony.Sms.SEEN, val);
887             }
888         }
889         if (values.size() < 1) {
890             LogUtil.w("updateMessageStatus: no value to update");
891             return false;
892         }
893         // Clear the calling identity and query the database using the phone user id
894         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
895         // between the calling uid and the package uid
896         final long identity = Binder.clearCallingIdentity();
897         try {
898             if (getContentResolver().update(
899                     messageUri, values, null/*where*/, null/*selectionArgs*/) != 1) {
900                 LogUtil.e("updateMessageStatus: failed to update database");
901                 return false;
902             }
903             return true;
904         } catch (SQLiteException e) {
905             LogUtil.e("updateMessageStatus: failed to update database", e);
906         } finally {
907             Binder.restoreCallingIdentity(identity);
908         }
909         return false;
910     }
911 
912     private static final String ARCHIVE_CONVERSATION_SELECTION = Telephony.Threads._ID + "=?";
913 
archiveConversation(long conversationId, boolean archived)914     private boolean archiveConversation(long conversationId, boolean archived) {
915         final ContentValues values = new ContentValues(1);
916         values.put(Telephony.Threads.ARCHIVED, archived ? 1 : 0);
917         // Clear the calling identity and query the database using the phone user id
918         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
919         // between the calling uid and the package uid
920         final long identity = Binder.clearCallingIdentity();
921         try {
922             if (getContentResolver().update(
923                     Telephony.Threads.CONTENT_URI,
924                     values,
925                     ARCHIVE_CONVERSATION_SELECTION,
926                     new String[]{Long.toString(conversationId)}) != 1) {
927                 LogUtil.e("archiveConversation: failed to update database");
928                 return false;
929             }
930             return true;
931         } catch (SQLiteException e) {
932             LogUtil.e("archiveConversation: failed to update database", e);
933         } finally {
934             Binder.restoreCallingIdentity(identity);
935         }
936         return false;
937     }
938 
addSmsDraft(String address, String text, String creator)939     private Uri addSmsDraft(String address, String text, String creator) {
940         final ContentValues values = new ContentValues(5);
941         values.put(Telephony.Sms.ADDRESS, address);
942         values.put(Telephony.Sms.BODY, text);
943         values.put(Telephony.Sms.READ, 1);
944         values.put(Telephony.Sms.SEEN, 1);
945         if (!TextUtils.isEmpty(creator)) {
946             values.put(Telephony.Mms.CREATOR, creator);
947         }
948         // Clear the calling identity and query the database using the phone user id
949         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
950         // between the calling uid and the package uid
951         final long identity = Binder.clearCallingIdentity();
952         try {
953             return getContentResolver().insert(Telephony.Sms.Draft.CONTENT_URI, values);
954         } catch (SQLiteException e) {
955             LogUtil.e("addSmsDraft: failed to store draft message", e);
956         } finally {
957             Binder.restoreCallingIdentity(identity);
958         }
959         return null;
960     }
961 
addMmsDraft(Uri contentUri, String creator)962     private Uri addMmsDraft(Uri contentUri, String creator) {
963         byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE);
964         if (pduData == null || pduData.length < 1) {
965             LogUtil.e("addMmsDraft: empty PDU");
966             return null;
967         }
968         // Clear the calling identity and query the database using the phone user id
969         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
970         // between the calling uid and the package uid
971         final long identity = Binder.clearCallingIdentity();
972         try {
973             final GenericPdu pdu = parsePduForAnyCarrier(pduData);
974             if (pdu == null) {
975                 LogUtil.e("addMmsDraft: can't parse input PDU");
976                 return null;
977             }
978             if (!(pdu instanceof SendReq)) {
979                 LogUtil.e("addMmsDraft; invalid MMS type: " + pdu.getClass().getCanonicalName());
980                 return null;
981             }
982             final PduPersister persister = PduPersister.getPduPersister(this);
983             final Uri uri = persister.persist(
984                     pdu,
985                     Telephony.Mms.Draft.CONTENT_URI,
986                     true/*createThreadId*/,
987                     true/*groupMmsEnabled*/,
988                     null/*preOpenedFiles*/);
989             if (uri == null) {
990                 LogUtil.e("addMmsDraft: failed to persist message");
991                 return null;
992             }
993             final ContentValues values = new ContentValues(3);
994             values.put(Telephony.Mms.READ, 1);
995             values.put(Telephony.Mms.SEEN, 1);
996             if (!TextUtils.isEmpty(creator)) {
997                 values.put(Telephony.Mms.CREATOR, creator);
998             }
999             if (SqliteWrapper.update(this, getContentResolver(), uri, values,
1000                     null/*where*/, null/*selectionArg*/) != 1) {
1001                 LogUtil.e("addMmsDraft: failed to update message");
1002             }
1003             return uri;
1004         } catch (RuntimeException e) {
1005             LogUtil.e("addMmsDraft: failed to parse input PDU", e);
1006         } catch (MmsException e) {
1007             LogUtil.e("addMmsDraft: failed to persist message", e);
1008         } finally {
1009             Binder.restoreCallingIdentity(identity);
1010         }
1011         return null;
1012     }
1013 
1014     /**
1015      * Try parsing a PDU without knowing the carrier. This is useful for importing
1016      * MMS or storing draft when carrier info is not available
1017      *
1018      * @param data The PDU data
1019      * @return Parsed PDU, null if failed to parse
1020      */
parsePduForAnyCarrier(final byte[] data)1021     private static GenericPdu parsePduForAnyCarrier(final byte[] data) {
1022         GenericPdu pdu = null;
1023         try {
1024             pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse();
1025         } catch (RuntimeException e) {
1026             LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU with content disposition", e);
1027         }
1028         if (pdu == null) {
1029             try {
1030                 pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse();
1031             } catch (RuntimeException e) {
1032                 LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU without content disposition",
1033                         e);
1034             }
1035         }
1036         return pdu;
1037     }
1038 
1039     @Override
getAutoPersistingPref()1040     public boolean getAutoPersistingPref() {
1041         final SharedPreferences preferences = getSharedPreferences(
1042                 SHARED_PREFERENCES_NAME, MODE_PRIVATE);
1043         return preferences.getBoolean(PREF_AUTO_PERSISTING, false);
1044     }
1045 
1046     /**
1047      * Read pdu from content provider uri.
1048      *
1049      * @param contentUri content provider uri from which to read.
1050      * @param maxSize    maximum number of bytes to read.
1051      * @return pdu bytes if succeeded else null.
1052      */
readPduFromContentUri(final Uri contentUri, final int maxSize)1053     public byte[] readPduFromContentUri(final Uri contentUri, final int maxSize) {
1054         // Request one extra byte to make sure file not bigger than maxSize
1055         byte[] pduData = new byte[maxSize + 1];
1056         int bytesRead = readPduBytesFromContentUri(contentUri, pduData);
1057         if (bytesRead <= 0) {
1058             return null;
1059         }
1060         if (bytesRead > maxSize) {
1061             LogUtil.e("PDU read is too large");
1062             return null;
1063         }
1064         return Arrays.copyOf(pduData, bytesRead);
1065     }
1066 
1067     /**
1068      * Read up to length of the pduData array from content provider uri.
1069      *
1070      * @param contentUri content provider uri from which to read.
1071      * @param pduData    the buffer into which the data is read.
1072      * @return the total number of bytes read into the pduData.
1073      */
readPduBytesFromContentUri(final Uri contentUri, byte[] pduData)1074     public int readPduBytesFromContentUri(final Uri contentUri, byte[] pduData) {
1075         if (contentUri == null) {
1076             LogUtil.e("Uri is null");
1077             return 0;
1078         }
1079         Callable<Integer> copyPduToArray = new Callable<Integer>() {
1080             public Integer call() {
1081                 ParcelFileDescriptor.AutoCloseInputStream inStream = null;
1082                 try {
1083                     ContentResolver cr = MmsService.this.getContentResolver();
1084                     ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "r");
1085                     inStream = new ParcelFileDescriptor.AutoCloseInputStream(pduFd);
1086                     int bytesRead = inStream.read(pduData, 0, pduData.length);
1087                     if (bytesRead <= 0) {
1088                         LogUtil.e("Empty PDU or at end of the file");
1089                     }
1090                     return bytesRead;
1091                 } catch (IOException ex) {
1092                     LogUtil.e("IO exception reading PDU", ex);
1093                     return 0;
1094                 } finally {
1095                     if (inStream != null) {
1096                         try {
1097                             inStream.close();
1098                         } catch (IOException ex) {
1099                         }
1100                     }
1101                 }
1102             }
1103         };
1104 
1105         final Future<Integer> pendingResult = mPduTransferExecutor.submit(copyPduToArray);
1106         try {
1107             return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
1108         } catch (Exception e) {
1109             // Typically a timeout occurred - cancel task
1110             pendingResult.cancel(true);
1111             LogUtil.e("Exception during PDU read", e);
1112         }
1113         return 0;
1114     }
1115 
1116     /**
1117      * Write pdu bytes to content provider uri
1118      *
1119      * @param contentUri content provider uri to which bytes should be written
1120      * @param pdu        Bytes to write
1121      * @return true if all bytes successfully written else false
1122      */
writePduToContentUri(final Uri contentUri, final byte[] pdu)1123     public boolean writePduToContentUri(final Uri contentUri, final byte[] pdu) {
1124         if (contentUri == null || pdu == null) {
1125             return false;
1126         }
1127         final Callable<Boolean> copyDownloadedPduToOutput = new Callable<Boolean>() {
1128             public Boolean call() {
1129                 ParcelFileDescriptor.AutoCloseOutputStream outStream = null;
1130                 try {
1131                     ContentResolver cr = MmsService.this.getContentResolver();
1132                     ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "w");
1133                     outStream = new ParcelFileDescriptor.AutoCloseOutputStream(pduFd);
1134                     outStream.write(pdu);
1135                     return Boolean.TRUE;
1136                 } catch (IOException ex) {
1137                     LogUtil.e("IO exception writing PDU", ex);
1138                     return Boolean.FALSE;
1139                 } finally {
1140                     if (outStream != null) {
1141                         try {
1142                             outStream.close();
1143                         } catch (IOException ex) {
1144                         }
1145                     }
1146                 }
1147             }
1148         };
1149 
1150         final Future<Boolean> pendingResult =
1151                 mPduTransferExecutor.submit(copyDownloadedPduToOutput);
1152         try {
1153             return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
1154         } catch (Exception e) {
1155             // Typically a timeout occurred - cancel task
1156             pendingResult.cancel(true);
1157             LogUtil.e("Exception during PDU write", e);
1158         }
1159         return false;
1160     }
1161 
formatCrossStackMessageId(long id)1162     static String formatCrossStackMessageId(long id) {
1163         return "{x-message-id:" + id + "}";
1164     }
1165 }
1166