• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 Samsung System LSI
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 package com.android.bluetooth.map;
16 
17 import android.bluetooth.BluetoothProfile;
18 import android.bluetooth.BluetoothProtoEnums;
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.os.ParcelFileDescriptor;
24 import android.provider.BaseColumns;
25 import android.provider.ContactsContract;
26 import android.provider.ContactsContract.Contacts;
27 import android.provider.ContactsContract.PhoneLookup;
28 import android.provider.Telephony.CanonicalAddressesColumns;
29 import android.provider.Telephony.Mms;
30 import android.provider.Telephony.MmsSms;
31 import android.provider.Telephony.Sms;
32 import android.provider.Telephony.Threads;
33 import android.telephony.PhoneNumberUtils;
34 import android.telephony.TelephonyManager;
35 import android.text.TextUtils;
36 import android.text.util.Rfc822Token;
37 import android.text.util.Rfc822Tokenizer;
38 import android.util.Log;
39 
40 import com.android.bluetooth.BluetoothMethodProxy;
41 import com.android.bluetooth.BluetoothStatsLog;
42 import com.android.bluetooth.DeviceWorkArounds;
43 import com.android.bluetooth.SignedLongLong;
44 import com.android.bluetooth.Utils;
45 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
46 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
47 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
48 import com.android.bluetooth.mapapi.BluetoothMapContract;
49 import com.android.bluetooth.mapapi.BluetoothMapContract.ConversationColumns;
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import com.google.android.mms.pdu.CharacterSets;
53 import com.google.android.mms.pdu.PduHeaders;
54 
55 import java.io.ByteArrayOutputStream;
56 import java.io.Closeable;
57 import java.io.FileInputStream;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.UnsupportedEncodingException;
62 import java.nio.charset.StandardCharsets;
63 import java.util.Arrays;
64 import java.util.HashMap;
65 import java.util.List;
66 import java.util.Locale;
67 import java.util.Map;
68 import java.util.stream.Collectors;
69 
70 // Next tag value for ContentProfileErrorReportUtils.report(): 15
71 public class BluetoothMapContent {
72     private static final String TAG = BluetoothMapContent.class.getSimpleName();
73 
74     // Parameter Mask for selection of parameters to return in listings
75     private static final int MASK_SUBJECT = 0x00000001;
76     @VisibleForTesting static final int MASK_DATETIME = 0x00000002;
77     @VisibleForTesting static final int MASK_SENDER_NAME = 0x00000004;
78     @VisibleForTesting static final int MASK_SENDER_ADDRESSING = 0x00000008;
79     private static final int MASK_RECIPIENT_NAME = 0x00000010;
80     @VisibleForTesting static final int MASK_RECIPIENT_ADDRESSING = 0x00000020;
81     private static final int MASK_TYPE = 0x00000040;
82     private static final int MASK_SIZE = 0x00000080;
83     private static final int MASK_RECEPTION_STATUS = 0x00000100;
84     private static final int MASK_TEXT = 0x00000200;
85     @VisibleForTesting static final int MASK_ATTACHMENT_SIZE = 0x00000400;
86     private static final int MASK_PRIORITY = 0x00000800;
87     private static final int MASK_READ = 0x00001000;
88     private static final int MASK_SENT = 0x00002000;
89     private static final int MASK_PROTECTED = 0x00004000;
90     // private static final int MASK_REPLYTO_ADDRESSING = 0x00008000;
91     // TODO: Duplicate in proposed spec
92     // private static final int MASK_RECEPTION_STATE       = 0x00010000;
93     @VisibleForTesting static final int MASK_DELIVERY_STATUS = 0x00010000;
94     private static final int MASK_CONVERSATION_ID = 0x00020000;
95     private static final int MASK_CONVERSATION_NAME = 0x00040000;
96     // private static final int MASK_FOLDER_TYPE = 0x00100000;
97     // TODO: about to be removed from proposed spec
98     // private static final int MASK_SEQUENCE_NUMBER       = 0x00200000;
99     private static final int MASK_ATTACHMENT_MIME = 0x00100000;
100 
101     private static final int CONVO_PARAM_MASK_CONVO_NAME = 0x00000001;
102     private static final int CONVO_PARAM_MASK_CONVO_LAST_ACTIVITY = 0x00000002;
103     private static final int CONVO_PARAM_MASK_CONVO_READ_STATUS = 0x00000004;
104     private static final int CONVO_PARAM_MASK_CONVO_VERSION_COUNTER = 0x00000008;
105     private static final int CONVO_PARAM_MASK_CONVO_SUMMARY = 0x00000010;
106     private static final int CONVO_PARAM_MASK_PARTICIPANTS = 0x00000020;
107     private static final int CONVO_PARAM_MASK_PART_UCI = 0x00000040;
108     private static final int CONVO_PARAM_MASK_PART_DISP_NAME = 0x00000080;
109     private static final int CONVO_PARAM_MASK_PART_CHAT_STATE = 0x00000100;
110     private static final int CONVO_PARAM_MASK_PART_LAST_ACTIVITY = 0x00000200;
111     private static final int CONVO_PARAM_MASK_PART_X_BT_UID = 0x00000400;
112     private static final int CONVO_PARAM_MASK_PART_NAME = 0x00000800;
113     private static final int CONVO_PARAM_MASK_PART_PRESENCE = 0x00001000;
114     private static final int CONVO_PARAM_MASK_PART_PRESENCE_TEXT = 0x00002000;
115     private static final int CONVO_PARAM_MASK_PART_PRIORITY = 0x00004000;
116 
117     /* Default values for omitted or 0 parameterMask application parameters */
118     // MAP specification states that the default value for parameter mask are
119     // the #REQUIRED attributes in the DTD, and not all enabled
120     public static final long PARAMETER_MASK_ALL_ENABLED = 0xFFFFFFFFL;
121     public static final long CONVO_PARAMETER_MASK_ALL_ENABLED = 0xFFFFFFFFL;
122     public static final long CONVO_PARAMETER_MASK_DEFAULT =
123             CONVO_PARAM_MASK_CONVO_NAME
124                     | CONVO_PARAM_MASK_PARTICIPANTS
125                     | CONVO_PARAM_MASK_PART_UCI
126                     | CONVO_PARAM_MASK_PART_DISP_NAME;
127 
128     private static final int FILTER_READ_STATUS_UNREAD_ONLY = 0x01;
129     private static final int FILTER_READ_STATUS_READ_ONLY = 0x02;
130     // private static final int FILTER_READ_STATUS_ALL = 0x00;
131 
132     /* Type of MMS address. From Telephony.java it must be one of PduHeaders.BCC, */
133     /* PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO. These are from PduHeaders.java */
134     public static final int MMS_FROM = 0x89;
135     public static final int MMS_TO = 0x97;
136     public static final int MMS_BCC = 0x81;
137     public static final int MMS_CC = 0x82;
138 
139     /* OMA-TS-MMS-ENC defined many types in X-Mms-Message-Type.
140     Only m-send-req (128) m-retrieve-conf (132), m-notification-ind (130)
141     are interested by user */
142     private static final String INTERESTED_MESSAGE_TYPE_CLAUSE =
143             Utils.formatSimple(
144                     "( %s = %d OR %s = %d OR %s = %d )",
145                     Mms.MESSAGE_TYPE,
146                     PduHeaders.MESSAGE_TYPE_SEND_REQ,
147                     Mms.MESSAGE_TYPE,
148                     PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF,
149                     Mms.MESSAGE_TYPE,
150                     PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
151 
152     private static final String INSERT_ADDRESS_TOKEN = "insert-address-token";
153 
154     private final Context mContext;
155     private final ContentResolver mResolver;
156     @VisibleForTesting final String mBaseUri;
157     private final BluetoothMapAccountItem mAccount;
158     /* The MasInstance reference is used to update persistent (over a connection) version counters*/
159     private final BluetoothMapMasInstance mMasInstance;
160     @VisibleForTesting String mMessageVersion = BluetoothMapUtils.MAP_V10_STR;
161 
162     private int mRemoteFeatureMask = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK;
163     @VisibleForTesting int mMsgListingVersion = BluetoothMapUtils.MAP_MESSAGE_LISTING_FORMAT_V10;
164 
165     static final String[] SMS_PROJECTION =
166             new String[] {
167                 BaseColumns._ID,
168                 Sms.THREAD_ID,
169                 Sms.ADDRESS,
170                 Sms.BODY,
171                 Sms.DATE,
172                 Sms.READ,
173                 Sms.TYPE,
174                 Sms.STATUS,
175                 Sms.LOCKED,
176                 Sms.ERROR_CODE
177             };
178 
179     static final String[] MMS_PROJECTION =
180             new String[] {
181                 BaseColumns._ID,
182                 Mms.THREAD_ID,
183                 Mms.MESSAGE_ID,
184                 Mms.MESSAGE_SIZE,
185                 Mms.SUBJECT,
186                 Mms.CONTENT_TYPE,
187                 Mms.TEXT_ONLY,
188                 Mms.DATE,
189                 Mms.DATE_SENT,
190                 Mms.READ,
191                 Mms.MESSAGE_BOX,
192                 Mms.STATUS,
193                 Mms.PRIORITY,
194             };
195 
196     static final String[] SMS_CONVO_PROJECTION =
197             new String[] {
198                 BaseColumns._ID,
199                 Sms.THREAD_ID,
200                 Sms.ADDRESS,
201                 Sms.DATE,
202                 Sms.READ,
203                 Sms.TYPE,
204                 Sms.STATUS,
205                 Sms.LOCKED,
206                 Sms.ERROR_CODE
207             };
208 
209     static final String[] MMS_CONVO_PROJECTION =
210             new String[] {
211                 BaseColumns._ID,
212                 Mms.THREAD_ID,
213                 Mms.MESSAGE_ID,
214                 Mms.MESSAGE_SIZE,
215                 Mms.SUBJECT,
216                 Mms.CONTENT_TYPE,
217                 Mms.TEXT_ONLY,
218                 Mms.DATE,
219                 Mms.DATE_SENT,
220                 Mms.READ,
221                 Mms.MESSAGE_BOX,
222                 Mms.STATUS,
223                 Mms.PRIORITY,
224                 Mms.Addr.ADDRESS
225             };
226 
227     /* CONVO LISTING projections and column indexes */
228     @VisibleForTesting
229     static final String[] MMS_SMS_THREAD_PROJECTION = {
230         Threads._ID,
231         Threads.DATE,
232         Threads.SNIPPET,
233         Threads.SNIPPET_CHARSET,
234         Threads.READ,
235         Threads.RECIPIENT_IDS
236     };
237 
238     private static final String[] CONVO_VERSION_PROJECTION =
239             new String[] {
240                 /* Thread information */
241                 ConversationColumns.THREAD_ID,
242                 ConversationColumns.THREAD_NAME,
243                 ConversationColumns.READ_STATUS,
244                 ConversationColumns.LAST_THREAD_ACTIVITY,
245                 ConversationColumns.SUMMARY,
246             };
247 
248     /* Optimize the Cursor access to avoid the need to do a getColumnIndex() */
249     private static final int MMS_SMS_THREAD_COL_ID;
250     private static final int MMS_SMS_THREAD_COL_DATE;
251     private static final int MMS_SMS_THREAD_COL_SNIPPET;
252     private static final int MMS_SMS_THREAD_COL_SNIPPET_CS;
253     private static final int MMS_SMS_THREAD_COL_READ;
254     private static final int MMS_SMS_THREAD_COL_RECIPIENT_IDS;
255 
256     static {
257         // TODO: This might not work, if the projection is mapped in the content provider...
258         //       Change to init at first query? (Current use in the AOSP code is hard coded values
259         //       unrelated to the projection used)
260         List<String> projection = Arrays.asList(MMS_SMS_THREAD_PROJECTION);
261         MMS_SMS_THREAD_COL_ID = projection.indexOf(Threads._ID);
262         MMS_SMS_THREAD_COL_DATE = projection.indexOf(Threads.DATE);
263         MMS_SMS_THREAD_COL_SNIPPET = projection.indexOf(Threads.SNIPPET);
264         MMS_SMS_THREAD_COL_SNIPPET_CS = projection.indexOf(Threads.SNIPPET_CHARSET);
265         MMS_SMS_THREAD_COL_READ = projection.indexOf(Threads.READ);
266         MMS_SMS_THREAD_COL_RECIPIENT_IDS = projection.indexOf(Threads.RECIPIENT_IDS);
267     }
268 
269     @VisibleForTesting
270     static class FilterInfo {
271         public static final int TYPE_SMS = 0;
272         public static final int TYPE_MMS = 1;
273         public static final int TYPE_EMAIL = 2;
274         public static final int TYPE_IM = 3;
275 
276         // TODO: Change to ENUM, to ensure correct usage
277         int mMsgType = TYPE_SMS;
278         int mPhoneType = 0;
279         String mPhoneNum = null;
280         String mPhoneAlphaTag = null;
281         /*column indices used to optimize queries */
282         public int mMessageColId = -1;
283         public int mMessageColDate = -1;
284         public int mMessageColBody = -1;
285         public int mMessageColSubject = -1;
286         public int mMessageColFolder = -1;
287         public int mMessageColRead = -1;
288         public int mMessageColSize = -1;
289         public int mMessageColFromAddress = -1;
290         public int mMessageColToAddress = -1;
291         public int mMessageColCcAddress = -1;
292         public int mMessageColBccAddress = -1;
293         public int mMessageColReplyTo = -1;
294         public int mMessageColAccountId = -1;
295         public int mMessageColAttachment = -1;
296         public int mMessageColAttachmentSize = -1;
297         public int mMessageColAttachmentMime = -1;
298         public int mMessageColPriority = -1;
299         public int mMessageColProtected = -1;
300         public int mMessageColReception = -1;
301         public int mMessageColDelivery = -1;
302         public int mMessageColThreadId = -1;
303         public int mMessageColThreadName = -1;
304 
305         public int mSmsColFolder = -1;
306         public int mSmsColRead = -1;
307         public int mSmsColId = -1;
308         public int mSmsColSubject = -1;
309         public int mSmsColAddress = -1;
310         public int mSmsColDate = -1;
311         public int mSmsColType = -1;
312         public int mSmsColThreadId = -1;
313 
314         public int mMmsColRead = -1;
315         public int mMmsColFolder = -1;
316         public int mMmsColAttachmentSize = -1;
317         public int mMmsColTextOnly = -1;
318         public int mMmsColId = -1;
319         public int mMmsColSize = -1;
320         public int mMmsColDate = -1;
321         public int mMmsColSubject = -1;
322         public int mMmsColThreadId = -1;
323 
324         public int mConvoColConvoId = -1;
325         public int mConvoColLastActivity = -1;
326         public int mConvoColName = -1;
327         public int mConvoColRead = -1;
328         public int mConvoColVersionCounter = -1;
329         public int mConvoColSummary = -1;
330         public int mContactColBtUid = -1;
331         public int mContactColChatState = -1;
332         public int mContactColContactUci = -1;
333         public int mContactColNickname = -1;
334         public int mContactColLastActive = -1;
335         public int mContactColName = -1;
336         public int mContactColPresenceState = -1;
337         public int mContactColPresenceText = -1;
338         public int mContactColPriority = -1;
339 
setMessageColumns(Cursor c)340         public void setMessageColumns(Cursor c) {
341             mMessageColId = c.getColumnIndex(BluetoothMapContract.MessageColumns._ID);
342             mMessageColDate = c.getColumnIndex(BluetoothMapContract.MessageColumns.DATE);
343             mMessageColSubject = c.getColumnIndex(BluetoothMapContract.MessageColumns.SUBJECT);
344             mMessageColFolder = c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID);
345             mMessageColRead = c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ);
346             mMessageColSize = c.getColumnIndex(BluetoothMapContract.MessageColumns.MESSAGE_SIZE);
347             mMessageColFromAddress =
348                     c.getColumnIndex(BluetoothMapContract.MessageColumns.FROM_LIST);
349             mMessageColToAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.TO_LIST);
350             mMessageColAttachment =
351                     c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_ATTACHMENT);
352             mMessageColAttachmentSize =
353                     c.getColumnIndex(BluetoothMapContract.MessageColumns.ATTACHMENT_SIZE);
354             mMessageColPriority =
355                     c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY);
356             mMessageColProtected =
357                     c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_PROTECTED);
358             mMessageColReception =
359                     c.getColumnIndex(BluetoothMapContract.MessageColumns.RECEPTION_STATE);
360             mMessageColDelivery =
361                     c.getColumnIndex(BluetoothMapContract.MessageColumns.DELIVERY_STATE);
362             mMessageColThreadId = c.getColumnIndex(BluetoothMapContract.MessageColumns.THREAD_ID);
363         }
364 
setEmailMessageColumns(Cursor c)365         public void setEmailMessageColumns(Cursor c) {
366             setMessageColumns(c);
367             mMessageColCcAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.CC_LIST);
368             mMessageColBccAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.BCC_LIST);
369             mMessageColReplyTo =
370                     c.getColumnIndex(BluetoothMapContract.MessageColumns.REPLY_TO_LIST);
371         }
372 
setImMessageColumns(Cursor c)373         public void setImMessageColumns(Cursor c) {
374             setMessageColumns(c);
375             mMessageColThreadName =
376                     c.getColumnIndex(BluetoothMapContract.MessageColumns.THREAD_NAME);
377             mMessageColAttachmentMime =
378                     c.getColumnIndex(BluetoothMapContract.MessageColumns.ATTACHMENT_MINE_TYPES);
379             // TODO this is temporary as text should come from parts table instead
380             mMessageColBody = c.getColumnIndex(BluetoothMapContract.MessageColumns.BODY);
381         }
382 
setEmailImConvoColumns(Cursor c)383         public void setEmailImConvoColumns(Cursor c) {
384             mConvoColConvoId = c.getColumnIndex(BluetoothMapContract.ConversationColumns.THREAD_ID);
385             mConvoColLastActivity =
386                     c.getColumnIndex(BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY);
387             mConvoColName = c.getColumnIndex(BluetoothMapContract.ConversationColumns.THREAD_NAME);
388             mConvoColRead = c.getColumnIndex(BluetoothMapContract.ConversationColumns.READ_STATUS);
389             mConvoColVersionCounter =
390                     c.getColumnIndex(BluetoothMapContract.ConversationColumns.VERSION_COUNTER);
391             mConvoColSummary = c.getColumnIndex(BluetoothMapContract.ConversationColumns.SUMMARY);
392             setEmailImConvoContactColumns(c);
393         }
394 
setEmailImConvoContactColumns(Cursor c)395         public void setEmailImConvoContactColumns(Cursor c) {
396             mContactColBtUid = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.X_BT_UID);
397             mContactColChatState =
398                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.CHAT_STATE);
399             mContactColContactUci = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.UCI);
400             mContactColNickname =
401                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NICKNAME);
402             mContactColLastActive =
403                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.LAST_ACTIVE);
404             mContactColName = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME);
405             mContactColPresenceState =
406                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.PRESENCE_STATE);
407             mContactColPresenceText =
408                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.STATUS_TEXT);
409             mContactColPriority =
410                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.PRIORITY);
411         }
412 
setSmsColumns(Cursor c)413         public void setSmsColumns(Cursor c) {
414             mSmsColId = c.getColumnIndex(BaseColumns._ID);
415             mSmsColFolder = c.getColumnIndex(Sms.TYPE);
416             mSmsColRead = c.getColumnIndex(Sms.READ);
417             mSmsColSubject = c.getColumnIndex(Sms.BODY);
418             mSmsColAddress = c.getColumnIndex(Sms.ADDRESS);
419             mSmsColDate = c.getColumnIndex(Sms.DATE);
420             mSmsColType = c.getColumnIndex(Sms.TYPE);
421             mSmsColThreadId = c.getColumnIndex(Sms.THREAD_ID);
422         }
423 
setMmsColumns(Cursor c)424         public void setMmsColumns(Cursor c) {
425             mMmsColId = c.getColumnIndex(BaseColumns._ID);
426             mMmsColFolder = c.getColumnIndex(Mms.MESSAGE_BOX);
427             mMmsColRead = c.getColumnIndex(Mms.READ);
428             mMmsColAttachmentSize = c.getColumnIndex(Mms.MESSAGE_SIZE);
429             mMmsColTextOnly = c.getColumnIndex(Mms.TEXT_ONLY);
430             mMmsColSize = c.getColumnIndex(Mms.MESSAGE_SIZE);
431             mMmsColDate = c.getColumnIndex(Mms.DATE);
432             mMmsColSubject = c.getColumnIndex(Mms.SUBJECT);
433             mMmsColThreadId = c.getColumnIndex(Mms.THREAD_ID);
434         }
435     }
436 
BluetoothMapContent( final Context context, BluetoothMapAccountItem account, BluetoothMapMasInstance mas)437     public BluetoothMapContent(
438             final Context context, BluetoothMapAccountItem account, BluetoothMapMasInstance mas) {
439         mContext = context;
440         mResolver = mContext.getContentResolver();
441         mMasInstance = mas;
442         if (mResolver == null) {
443             Log.d(TAG, "getContentResolver failed");
444         }
445 
446         if (account != null) {
447             mBaseUri = account.mBase_uri + "/";
448             mAccount = account;
449         } else {
450             mBaseUri = null;
451             mAccount = null;
452         }
453     }
454 
close(Closeable c)455     private static void close(Closeable c) {
456         try {
457             if (c != null) {
458                 c.close();
459             }
460         } catch (IOException e) {
461             ContentProfileErrorReportUtils.report(
462                     BluetoothProfile.MAP,
463                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
464                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
465                     0);
466             Log.w(TAG, e);
467         }
468     }
469 
setProtected( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)470     private static void setProtected(
471             BluetoothMapMessageListingElement e,
472             Cursor c,
473             FilterInfo fi,
474             BluetoothMapAppParams ap) {
475         if ((ap.getParameterMask() & MASK_PROTECTED) != 0) {
476             String protect = "no";
477             if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
478                 int flagProtected = c.getInt(fi.mMessageColProtected);
479                 if (flagProtected == 1) {
480                     protect = "yes";
481                 }
482             }
483             Log.v(TAG, "setProtected: " + protect + "\n");
484             e.setProtect(protect);
485         }
486     }
487 
setThreadId( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)488     private static void setThreadId(
489             BluetoothMapMessageListingElement e,
490             Cursor c,
491             FilterInfo fi,
492             BluetoothMapAppParams ap) {
493         if ((ap.getParameterMask() & MASK_CONVERSATION_ID) != 0) {
494             long threadId = 0;
495             TYPE type = TYPE.SMS_GSM; // Just used for handle encoding
496             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
497                 threadId = c.getLong(fi.mSmsColThreadId);
498             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
499                 threadId = c.getLong(fi.mMmsColThreadId);
500                 type = TYPE.MMS; // Just used for handle encoding
501             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
502                 threadId = c.getLong(fi.mMessageColThreadId);
503                 type = TYPE.EMAIL; // Just used for handle encoding
504             }
505             Log.v(TAG, "setThreadId: " + threadId + "\n");
506             e.setThreadId(threadId, type);
507         }
508     }
509 
setThreadName( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)510     private static void setThreadName(
511             BluetoothMapMessageListingElement e,
512             Cursor c,
513             FilterInfo fi,
514             BluetoothMapAppParams ap) {
515         // TODO: Maybe this should be valid for SMS/MMS
516         if ((ap.getParameterMask() & MASK_CONVERSATION_NAME) != 0) {
517             if (fi.mMsgType == FilterInfo.TYPE_IM) {
518                 String threadName = c.getString(fi.mMessageColThreadName);
519                 e.setThreadName(threadName);
520                 Log.v(TAG, "setThreadName: " + threadName + "\n");
521             }
522         }
523     }
524 
setSent( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)525     private static void setSent(
526             BluetoothMapMessageListingElement e,
527             Cursor c,
528             FilterInfo fi,
529             BluetoothMapAppParams ap) {
530         if ((ap.getParameterMask() & MASK_SENT) != 0) {
531             int msgType = 0;
532             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
533                 msgType = c.getInt(fi.mSmsColFolder);
534             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
535                 msgType = c.getInt(fi.mMmsColFolder);
536             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
537                 msgType = c.getInt(fi.mMessageColFolder);
538             }
539             String sent = null;
540             if (msgType == 2) {
541                 sent = "yes";
542             } else {
543                 sent = "no";
544             }
545             Log.v(TAG, "setSent: " + sent);
546             e.setSent(sent);
547         }
548     }
549 
setRead( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)550     private static void setRead(
551             BluetoothMapMessageListingElement e,
552             Cursor c,
553             FilterInfo fi,
554             BluetoothMapAppParams ap) {
555         int read = 0;
556         if (fi.mMsgType == FilterInfo.TYPE_SMS) {
557             read = c.getInt(fi.mSmsColRead);
558         } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
559             read = c.getInt(fi.mMmsColRead);
560         } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
561             read = c.getInt(fi.mMessageColRead);
562         }
563         String setRead = null;
564 
565         Log.v(TAG, "setRead: " + setRead);
566         e.setRead((read == 1), ((ap.getParameterMask() & MASK_READ) != 0));
567     }
568 
setPriority( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)569     private static void setPriority(
570             BluetoothMapMessageListingElement e,
571             Cursor c,
572             FilterInfo fi,
573             BluetoothMapAppParams ap) {
574         if ((ap.getParameterMask() & MASK_PRIORITY) != 0) {
575             String priority = "no";
576             if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
577                 int highPriority = c.getInt(fi.mMessageColPriority);
578                 if (highPriority == 1) {
579                     priority = "yes";
580                 }
581             }
582             int pri = 0;
583             if (fi.mMsgType == FilterInfo.TYPE_MMS) {
584                 pri = c.getInt(c.getColumnIndex(Mms.PRIORITY));
585             }
586             if (pri == PduHeaders.PRIORITY_HIGH) {
587                 priority = "yes";
588             }
589             Log.v(TAG, "setPriority: " + priority);
590             e.setPriority(priority);
591         }
592     }
593 
594     /**
595      * For SMS we set the attachment size to 0, as all data will be text data, hence attachments for
596      * SMS is not possible. For MMS all data is actually attachments, hence we do set the attachment
597      * size to the total message size. To provide a more accurate attachment size, one could extract
598      * the length (in bytes) of the text parts.
599      */
600     @VisibleForTesting
setAttachment( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)601     void setAttachment(
602             BluetoothMapMessageListingElement e,
603             Cursor c,
604             FilterInfo fi,
605             BluetoothMapAppParams ap) {
606         if ((ap.getParameterMask() & MASK_ATTACHMENT_SIZE) != 0) {
607             int size = 0;
608             String attachmentMimeTypes = null;
609             if (fi.mMsgType == FilterInfo.TYPE_MMS) {
610                 if (c.getInt(fi.mMmsColTextOnly) == 0) {
611                     size = c.getInt(fi.mMmsColAttachmentSize);
612                     if (size <= 0) {
613                         // We know there are attachments, since it is not TextOnly
614                         // Hence the size in the database must be wrong.
615                         // Set size to 1 to indicate to the client, that attachments are present
616                         Log.d(
617                                 TAG,
618                                 "Error in message database, size reported as: "
619                                         + size
620                                         + " Changing size to 1");
621                         size = 1;
622                     }
623                     // TODO: Add handling of attachment mime types
624                 }
625             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
626                 int attachment = c.getInt(fi.mMessageColAttachment);
627                 size = c.getInt(fi.mMessageColAttachmentSize);
628                 if (attachment == 1 && size == 0) {
629                     Log.d(
630                             TAG,
631                             "Error in message database, attachment size reported as: "
632                                     + size
633                                     + " Changing size to 1");
634                     size = 1; /* Ensure we indicate we have attachments in the size, if the
635                                  message has attachments, in case the e-mail client do not
636                                  report a size */
637                 }
638             } else if (fi.mMsgType == FilterInfo.TYPE_IM) {
639                 int attachment = c.getInt(fi.mMessageColAttachment);
640                 size = c.getInt(fi.mMessageColAttachmentSize);
641                 if (attachment == 1 && size == 0) {
642                     size = 1; /* Ensure we indicate we have attachments in the size, it the
643                                   message has attachments, in case the e-mail client do not
644                                   report a size */
645                     attachmentMimeTypes = c.getString(fi.mMessageColAttachmentMime);
646                 }
647             }
648             Log.v(
649                     TAG,
650                     "setAttachmentSize: "
651                             + size
652                             + "\n"
653                             + "setAttachmentMimeTypes: "
654                             + attachmentMimeTypes);
655             e.setAttachmentSize(size);
656 
657             if ((mMsgListingVersion > BluetoothMapUtils.MAP_MESSAGE_LISTING_FORMAT_V10)
658                     && ((ap.getParameterMask() & MASK_ATTACHMENT_MIME) != 0)) {
659                 e.setAttachmentMimeTypes(attachmentMimeTypes);
660             }
661         }
662     }
663 
setText( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)664     private void setText(
665             BluetoothMapMessageListingElement e,
666             Cursor c,
667             FilterInfo fi,
668             BluetoothMapAppParams ap) {
669         if ((ap.getParameterMask() & MASK_TEXT) != 0) {
670             String hasText = "";
671             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
672                 hasText = "yes";
673             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
674                 int textOnly = c.getInt(fi.mMmsColTextOnly);
675                 if (textOnly == 1) {
676                     hasText = "yes";
677                 } else {
678                     long id = c.getLong(fi.mMmsColId);
679                     String text = getTextPartsMms(mResolver, id);
680                     if (text != null && text.length() > 0) {
681                         hasText = "yes";
682                     } else {
683                         hasText = "no";
684                     }
685                 }
686             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
687                 hasText = "yes";
688             }
689             Log.v(TAG, "setText: " + hasText);
690             e.setText(hasText);
691         }
692     }
693 
setReceptionStatus( BluetoothMapMessageListingElement e, BluetoothMapAppParams ap)694     private static void setReceptionStatus(
695             BluetoothMapMessageListingElement e, BluetoothMapAppParams ap) {
696         if ((ap.getParameterMask() & MASK_RECEPTION_STATUS) != 0) {
697             String status = "complete";
698             Log.v(TAG, "setReceptionStatus: " + status);
699             e.setReceptionStatus(status);
700         }
701     }
702 
703     @VisibleForTesting
setDeliveryStatus( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)704     void setDeliveryStatus(
705             BluetoothMapMessageListingElement e,
706             Cursor c,
707             FilterInfo fi,
708             BluetoothMapAppParams ap) {
709         if ((ap.getParameterMask() & MASK_DELIVERY_STATUS) != 0) {
710             String deliveryStatus = "delivered";
711             // TODO: Should be handled for SMS and MMS as well
712             if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
713                 deliveryStatus = c.getString(fi.mMessageColDelivery);
714             }
715             Log.v(TAG, "setDeliveryStatus: " + deliveryStatus);
716             e.setDeliveryStatus(deliveryStatus);
717         }
718     }
719 
setSize( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)720     private void setSize(
721             BluetoothMapMessageListingElement e,
722             Cursor c,
723             FilterInfo fi,
724             BluetoothMapAppParams ap) {
725         if ((ap.getParameterMask() & MASK_SIZE) != 0) {
726             int size = 0;
727             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
728                 String subject = c.getString(fi.mSmsColSubject);
729                 size = subject.length();
730             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
731                 size = c.getInt(fi.mMmsColSize);
732                 // MMS complete size = attachment_size + subject length
733                 String subject = e.getSubject();
734                 if (subject == null || subject.length() == 0) {
735                     // Handle setSubject if not done case
736                     setSubject(e, c, fi, ap);
737                 }
738                 if (subject != null && subject.length() != 0) {
739                     size += subject.length();
740                 }
741             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
742                 size = c.getInt(fi.mMessageColSize);
743             }
744             if (size <= 0) {
745                 // A message cannot have size 0
746                 // Hence the size in the database must be wrong.
747                 // Set size to 1 to indicate to the client, that the message has content.
748                 Log.d(
749                         TAG,
750                         "Error in message database, size reported as: "
751                                 + size
752                                 + " Changing size to 1");
753                 size = 1;
754             }
755             Log.v(TAG, "setSize: " + size);
756             e.setSize(size);
757         }
758     }
759 
getType(FilterInfo fi)760     private static TYPE getType(FilterInfo fi) {
761         TYPE type = null;
762         Log.d(TAG, "getType: for filterMsgType" + fi.mMsgType);
763         if (fi.mMsgType == FilterInfo.TYPE_SMS) {
764             Log.d(TAG, "getType: phoneType for SMS " + fi.mPhoneType);
765             if (fi.mPhoneType == TelephonyManager.PHONE_TYPE_CDMA) {
766                 type = TYPE.SMS_CDMA;
767             } else {
768                 type = TYPE.SMS_GSM;
769             }
770         } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
771             type = TYPE.MMS;
772         } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
773             type = TYPE.EMAIL;
774         } else if (fi.mMsgType == FilterInfo.TYPE_IM) {
775             type = TYPE.IM;
776         }
777         Log.v(TAG, "getType: " + type);
778 
779         return type;
780     }
781 
782     @VisibleForTesting
getRecipientNameEmail(Cursor c, FilterInfo fi)783     String getRecipientNameEmail(Cursor c, FilterInfo fi) {
784 
785         String toAddress, ccAddress, bccAddress;
786         toAddress = c.getString(fi.mMessageColToAddress);
787         ccAddress = c.getString(fi.mMessageColCcAddress);
788         bccAddress = c.getString(fi.mMessageColBccAddress);
789 
790         StringBuilder sb = new StringBuilder();
791         if (toAddress != null) {
792             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(toAddress);
793             if (tokens.length != 0) {
794                 Log.d(TAG, "toName count= " + tokens.length);
795                 int i = 0;
796                 boolean first = true;
797                 while (i < tokens.length) {
798                     Log.v(TAG, "ToName = " + tokens[i].toString());
799                     String name = tokens[i].getName();
800                     if (!first) {
801                         sb.append("; "); // Delimiter
802                     }
803                     sb.append(name);
804                     first = false;
805                     i++;
806                 }
807             }
808 
809             if (ccAddress != null) {
810                 sb.append("; ");
811             }
812         }
813         if (ccAddress != null) {
814             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(ccAddress);
815             if (tokens.length != 0) {
816                 Log.d(TAG, "ccName count= " + tokens.length);
817                 int i = 0;
818                 boolean first = true;
819                 while (i < tokens.length) {
820                     Log.v(TAG, "ccName = " + tokens[i].toString());
821                     String name = tokens[i].getName();
822                     if (!first) {
823                         sb.append("; "); // Delimiter
824                     }
825                     sb.append(name);
826                     first = false;
827                     i++;
828                 }
829             }
830             if (bccAddress != null) {
831                 sb.append("; ");
832             }
833         }
834         if (bccAddress != null) {
835             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(bccAddress);
836             if (tokens.length != 0) {
837                 Log.d(TAG, "bccName count= " + tokens.length);
838                 int i = 0;
839                 boolean first = true;
840                 while (i < tokens.length) {
841                     Log.v(TAG, "bccName = " + tokens[i].toString());
842                     String name = tokens[i].getName();
843                     if (!first) {
844                         sb.append("; "); // Delimiter
845                     }
846                     sb.append(name);
847                     first = false;
848                     i++;
849                 }
850             }
851         }
852         return sb.toString();
853     }
854 
855     @VisibleForTesting
getRecipientAddressingEmail(Cursor c, FilterInfo fi)856     String getRecipientAddressingEmail(Cursor c, FilterInfo fi) {
857         String toAddress, ccAddress, bccAddress;
858         toAddress = c.getString(fi.mMessageColToAddress);
859         ccAddress = c.getString(fi.mMessageColCcAddress);
860         bccAddress = c.getString(fi.mMessageColBccAddress);
861 
862         StringBuilder sb = new StringBuilder();
863         if (toAddress != null) {
864             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(toAddress);
865             if (tokens.length != 0) {
866                 Log.d(TAG, "toAddress count= " + tokens.length);
867                 int i = 0;
868                 boolean first = true;
869                 while (i < tokens.length) {
870                     Log.v(TAG, "ToAddress = " + tokens[i].toString());
871                     String email = tokens[i].getAddress();
872                     if (!first) {
873                         sb.append("; "); // Delimiter
874                     }
875                     sb.append(email);
876                     first = false;
877                     i++;
878                 }
879             }
880 
881             if (ccAddress != null) {
882                 sb.append("; ");
883             }
884         }
885         if (ccAddress != null) {
886             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(ccAddress);
887             if (tokens.length != 0) {
888                 Log.d(TAG, "ccAddress count= " + tokens.length);
889                 int i = 0;
890                 boolean first = true;
891                 while (i < tokens.length) {
892                     Log.v(TAG, "ccAddress = " + tokens[i].toString());
893                     String email = tokens[i].getAddress();
894                     if (!first) {
895                         sb.append("; "); // Delimiter
896                     }
897                     sb.append(email);
898                     first = false;
899                     i++;
900                 }
901             }
902             if (bccAddress != null) {
903                 sb.append("; ");
904             }
905         }
906         if (bccAddress != null) {
907             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(bccAddress);
908             if (tokens.length != 0) {
909                 Log.d(TAG, "bccAddress count= " + tokens.length);
910                 int i = 0;
911                 boolean first = true;
912                 while (i < tokens.length) {
913                     Log.v(TAG, "bccAddress = " + tokens[i].toString());
914                     String email = tokens[i].getAddress();
915                     if (!first) {
916                         sb.append("; "); // Delimiter
917                     }
918                     sb.append(email);
919                     first = false;
920                     i++;
921                 }
922             }
923         }
924         return sb.toString();
925     }
926 
927     @VisibleForTesting
setRecipientAddressing( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)928     void setRecipientAddressing(
929             BluetoothMapMessageListingElement e,
930             Cursor c,
931             FilterInfo fi,
932             BluetoothMapAppParams ap) {
933         if ((ap.getParameterMask() & MASK_RECIPIENT_ADDRESSING) != 0) {
934             String address = null;
935             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
936                 int msgType = c.getInt(fi.mSmsColType);
937                 if (msgType == Sms.MESSAGE_TYPE_INBOX) {
938                     address = fi.mPhoneNum;
939                 } else {
940                     address = c.getString(c.getColumnIndex(Sms.ADDRESS));
941                 }
942                 if ((address == null) && msgType == Sms.MESSAGE_TYPE_DRAFT) {
943                     // Fetch address for Drafts folder from "canonical_address" table
944                     int threadIdInd = c.getColumnIndex(Sms.THREAD_ID);
945                     String threadIdStr = c.getString(threadIdInd);
946                     // If a draft message has no recipient, it has no thread ID
947                     // hence threadIdStr could possibly be null
948                     if (threadIdStr != null) {
949                         address = getCanonicalAddressSms(mResolver, Integer.valueOf(threadIdStr));
950                     }
951                     Log.v(TAG, "threadId = " + threadIdStr + " address:" + address + "\n");
952                 }
953             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
954                 long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
955                 address = getAddressMms(mResolver, id, MMS_TO);
956             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
957                 /* Might be another way to handle addresses */
958                 address = getRecipientAddressingEmail(c, fi);
959             }
960             Log.v(TAG, "setRecipientAddressing: " + address);
961             if (address == null) {
962                 address = "";
963             }
964             e.setRecipientAddressing(address);
965         }
966     }
967 
setRecipientName( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)968     private void setRecipientName(
969             BluetoothMapMessageListingElement e,
970             Cursor c,
971             FilterInfo fi,
972             BluetoothMapAppParams ap) {
973         if ((ap.getParameterMask() & MASK_RECIPIENT_NAME) != 0) {
974             String name = null;
975             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
976                 int msgType = c.getInt(fi.mSmsColType);
977                 if (msgType != 1) {
978                     String phone = c.getString(fi.mSmsColAddress);
979                     if (phone != null && !phone.isEmpty()) {
980                         name = getContactNameFromPhone(phone, mResolver);
981                     }
982                 } else {
983                     name = fi.mPhoneAlphaTag;
984                 }
985             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
986                 long id = c.getLong(fi.mMmsColId);
987                 String phone;
988                 if (e.getRecipientAddressing() != null) {
989                     phone = getAddressMms(mResolver, id, MMS_TO);
990                 } else {
991                     phone = e.getRecipientAddressing();
992                 }
993                 if (phone != null && !phone.isEmpty()) {
994                     name = getContactNameFromPhone(phone, mResolver);
995                 }
996             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
997                 /* Might be another way to handle address and names */
998                 name = getRecipientNameEmail(c, fi);
999             }
1000             Log.v(TAG, "setRecipientName: " + name);
1001             if (name == null) {
1002                 name = "";
1003             }
1004             e.setRecipientName(name);
1005         }
1006     }
1007 
1008     @VisibleForTesting
setSenderAddressing( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1009     void setSenderAddressing(
1010             BluetoothMapMessageListingElement e,
1011             Cursor c,
1012             FilterInfo fi,
1013             BluetoothMapAppParams ap) {
1014         if ((ap.getParameterMask() & MASK_SENDER_ADDRESSING) != 0) {
1015             String address = "";
1016             String tempAddress;
1017             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1018                 int msgType = c.getInt(fi.mSmsColType);
1019                 if (msgType == 1) { // INBOX
1020                     tempAddress = c.getString(fi.mSmsColAddress);
1021                 } else {
1022                     tempAddress = fi.mPhoneNum;
1023                 }
1024                 if (tempAddress == null) {
1025                     /* This can only happen on devices with no SIM -
1026                     hence will typically not have any SMS messages. */
1027                 } else {
1028                     address = PhoneNumberUtils.extractNetworkPortion(tempAddress);
1029                     /* extractNetworkPortion can return N if the number is a service "number" =
1030                      * a string with the a name in (i.e. "Some-Tele-company" would return N
1031                      * because of the N in compaNy)
1032                      * Hence we need to check if the number is actually a string with alpha chars.
1033                      * */
1034                     Boolean alpha =
1035                             PhoneNumberUtils.stripSeparators(tempAddress)
1036                                     .matches("[0-9]*[a-zA-Z]+[0-9]*");
1037 
1038                     if (address == null || address.length() < 2 || alpha) {
1039                         address = tempAddress; // if the number is a service ascii text just use it
1040                     }
1041                 }
1042             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1043                 long id = c.getLong(fi.mMmsColId);
1044                 tempAddress = getAddressMms(mResolver, id, MMS_FROM);
1045                 address = PhoneNumberUtils.extractNetworkPortion(tempAddress);
1046                 if (address == null || address.length() < 1) {
1047                     address = tempAddress; // if the number is a service ascii text just use it
1048                 }
1049             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL /* ||
1050                        fi.mMsgType == FilterInfo.TYPE_IM*/) {
1051                 String nameEmail = c.getString(fi.mMessageColFromAddress);
1052                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(nameEmail);
1053                 Log.d(TAG, "Originator count= " + tokens.length);
1054                 address =
1055                         Arrays.stream(tokens)
1056                                 .peek(token -> Log.v(TAG, "SenderAddress=" + token))
1057                                 .map(Rfc822Token::getAddress)
1058                                 .collect(Collectors.joining("; "));
1059             } else if (fi.mMsgType == FilterInfo.TYPE_IM) {
1060                 // TODO: For IM we add the contact ID in the addressing
1061                 long contactId = c.getLong(fi.mMessageColFromAddress);
1062                 // TODO: This is a BAD hack, that we map the contact ID to a conversation ID!!!
1063                 //       We need to reach a conclusion on what to do
1064                 Uri contactsUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_CONVOCONTACT);
1065                 Cursor contacts =
1066                         BluetoothMethodProxy.getInstance()
1067                                 .contentResolverQuery(
1068                                         mResolver,
1069                                         contactsUri,
1070                                         BluetoothMapContract.BT_CONTACT_PROJECTION,
1071                                         BluetoothMapContract.ConvoContactColumns.CONVO_ID
1072                                                 + " = "
1073                                                 + contactId,
1074                                         null,
1075                                         null);
1076                 try {
1077                     // TODO this will not work for group-chats
1078                     if (contacts != null && contacts.moveToFirst()) {
1079                         address =
1080                                 contacts.getString(
1081                                         contacts.getColumnIndex(
1082                                                 BluetoothMapContract.ConvoContactColumns.UCI));
1083                     }
1084                 } finally {
1085                     if (contacts != null) {
1086                         contacts.close();
1087                     }
1088                 }
1089             }
1090             Log.v(TAG, "setSenderAddressing: " + address);
1091             if (address == null) {
1092                 address = "";
1093             }
1094             e.setSenderAddressing(address);
1095         }
1096     }
1097 
1098     @VisibleForTesting
setSenderName( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1099     void setSenderName(
1100             BluetoothMapMessageListingElement e,
1101             Cursor c,
1102             FilterInfo fi,
1103             BluetoothMapAppParams ap) {
1104         if ((ap.getParameterMask() & MASK_SENDER_NAME) != 0) {
1105             String name = "";
1106             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1107                 int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
1108                 if (msgType == 1) {
1109                     String phone = c.getString(fi.mSmsColAddress);
1110                     if (phone != null && !phone.isEmpty()) {
1111                         name = getContactNameFromPhone(phone, mResolver);
1112                     }
1113                 } else {
1114                     name = fi.mPhoneAlphaTag;
1115                 }
1116             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1117                 long id = c.getLong(fi.mMmsColId);
1118                 String phone;
1119                 if (e.getSenderAddressing() != null) {
1120                     phone = getAddressMms(mResolver, id, MMS_FROM);
1121                 } else {
1122                     phone = e.getSenderAddressing();
1123                 }
1124                 if (phone != null && !phone.isEmpty()) {
1125                     name = getContactNameFromPhone(phone, mResolver);
1126                 }
1127             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL /*  ||
1128                        fi.mMsgType == FilterInfo.TYPE_IM*/) {
1129                 String nameEmail = c.getString(fi.mMessageColFromAddress);
1130                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(nameEmail);
1131                 Log.d(TAG, "Originator count= " + tokens.length);
1132                 name =
1133                         Arrays.stream(tokens)
1134                                 .peek(token -> Log.v(TAG, "SenderName=" + token))
1135                                 .map(Rfc822Token::getName)
1136                                 .collect(Collectors.joining("; "));
1137             } else if (fi.mMsgType == FilterInfo.TYPE_IM) {
1138                 // For IM we add the contact ID in the addressing
1139                 long contactId = c.getLong(fi.mMessageColFromAddress);
1140                 Uri contactsUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_CONVOCONTACT);
1141                 Cursor contacts =
1142                         BluetoothMethodProxy.getInstance()
1143                                 .contentResolverQuery(
1144                                         mResolver,
1145                                         contactsUri,
1146                                         BluetoothMapContract.BT_CONTACT_PROJECTION,
1147                                         BluetoothMapContract.ConvoContactColumns.CONVO_ID
1148                                                 + " = "
1149                                                 + contactId,
1150                                         null,
1151                                         null);
1152                 try {
1153                     // TODO this will not work for group-chats
1154                     if (contacts != null && contacts.moveToFirst()) {
1155                         name =
1156                                 contacts.getString(
1157                                         contacts.getColumnIndex(
1158                                                 BluetoothMapContract.ConvoContactColumns.NAME));
1159                     }
1160                 } finally {
1161                     if (contacts != null) {
1162                         contacts.close();
1163                     }
1164                 }
1165             }
1166             Log.v(TAG, "setSenderName: " + name);
1167             if (name == null) {
1168                 name = "";
1169             }
1170             e.setSenderName(name);
1171         }
1172     }
1173 
1174     @VisibleForTesting
setDateTime( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1175     void setDateTime(
1176             BluetoothMapMessageListingElement e,
1177             Cursor c,
1178             FilterInfo fi,
1179             BluetoothMapAppParams ap) {
1180         if ((ap.getParameterMask() & MASK_DATETIME) != 0) {
1181             long date = 0;
1182             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1183                 date = c.getLong(fi.mSmsColDate);
1184             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1185                 /* Use Mms.DATE for all messages. Although contract class states */
1186                 /* Mms.DATE_SENT are for outgoing messages. But that is not working. */
1187                 date = c.getLong(fi.mMmsColDate) * 1000L;
1188 
1189                 /* int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); */
1190                 /* if (msgBox == Mms.MESSAGE_BOX_INBOX) { */
1191                 /*     date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L; */
1192                 /* } else { */
1193                 /*     date = c.getLong(c.getColumnIndex(Mms.DATE_SENT)) * 1000L; */
1194                 /* } */
1195             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1196                 date = c.getLong(fi.mMessageColDate);
1197             }
1198             e.setDateTime(date);
1199         }
1200     }
1201 
1202     @VisibleForTesting
setLastActivity(BluetoothMapConvoListingElement e, Cursor c, FilterInfo fi)1203     void setLastActivity(BluetoothMapConvoListingElement e, Cursor c, FilterInfo fi) {
1204         long date = 0;
1205         if (fi.mMsgType == FilterInfo.TYPE_SMS || fi.mMsgType == FilterInfo.TYPE_MMS) {
1206             date = c.getLong(MMS_SMS_THREAD_COL_DATE);
1207         } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1208             date = c.getLong(fi.mConvoColLastActivity);
1209         }
1210         e.setLastActivity(date);
1211         Log.v(TAG, "setDateTime: " + e.getLastActivityString());
1212     }
1213 
getTextPartsMms(ContentResolver r, long id)1214     public static String getTextPartsMms(ContentResolver r, long id) {
1215         StringBuilder textBuilder = new StringBuilder();
1216         String selection = new String("mid=" + id);
1217         String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/part");
1218         Uri uriAddress = Uri.parse(uriStr);
1219         // TODO: maybe use a projection with only "ct" and "text"
1220         Cursor c =
1221                 BluetoothMethodProxy.getInstance()
1222                         .contentResolverQuery(r, uriAddress, null, selection, null, null);
1223         try {
1224             if (c != null && c.moveToFirst()) {
1225                 do {
1226                     String ct = c.getString(c.getColumnIndex("ct"));
1227                     if (ct.equals("text/plain")) {
1228                         String part = c.getString(c.getColumnIndex("text"));
1229                         if (part != null) {
1230                             textBuilder.append(part);
1231                         }
1232                     }
1233                 } while (c.moveToNext());
1234             }
1235         } finally {
1236             if (c != null) {
1237                 c.close();
1238             }
1239         }
1240 
1241         return textBuilder.toString();
1242     }
1243 
setSubject( BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1244     private void setSubject(
1245             BluetoothMapMessageListingElement e,
1246             Cursor c,
1247             FilterInfo fi,
1248             BluetoothMapAppParams ap) {
1249         String subject = "";
1250         int subLength = ap.getSubjectLength();
1251         if (subLength == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
1252             subLength = 256;
1253         }
1254 
1255         // Fix Subject Display issue with HONDA Carkit - Ignore subject Mask.
1256         boolean isHondaCarkit;
1257         if (Utils.isInstrumentationTestMode()) {
1258             isHondaCarkit = false;
1259         } else {
1260             isHondaCarkit =
1261                     DeviceWorkArounds.addressStartsWith(
1262                             BluetoothMapService.getBluetoothMapService()
1263                                     .getRemoteDevice()
1264                                     .getAddress(),
1265                             DeviceWorkArounds.HONDA_CARKIT);
1266         }
1267         if (isHondaCarkit || (ap.getParameterMask() & MASK_SUBJECT) != 0) {
1268             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1269                 subject = c.getString(fi.mSmsColSubject);
1270             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1271                 subject = c.getString(fi.mMmsColSubject);
1272                 if (subject == null || subject.length() == 0) {
1273                     /* Get subject from mms text body parts - if any exists */
1274                     long id = c.getLong(fi.mMmsColId);
1275                     subject = getTextPartsMms(mResolver, id);
1276                 }
1277             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1278                 subject = c.getString(fi.mMessageColSubject);
1279             }
1280             if (subject != null && subject.length() > subLength) {
1281                 subject = subject.substring(0, subLength);
1282             } else if (subject == null) {
1283                 subject = "";
1284             }
1285             Log.v(TAG, "setSubject: " + subject);
1286             e.setSubject(subject);
1287         }
1288     }
1289 
setHandle(BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi)1290     private static void setHandle(BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi) {
1291         long handle = -1;
1292         if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1293             handle = c.getLong(fi.mSmsColId);
1294         } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1295             handle = c.getLong(fi.mMmsColId);
1296         } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1297             handle = c.getLong(fi.mMessageColId);
1298         }
1299         Log.v(TAG, "setHandle: " + handle);
1300         e.setHandle(handle);
1301     }
1302 
element( Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1303     private BluetoothMapMessageListingElement element(
1304             Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
1305         BluetoothMapMessageListingElement e = new BluetoothMapMessageListingElement();
1306         setHandle(e, c, fi);
1307         setDateTime(e, c, fi, ap);
1308         e.setType(getType(fi), (ap.getParameterMask() & MASK_TYPE) != 0);
1309         setRead(e, c, fi, ap);
1310         // we set number and name for sender/recipient later
1311         // they require lookup on contacts so no need to
1312         // do it for all elements unless they are to be used.
1313         e.setCursorIndex(c.getPosition());
1314         return e;
1315     }
1316 
createConvoElement(Cursor c, FilterInfo fi)1317     private BluetoothMapConvoListingElement createConvoElement(Cursor c, FilterInfo fi) {
1318         BluetoothMapConvoListingElement e = new BluetoothMapConvoListingElement();
1319         setLastActivity(e, c, fi);
1320         e.setType(getType(fi));
1321         e.setCursorIndex(c.getPosition());
1322         return e;
1323     }
1324 
1325     /* TODO: Change to use SmsMmsContacts.getContactNameFromPhone() with proper use of
1326      *       caching. */
getContactNameFromPhone(String phone, ContentResolver resolver)1327     public static String getContactNameFromPhone(String phone, ContentResolver resolver) {
1328         String name = null;
1329         // Handle possible exception for empty phone address
1330         if (TextUtils.isEmpty(phone)) {
1331             return name;
1332         }
1333 
1334         Uri uri =
1335                 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone));
1336 
1337         String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME};
1338         String selection = Contacts.IN_VISIBLE_GROUP + "=1";
1339         String orderBy = Contacts.DISPLAY_NAME + " ASC";
1340         Cursor c = null;
1341         try {
1342             c =
1343                     BluetoothMethodProxy.getInstance()
1344                             .contentResolverQuery(
1345                                     resolver, uri, projection, selection, null, orderBy);
1346             if (c != null) {
1347                 int colIndex = c.getColumnIndex(Contacts.DISPLAY_NAME);
1348                 if (c.getCount() >= 1) {
1349                     c.moveToFirst();
1350                     name = c.getString(colIndex);
1351                 }
1352             }
1353         } finally {
1354             if (c != null) {
1355                 c.close();
1356             }
1357         }
1358         return name;
1359     }
1360 
1361     private static final String[] RECIPIENT_ID_PROJECTION = {Threads.RECIPIENT_IDS};
1362 
1363     /** Get SMS RecipientAddresses for DRAFT folder based on threadId */
getCanonicalAddressSms(ContentResolver r, int threadId)1364     public static String getCanonicalAddressSms(ContentResolver r, int threadId) {
1365 
1366         /*
1367          1. Get Recipient Ids from Threads.CONTENT_URI
1368          2. Get Recipient Address for corresponding Id from canonical-addresses table.
1369         */
1370 
1371         // Uri sAllCanonical = Uri.parse("content://mms-sms/canonical-addresses");
1372         Uri sAllCanonical =
1373                 MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build();
1374         Uri sAllThreadsUri =
1375                 Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
1376         Cursor cr = null;
1377         String recipientIds = null;
1378         String whereClause = "_id=" + threadId;
1379         Log.v(TAG, "whereClause is " + whereClause);
1380         try {
1381             cr =
1382                     BluetoothMethodProxy.getInstance()
1383                             .contentResolverQuery(
1384                                     r,
1385                                     sAllThreadsUri,
1386                                     RECIPIENT_ID_PROJECTION,
1387                                     whereClause,
1388                                     null,
1389                                     null);
1390             if (cr != null && cr.moveToFirst()) {
1391                 recipientIds = cr.getString(0);
1392                 Log.v(
1393                         TAG,
1394                         "cursor.getCount(): "
1395                                 + cr.getCount()
1396                                 + "recipientIds: "
1397                                 + recipientIds
1398                                 + "selection: "
1399                                 + whereClause);
1400             }
1401         } finally {
1402             if (cr != null) {
1403                 cr.close();
1404                 cr = null;
1405             }
1406         }
1407         Log.v(TAG, "recipientIds with spaces: " + recipientIds + "\n");
1408         StringBuilder recipientAddress = new StringBuilder();
1409         if (recipientIds != null) {
1410             whereClause =
1411                     Arrays.stream(recipientIds.split(" "))
1412                             .map(id -> "_id=" + id)
1413                             .collect(Collectors.joining(" OR "));
1414             Log.v(TAG, "whereClause is " + whereClause);
1415             try {
1416                 cr =
1417                         BluetoothMethodProxy.getInstance()
1418                                 .contentResolverQuery(
1419                                         r, sAllCanonical, null, whereClause, null, null);
1420                 if (cr != null && cr.moveToFirst()) {
1421                     do {
1422                         // TODO: Multiple Recipients are appended with ";" for now.
1423                         if (recipientAddress.length() != 0) {
1424                             recipientAddress.append(";");
1425                         }
1426                         recipientAddress.append(
1427                                 cr.getString(cr.getColumnIndex(CanonicalAddressesColumns.ADDRESS)));
1428                     } while (cr.moveToNext());
1429                 }
1430             } finally {
1431                 if (cr != null) {
1432                     cr.close();
1433                 }
1434             }
1435         }
1436 
1437         Log.v(TAG, "Final recipientAddress: " + recipientAddress.toString());
1438         return recipientAddress.toString();
1439     }
1440 
getAddressMms(ContentResolver r, long id, int type)1441     public static String getAddressMms(ContentResolver r, long id, int type) {
1442         String selection = new String("msg_id=" + id + " AND type=" + type);
1443         String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/addr");
1444         Uri uriAddress = Uri.parse(uriStr);
1445         String addr = null;
1446         String[] projection = {Mms.Addr.ADDRESS};
1447         Cursor c = null;
1448         try {
1449             c =
1450                     BluetoothMethodProxy.getInstance()
1451                             .contentResolverQuery(
1452                                     r,
1453                                     uriAddress,
1454                                     projection,
1455                                     selection,
1456                                     null,
1457                                     null); // TODO: Add projection
1458             int colIndex = c.getColumnIndex(Mms.Addr.ADDRESS);
1459             if (c != null) {
1460                 if (c.moveToFirst()) {
1461                     addr = c.getString(colIndex);
1462                     if (INSERT_ADDRESS_TOKEN.equals(addr)) {
1463                         addr = "";
1464                     }
1465                 }
1466             }
1467         } finally {
1468             if (c != null) {
1469                 c.close();
1470             }
1471         }
1472         return addr;
1473     }
1474 
1475     /**
1476      * Matching functions for originator and recipient for MMS
1477      *
1478      * @return true if found a match
1479      */
matchRecipientMms(Cursor c, String recipient)1480     private boolean matchRecipientMms(Cursor c, String recipient) {
1481         boolean res;
1482         long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1483         String phone = getAddressMms(mResolver, id, MMS_TO);
1484         if (phone != null && phone.length() > 0) {
1485             if (phone.matches(recipient)) {
1486                 Log.v(TAG, "matchRecipientMms: match recipient phone = " + phone);
1487                 res = true;
1488             } else {
1489                 String name = getContactNameFromPhone(phone, mResolver);
1490                 if (name != null && name.length() > 0 && name.matches(recipient)) {
1491                     Log.v(TAG, "matchRecipientMms: match recipient name = " + name);
1492                     res = true;
1493                 } else {
1494                     res = false;
1495                 }
1496             }
1497         } else {
1498             res = false;
1499         }
1500         return res;
1501     }
1502 
matchRecipientSms(Cursor c, FilterInfo fi, String recipient)1503     private boolean matchRecipientSms(Cursor c, FilterInfo fi, String recipient) {
1504         boolean res;
1505         int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
1506         if (msgType == 1) {
1507             String phone = fi.mPhoneNum;
1508             String name = fi.mPhoneAlphaTag;
1509             if (phone != null && phone.length() > 0 && phone.matches(recipient)) {
1510                 Log.v(TAG, "matchRecipientSms: match recipient phone = " + phone);
1511                 res = true;
1512             } else if (name != null && name.length() > 0 && name.matches(recipient)) {
1513                 Log.v(TAG, "matchRecipientSms: match recipient name = " + name);
1514                 res = true;
1515             } else {
1516                 res = false;
1517             }
1518         } else {
1519             String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
1520             if (phone != null && phone.length() > 0) {
1521                 if (phone.matches(recipient)) {
1522                     Log.v(TAG, "matchRecipientSms: match recipient phone = " + phone);
1523                     res = true;
1524                 } else {
1525                     String name = getContactNameFromPhone(phone, mResolver);
1526                     if (name != null && name.length() > 0 && name.matches(recipient)) {
1527                         Log.v(TAG, "matchRecipientSms: match recipient name = " + name);
1528                         res = true;
1529                     } else {
1530                         res = false;
1531                     }
1532                 }
1533             } else {
1534                 res = false;
1535             }
1536         }
1537         return res;
1538     }
1539 
matchRecipient(Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1540     private boolean matchRecipient(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
1541         boolean res;
1542         String recipient = ap.getFilterRecipient();
1543         if (recipient != null && recipient.length() > 0) {
1544             recipient = recipient.replace("*", ".*");
1545             recipient = ".*" + recipient + ".*";
1546             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1547                 res = matchRecipientSms(c, fi, recipient);
1548             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1549                 res = matchRecipientMms(c, recipient);
1550             } else {
1551                 Log.d(TAG, "matchRecipient: Unknown msg type: " + fi.mMsgType);
1552                 res = false;
1553             }
1554         } else {
1555             res = true;
1556         }
1557         return res;
1558     }
1559 
matchOriginatorMms(Cursor c, String orig)1560     private boolean matchOriginatorMms(Cursor c, String orig) {
1561         boolean res;
1562         long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1563         String phone = getAddressMms(mResolver, id, MMS_FROM);
1564         if (phone != null && phone.length() > 0) {
1565             if (phone.matches(orig)) {
1566                 Log.v(TAG, "matchOriginatorMms: match originator phone = " + phone);
1567                 res = true;
1568             } else {
1569                 String name = getContactNameFromPhone(phone, mResolver);
1570                 if (name != null && name.length() > 0 && name.matches(orig)) {
1571                     Log.v(TAG, "matchOriginatorMms: match originator name = " + name);
1572                     res = true;
1573                 } else {
1574                     res = false;
1575                 }
1576             }
1577         } else {
1578             res = false;
1579         }
1580         return res;
1581     }
1582 
matchOriginatorSms(Cursor c, FilterInfo fi, String orig)1583     private boolean matchOriginatorSms(Cursor c, FilterInfo fi, String orig) {
1584         boolean res;
1585         int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
1586         if (msgType == 1) {
1587             String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
1588             if (phone != null && phone.length() > 0) {
1589                 if (phone.matches(orig)) {
1590                     Log.v(TAG, "matchOriginatorSms: match originator phone = " + phone);
1591                     res = true;
1592                 } else {
1593                     String name = getContactNameFromPhone(phone, mResolver);
1594                     if (name != null && name.length() > 0 && name.matches(orig)) {
1595                         Log.v(TAG, "matchOriginatorSms: match originator name = " + name);
1596                         res = true;
1597                     } else {
1598                         res = false;
1599                     }
1600                 }
1601             } else {
1602                 res = false;
1603             }
1604         } else {
1605             String phone = fi.mPhoneNum;
1606             String name = fi.mPhoneAlphaTag;
1607             if (phone != null && phone.length() > 0 && phone.matches(orig)) {
1608                 Log.v(TAG, "matchOriginatorSms: match originator phone = " + phone);
1609                 res = true;
1610             } else if (name != null && name.length() > 0 && name.matches(orig)) {
1611                 Log.v(TAG, "matchOriginatorSms: match originator name = " + name);
1612                 res = true;
1613             } else {
1614                 res = false;
1615             }
1616         }
1617         return res;
1618     }
1619 
matchOriginator(Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1620     private boolean matchOriginator(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
1621         boolean res;
1622         String orig = ap.getFilterOriginator();
1623         if (orig != null && orig.length() > 0) {
1624             orig = orig.replace("*", ".*");
1625             orig = ".*" + orig + ".*";
1626             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1627                 res = matchOriginatorSms(c, fi, orig);
1628             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1629                 res = matchOriginatorMms(c, orig);
1630             } else {
1631                 Log.d(TAG, "matchOriginator: Unknown msg type: " + fi.mMsgType);
1632                 res = false;
1633             }
1634         } else {
1635             res = true;
1636         }
1637         return res;
1638     }
1639 
matchAddresses(Cursor c, FilterInfo fi, BluetoothMapAppParams ap)1640     private boolean matchAddresses(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
1641         return matchOriginator(c, fi, ap) && matchRecipient(c, fi, ap);
1642     }
1643 
1644     /* Where filter functions */
setWhereFilterFolderTypeSms(String folder)1645     private static String setWhereFilterFolderTypeSms(String folder) {
1646         String where = "";
1647         if (BluetoothMapContract.FOLDER_NAME_INBOX.equalsIgnoreCase(folder)) {
1648             where = Sms.TYPE + " = 1 AND " + Sms.THREAD_ID + " <> -1";
1649         } else if (BluetoothMapContract.FOLDER_NAME_OUTBOX.equalsIgnoreCase(folder)) {
1650             where =
1651                     "("
1652                             + Sms.TYPE
1653                             + " = 4 OR "
1654                             + Sms.TYPE
1655                             + " = 5 OR "
1656                             + Sms.TYPE
1657                             + " = 6) AND "
1658                             + Sms.THREAD_ID
1659                             + " <> -1";
1660         } else if (BluetoothMapContract.FOLDER_NAME_SENT.equalsIgnoreCase(folder)) {
1661             where = Sms.TYPE + " = 2 AND " + Sms.THREAD_ID + " <> -1";
1662         } else if (BluetoothMapContract.FOLDER_NAME_DRAFT.equalsIgnoreCase(folder)) {
1663             where =
1664                     Sms.TYPE
1665                             + " = 3 AND "
1666                             + "("
1667                             + Sms.THREAD_ID
1668                             + " IS NULL OR "
1669                             + Sms.THREAD_ID
1670                             + " <> -1 )";
1671         } else if (BluetoothMapContract.FOLDER_NAME_DELETED.equalsIgnoreCase(folder)) {
1672             where = Sms.THREAD_ID + " = -1";
1673         }
1674 
1675         return where;
1676     }
1677 
setWhereFilterFolderTypeMms(String folder)1678     private static String setWhereFilterFolderTypeMms(String folder) {
1679         String where = "";
1680         if (BluetoothMapContract.FOLDER_NAME_INBOX.equalsIgnoreCase(folder)) {
1681             where = Mms.MESSAGE_BOX + " = 1 AND " + Mms.THREAD_ID + " <> -1";
1682         } else if (BluetoothMapContract.FOLDER_NAME_OUTBOX.equalsIgnoreCase(folder)) {
1683             where = Mms.MESSAGE_BOX + " = 4 AND " + Mms.THREAD_ID + " <> -1";
1684         } else if (BluetoothMapContract.FOLDER_NAME_SENT.equalsIgnoreCase(folder)) {
1685             where = Mms.MESSAGE_BOX + " = 2 AND " + Mms.THREAD_ID + " <> -1";
1686         } else if (BluetoothMapContract.FOLDER_NAME_DRAFT.equalsIgnoreCase(folder)) {
1687             where =
1688                     Mms.MESSAGE_BOX
1689                             + " = 3 AND "
1690                             + "("
1691                             + Mms.THREAD_ID
1692                             + " IS NULL OR "
1693                             + Mms.THREAD_ID
1694                             + " <> -1 )";
1695         } else if (BluetoothMapContract.FOLDER_NAME_DELETED.equalsIgnoreCase(folder)) {
1696             where = Mms.THREAD_ID + " = -1";
1697         }
1698 
1699         return where;
1700     }
1701 
setWhereFilterFolderTypeEmail(long folderId)1702     private static String setWhereFilterFolderTypeEmail(long folderId) {
1703         String where = "";
1704         if (folderId >= 0) {
1705             where = BluetoothMapContract.MessageColumns.FOLDER_ID + " = " + folderId;
1706         } else {
1707             Log.e(TAG, "setWhereFilterFolderTypeEmail: not valid!");
1708             ContentProfileErrorReportUtils.report(
1709                     BluetoothProfile.MAP,
1710                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
1711                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
1712                     1);
1713             throw new IllegalArgumentException("Invalid folder ID");
1714         }
1715         return where;
1716     }
1717 
setWhereFilterFolderTypeIm(long folderId)1718     private static String setWhereFilterFolderTypeIm(long folderId) {
1719         String where = "";
1720         if (folderId > BluetoothMapContract.FOLDER_ID_OTHER) {
1721             where = BluetoothMapContract.MessageColumns.FOLDER_ID + " = " + folderId;
1722         } else {
1723             Log.e(TAG, "setWhereFilterFolderTypeIm: not valid!");
1724             ContentProfileErrorReportUtils.report(
1725                     BluetoothProfile.MAP,
1726                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
1727                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
1728                     2);
1729             throw new IllegalArgumentException("Invalid folder ID");
1730         }
1731         return where;
1732     }
1733 
setWhereFilterFolderType( BluetoothMapFolderElement folderElement, FilterInfo fi)1734     private static String setWhereFilterFolderType(
1735             BluetoothMapFolderElement folderElement, FilterInfo fi) {
1736         String where = "1=1";
1737         if (!folderElement.shouldIgnore()) {
1738             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1739                 where = setWhereFilterFolderTypeSms(folderElement.getName());
1740             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1741                 where = setWhereFilterFolderTypeMms(folderElement.getName());
1742             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
1743                 where = setWhereFilterFolderTypeEmail(folderElement.getFolderId());
1744             } else if (fi.mMsgType == FilterInfo.TYPE_IM) {
1745                 where = setWhereFilterFolderTypeIm(folderElement.getFolderId());
1746             }
1747         }
1748 
1749         return where;
1750     }
1751 
setWhereFilterReadStatus(BluetoothMapAppParams ap, FilterInfo fi)1752     private static String setWhereFilterReadStatus(BluetoothMapAppParams ap, FilterInfo fi) {
1753         String where = "";
1754         if (ap.getFilterReadStatus() != -1) {
1755             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1756                 if ((ap.getFilterReadStatus() & 0x01) != 0) {
1757                     where = " AND " + Sms.READ + "= 0";
1758                 }
1759 
1760                 if ((ap.getFilterReadStatus() & 0x02) != 0) {
1761                     where = " AND " + Sms.READ + "= 1";
1762                 }
1763             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1764                 if ((ap.getFilterReadStatus() & 0x01) != 0) {
1765                     where = " AND " + Mms.READ + "= 0";
1766                 }
1767 
1768                 if ((ap.getFilterReadStatus() & 0x02) != 0) {
1769                     where = " AND " + Mms.READ + "= 1";
1770                 }
1771             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1772                 if ((ap.getFilterReadStatus() & 0x01) != 0) {
1773                     where = " AND " + BluetoothMapContract.MessageColumns.FLAG_READ + "= 0";
1774                 }
1775                 if ((ap.getFilterReadStatus() & 0x02) != 0) {
1776                     where = " AND " + BluetoothMapContract.MessageColumns.FLAG_READ + "= 1";
1777                 }
1778             }
1779         }
1780         return where;
1781     }
1782 
setWhereFilterPeriod(BluetoothMapAppParams ap, FilterInfo fi)1783     private static String setWhereFilterPeriod(BluetoothMapAppParams ap, FilterInfo fi) {
1784         String where = "";
1785 
1786         if ((ap.getFilterPeriodBegin() != -1)) {
1787             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1788                 where = " AND " + Sms.DATE + " >= " + ap.getFilterPeriodBegin();
1789             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1790                 where = " AND " + Mms.DATE + " >= " + (ap.getFilterPeriodBegin() / 1000L);
1791             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1792                 where =
1793                         " AND "
1794                                 + BluetoothMapContract.MessageColumns.DATE
1795                                 + " >= "
1796                                 + (ap.getFilterPeriodBegin());
1797             }
1798         }
1799 
1800         if ((ap.getFilterPeriodEnd() != -1)) {
1801             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1802                 where = where + " AND " + Sms.DATE + " < " + ap.getFilterPeriodEnd();
1803             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1804                 where = where + " AND " + Mms.DATE + " < " + (ap.getFilterPeriodEnd() / 1000L);
1805             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1806                 where =
1807                         where
1808                                 + " AND "
1809                                 + BluetoothMapContract.MessageColumns.DATE
1810                                 + " < "
1811                                 + ap.getFilterPeriodEnd();
1812             }
1813         }
1814         return where;
1815     }
1816 
setWhereFilterOriginatorEmail(BluetoothMapAppParams ap)1817     private static String setWhereFilterOriginatorEmail(BluetoothMapAppParams ap) {
1818         String where = "";
1819         String orig = ap.getFilterOriginator();
1820 
1821         /* Be aware of wild cards in the beginning of string, may not be valid? */
1822         if (orig != null && orig.length() > 0) {
1823             orig = orig.replace("*", "%");
1824             where =
1825                     " AND "
1826                             + BluetoothMapContract.MessageColumns.FROM_LIST
1827                             + " LIKE '%"
1828                             + orig
1829                             + "%'";
1830         }
1831         return where;
1832     }
1833 
setWhereFilterOriginatorIM(BluetoothMapAppParams ap)1834     private static String setWhereFilterOriginatorIM(BluetoothMapAppParams ap) {
1835         String where = "";
1836         String orig = ap.getFilterOriginator();
1837 
1838         /* Be aware of wild cards in the beginning of string, may not be valid? */
1839         if (orig != null && orig.length() > 0) {
1840             orig = orig.replace("*", "%");
1841             where =
1842                     " AND "
1843                             + BluetoothMapContract.MessageColumns.FROM_LIST
1844                             + " LIKE '%"
1845                             + orig
1846                             + "%'";
1847         }
1848         return where;
1849     }
1850 
setWhereFilterPriority(BluetoothMapAppParams ap, FilterInfo fi)1851     private static String setWhereFilterPriority(BluetoothMapAppParams ap, FilterInfo fi) {
1852         String where = "";
1853         int pri = ap.getFilterPriority();
1854         /*only MMS have priority info */
1855         if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1856             if (pri == 0x0002) {
1857                 where =
1858                         " AND "
1859                                 + Mms.PRIORITY
1860                                 + "<="
1861                                 + Integer.toString(PduHeaders.PRIORITY_NORMAL);
1862             } else if (pri == 0x0001) {
1863                 where = " AND " + Mms.PRIORITY + "=" + Integer.toString(PduHeaders.PRIORITY_HIGH);
1864             }
1865         }
1866         if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1867             if (pri == 0x0002) {
1868                 where = " AND " + BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY + "!=1";
1869             } else if (pri == 0x0001) {
1870                 where = " AND " + BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY + "=1";
1871             }
1872         }
1873         // TODO: no priority filtering in IM
1874         return where;
1875     }
1876 
setWhereFilterRecipientEmail(BluetoothMapAppParams ap)1877     private static String setWhereFilterRecipientEmail(BluetoothMapAppParams ap) {
1878         String where = "";
1879         String recipient = ap.getFilterRecipient();
1880 
1881         /* Be aware of wild cards in the beginning of string, may not be valid? */
1882         if (recipient != null && recipient.length() > 0) {
1883             recipient = recipient.replace("*", "%");
1884             where =
1885                     " AND ("
1886                             + BluetoothMapContract.MessageColumns.TO_LIST
1887                             + " LIKE '%"
1888                             + recipient
1889                             + "%' OR "
1890                             + BluetoothMapContract.MessageColumns.CC_LIST
1891                             + " LIKE '%"
1892                             + recipient
1893                             + "%' OR "
1894                             + BluetoothMapContract.MessageColumns.BCC_LIST
1895                             + " LIKE '%"
1896                             + recipient
1897                             + "%' )";
1898         }
1899         return where;
1900     }
1901 
setWhereFilterMessageHandle(BluetoothMapAppParams ap, FilterInfo fi)1902     private static String setWhereFilterMessageHandle(BluetoothMapAppParams ap, FilterInfo fi) {
1903         String where = "";
1904         long id = -1;
1905         String msgHandle = ap.getFilterMsgHandleString();
1906         if (msgHandle != null) {
1907             id = BluetoothMapUtils.getCpHandle(msgHandle);
1908             Log.d(TAG, "id: " + id);
1909         }
1910         if (id != -1) {
1911             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1912                 where = " AND " + Sms._ID + " = " + id;
1913             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1914                 where = " AND " + Mms._ID + " = " + id;
1915             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1916                 where = " AND " + BluetoothMapContract.MessageColumns._ID + " = " + id;
1917             }
1918         }
1919         return where;
1920     }
1921 
setWhereFilterThreadId(BluetoothMapAppParams ap, FilterInfo fi)1922     private static String setWhereFilterThreadId(BluetoothMapAppParams ap, FilterInfo fi) {
1923         String where = "";
1924         long id = -1;
1925         String msgHandle = ap.getFilterConvoIdString();
1926         if (msgHandle != null) {
1927             id = BluetoothMapUtils.getMsgHandleAsLong(msgHandle);
1928             Log.d(TAG, "id: " + id);
1929         }
1930         if (id > 0) {
1931             if (fi.mMsgType == FilterInfo.TYPE_SMS) {
1932                 where = " AND " + Sms.THREAD_ID + " = " + id;
1933             } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
1934                 where = " AND " + Mms.THREAD_ID + " = " + id;
1935             } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL || fi.mMsgType == FilterInfo.TYPE_IM) {
1936                 where = " AND " + BluetoothMapContract.MessageColumns.THREAD_ID + " = " + id;
1937             }
1938         }
1939 
1940         return where;
1941     }
1942 
setWhereFilter( BluetoothMapFolderElement folderElement, FilterInfo fi, BluetoothMapAppParams ap)1943     private static String setWhereFilter(
1944             BluetoothMapFolderElement folderElement, FilterInfo fi, BluetoothMapAppParams ap) {
1945         StringBuilder where = new StringBuilder(setWhereFilterFolderType(folderElement, fi));
1946 
1947         String msgHandleWhere = setWhereFilterMessageHandle(ap, fi);
1948         /* if message handle filter is available, the other filters should be ignored */
1949         if (!msgHandleWhere.isEmpty()) {
1950             where.append(msgHandleWhere);
1951             return where.toString();
1952         }
1953 
1954         where.append(setWhereFilterReadStatus(ap, fi));
1955         where.append(setWhereFilterPriority(ap, fi));
1956         where.append(setWhereFilterPeriod(ap, fi));
1957         if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
1958             where.append(setWhereFilterOriginatorEmail(ap));
1959             where.append(setWhereFilterRecipientEmail(ap));
1960         }
1961         if (fi.mMsgType == FilterInfo.TYPE_IM) {
1962             where.append(setWhereFilterOriginatorIM(ap));
1963             // TODO: set 'where' filer recipient?
1964         }
1965         where.append(setWhereFilterThreadId(ap, fi));
1966 
1967         return where.toString();
1968     }
1969 
1970     /* Used only for SMS/MMS */
1971     @VisibleForTesting
setConvoWhereFilterSmsMms( StringBuilder selection, FilterInfo fi, BluetoothMapAppParams ap)1972     void setConvoWhereFilterSmsMms(
1973             StringBuilder selection, FilterInfo fi, BluetoothMapAppParams ap) {
1974 
1975         if (smsSelected(fi, ap) || mmsSelected(ap)) {
1976 
1977             // Filter Read Status
1978             if (ap.getFilterReadStatus() != BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
1979                 if ((ap.getFilterReadStatus() & FILTER_READ_STATUS_UNREAD_ONLY) != 0) {
1980                     selection.append(" AND ").append(Threads.READ).append(" = 0");
1981                 }
1982                 if ((ap.getFilterReadStatus() & FILTER_READ_STATUS_READ_ONLY) != 0) {
1983                     selection.append(" AND ").append(Threads.READ).append(" = 1");
1984                 }
1985             }
1986 
1987             // Filter time
1988             if ((ap.getFilterLastActivityBegin()
1989                     != BluetoothMapAppParams.INVALID_VALUE_PARAMETER)) {
1990                 selection
1991                         .append(" AND ")
1992                         .append(Threads.DATE)
1993                         .append(" >= ")
1994                         .append(ap.getFilterLastActivityBegin());
1995             }
1996             if ((ap.getFilterLastActivityEnd() != BluetoothMapAppParams.INVALID_VALUE_PARAMETER)) {
1997                 selection
1998                         .append(" AND ")
1999                         .append(Threads.DATE)
2000                         .append(" <= ")
2001                         .append(ap.getFilterLastActivityEnd());
2002             }
2003 
2004             // Filter ConvoId
2005             long convoId = -1;
2006             if (ap.getFilterConvoId() != null) {
2007                 convoId = ap.getFilterConvoId().leastSignificantBits();
2008             }
2009             if (convoId > 0) {
2010                 selection
2011                         .append(" AND ")
2012                         .append(Threads._ID)
2013                         .append(" = ")
2014                         .append(Long.toString(convoId));
2015             }
2016         }
2017     }
2018 
2019     /**
2020      * Determine from application parameter if sms should be included. The filter mask is set for
2021      * message types not selected
2022      *
2023      * @return boolean true if sms is selected, false if not
2024      */
2025     @VisibleForTesting
smsSelected(FilterInfo fi, BluetoothMapAppParams ap)2026     boolean smsSelected(FilterInfo fi, BluetoothMapAppParams ap) {
2027         int msgType = ap.getFilterMessageType();
2028         int phoneType = fi.mPhoneType;
2029 
2030         Log.d(TAG, "smsSelected msgType: " + msgType);
2031 
2032         if (msgType == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
2033             return true;
2034         }
2035 
2036         if ((msgType
2037                         & (BluetoothMapAppParams.FILTER_NO_SMS_CDMA
2038                                 | BluetoothMapAppParams.FILTER_NO_SMS_GSM))
2039                 == 0) {
2040             return true;
2041         }
2042 
2043         if (((msgType & BluetoothMapAppParams.FILTER_NO_SMS_GSM) == 0)
2044                 && (phoneType == TelephonyManager.PHONE_TYPE_GSM)) {
2045             return true;
2046         }
2047 
2048         if (((msgType & BluetoothMapAppParams.FILTER_NO_SMS_CDMA) == 0)
2049                 && (phoneType == TelephonyManager.PHONE_TYPE_CDMA)) {
2050             return true;
2051         }
2052 
2053         return false;
2054     }
2055 
2056     /**
2057      * Determine from application parameter if mms should be included. The filter mask is set for
2058      * message types not selected
2059      *
2060      * @return boolean true if mms is selected, false if not
2061      */
2062     @VisibleForTesting
mmsSelected(BluetoothMapAppParams ap)2063     boolean mmsSelected(BluetoothMapAppParams ap) {
2064         int msgType = ap.getFilterMessageType();
2065 
2066         Log.d(TAG, "mmsSelected msgType: " + msgType);
2067 
2068         if (msgType == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
2069             return true;
2070         }
2071 
2072         if ((msgType & BluetoothMapAppParams.FILTER_NO_MMS) == 0) {
2073             return true;
2074         }
2075 
2076         return false;
2077     }
2078 
2079     /**
2080      * Determine from application parameter if email should be included. The filter mask is set for
2081      * message types not selected
2082      *
2083      * @return boolean true if email is selected, false if not
2084      */
emailSelected(BluetoothMapAppParams ap)2085     private static boolean emailSelected(BluetoothMapAppParams ap) {
2086         int msgType = ap.getFilterMessageType();
2087 
2088         Log.d(TAG, "emailSelected msgType: " + msgType);
2089 
2090         if (msgType == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
2091             return true;
2092         }
2093 
2094         if ((msgType & BluetoothMapAppParams.FILTER_NO_EMAIL) == 0) {
2095             return true;
2096         }
2097 
2098         return false;
2099     }
2100 
2101     /**
2102      * Determine from application parameter if IM should be included. The filter mask is set for
2103      * message types not selected
2104      *
2105      * @return boolean true if im is selected, false if not
2106      */
imSelected(BluetoothMapAppParams ap)2107     private static boolean imSelected(BluetoothMapAppParams ap) {
2108         int msgType = ap.getFilterMessageType();
2109 
2110         Log.d(TAG, "imSelected msgType: " + msgType);
2111 
2112         if (msgType == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
2113             return true;
2114         }
2115 
2116         if ((msgType & BluetoothMapAppParams.FILTER_NO_IM) == 0) {
2117             return true;
2118         }
2119 
2120         return false;
2121     }
2122 
2123     @VisibleForTesting
setFilterInfo(FilterInfo fi)2124     void setFilterInfo(FilterInfo fi) {
2125         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
2126         if (tm != null) {
2127             fi.mPhoneType = tm.getPhoneType();
2128             fi.mPhoneNum = tm.getLine1Number();
2129         }
2130     }
2131 
2132     /**
2133      * Get a listing of message in folder after applying filter.
2134      *
2135      * @param folderElement Must contain a valid folder string != null
2136      * @param ap Parameters specifying message content and filters
2137      * @return Listing object containing requested messages
2138      */
msgListing( BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap)2139     public BluetoothMapMessageListing msgListing(
2140             BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap) {
2141         Log.d(TAG, "msgListing: messageType = " + ap.getFilterMessageType());
2142 
2143         BluetoothMapMessageListing bmList = new BluetoothMapMessageListing();
2144 
2145         /* We overwrite the parameter mask here if it is 0 or not present, as this
2146          * should cause all parameters to be included in the message list. */
2147         if (ap.getParameterMask() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER
2148                 || ap.getParameterMask() == 0) {
2149             ap.setParameterMask(PARAMETER_MASK_ALL_ENABLED);
2150             Log.v(
2151                     TAG,
2152                     "msgListing(): appParameterMask is zero or not present, "
2153                             + "changing to all enabled by default: "
2154                             + ap.getParameterMask());
2155         }
2156         Log.v(
2157                 TAG,
2158                 "folderElement hasSmsMmsContent = "
2159                         + folderElement.hasSmsMmsContent()
2160                         + " folderElement.hasEmailContent = "
2161                         + folderElement.hasEmailContent()
2162                         + " folderElement.hasImContent = "
2163                         + folderElement.hasImContent());
2164 
2165         /* Cache some info used throughout filtering */
2166         FilterInfo fi = new FilterInfo();
2167         setFilterInfo(fi);
2168         Cursor smsCursor = null;
2169         Cursor mmsCursor = null;
2170         Cursor emailCursor = null;
2171         Cursor imCursor = null;
2172         String limit = "";
2173         int offsetNum = ap.getStartOffset();
2174         if (ap.getMaxListCount() > 0) {
2175             limit = " LIMIT " + (ap.getMaxListCount() + ap.getStartOffset());
2176         }
2177         try {
2178             if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
2179                 if (ap.getFilterMessageType()
2180                                 == (BluetoothMapAppParams.FILTER_NO_EMAIL
2181                                         | BluetoothMapAppParams.FILTER_NO_MMS
2182                                         | BluetoothMapAppParams.FILTER_NO_SMS_GSM
2183                                         | BluetoothMapAppParams.FILTER_NO_IM)
2184                         || ap.getFilterMessageType()
2185                                 == (BluetoothMapAppParams.FILTER_NO_EMAIL
2186                                         | BluetoothMapAppParams.FILTER_NO_MMS
2187                                         | BluetoothMapAppParams.FILTER_NO_SMS_CDMA
2188                                         | BluetoothMapAppParams.FILTER_NO_IM)) {
2189                     // set real limit and offset if only this type is used
2190                     // (only if offset/limit is used)
2191                     limit = " LIMIT " + ap.getMaxListCount() + " OFFSET " + ap.getStartOffset();
2192                     Log.d(TAG, "SMS Limit => " + limit);
2193                     offsetNum = 0;
2194                 }
2195                 fi.mMsgType = FilterInfo.TYPE_SMS;
2196                 if (ap.getFilterPriority() != 1) {
2197                     /*SMS cannot have high priority*/
2198                     String where = setWhereFilter(folderElement, fi, ap);
2199                     Log.d(TAG, "msgType: " + fi.mMsgType + " where: " + where);
2200                     smsCursor =
2201                             BluetoothMethodProxy.getInstance()
2202                                     .contentResolverQuery(
2203                                             mResolver,
2204                                             Sms.CONTENT_URI,
2205                                             SMS_PROJECTION,
2206                                             where,
2207                                             null,
2208                                             Sms.DATE + " DESC" + limit);
2209                     if (smsCursor != null) {
2210                         BluetoothMapMessageListingElement e = null;
2211                         // store column index so we dont have to look them up anymore (optimization)
2212                         Log.d(TAG, "Found " + smsCursor.getCount() + " sms messages.");
2213                         fi.setSmsColumns(smsCursor);
2214                         while (smsCursor.moveToNext()) {
2215                             if (matchAddresses(smsCursor, fi, ap)) {
2216                                 BluetoothMapUtils.printCursor(smsCursor);
2217                                 e = element(smsCursor, fi, ap);
2218                                 bmList.add(e);
2219                             }
2220                         }
2221                     }
2222                 }
2223             }
2224 
2225             if (mmsSelected(ap) && folderElement.hasSmsMmsContent()) {
2226                 if (ap.getFilterMessageType()
2227                         == (BluetoothMapAppParams.FILTER_NO_EMAIL
2228                                 | BluetoothMapAppParams.FILTER_NO_SMS_CDMA
2229                                 | BluetoothMapAppParams.FILTER_NO_SMS_GSM
2230                                 | BluetoothMapAppParams.FILTER_NO_IM)) {
2231                     // set real limit and offset if only this type is used
2232                     // (only if offset/limit is used)
2233                     limit = " LIMIT " + ap.getMaxListCount() + " OFFSET " + ap.getStartOffset();
2234                     Log.d(TAG, "MMS Limit => " + limit);
2235                     offsetNum = 0;
2236                 }
2237                 fi.mMsgType = FilterInfo.TYPE_MMS;
2238                 String where =
2239                         setWhereFilter(folderElement, fi, ap)
2240                                 + " AND "
2241                                 + INTERESTED_MESSAGE_TYPE_CLAUSE;
2242                 if (!where.isEmpty()) {
2243                     Log.d(TAG, "msgType: " + fi.mMsgType + " where: " + where);
2244                     mmsCursor =
2245                             BluetoothMethodProxy.getInstance()
2246                                     .contentResolverQuery(
2247                                             mResolver,
2248                                             Mms.CONTENT_URI,
2249                                             MMS_PROJECTION,
2250                                             where,
2251                                             null,
2252                                             Mms.DATE + " DESC" + limit);
2253                     if (mmsCursor != null) {
2254                         BluetoothMapMessageListingElement e = null;
2255                         // store column index so we dont have to look them up anymore (optimization)
2256                         fi.setMmsColumns(mmsCursor);
2257                         Log.d(TAG, "Found " + mmsCursor.getCount() + " mms messages.");
2258                         while (mmsCursor.moveToNext()) {
2259                             if (matchAddresses(mmsCursor, fi, ap)) {
2260                                 BluetoothMapUtils.printCursor(mmsCursor);
2261                                 e = element(mmsCursor, fi, ap);
2262                                 bmList.add(e);
2263                             }
2264                         }
2265                     }
2266                 }
2267             }
2268 
2269             if (emailSelected(ap) && folderElement.hasEmailContent()) {
2270                 if (ap.getFilterMessageType()
2271                         == (BluetoothMapAppParams.FILTER_NO_MMS
2272                                 | BluetoothMapAppParams.FILTER_NO_SMS_CDMA
2273                                 | BluetoothMapAppParams.FILTER_NO_SMS_GSM
2274                                 | BluetoothMapAppParams.FILTER_NO_IM)) {
2275                     // set real limit and offset if only this type is used
2276                     // (only if offset/limit is used)
2277                     limit = " LIMIT " + ap.getMaxListCount() + " OFFSET " + ap.getStartOffset();
2278                     Log.d(TAG, "Email Limit => " + limit);
2279                     offsetNum = 0;
2280                 }
2281                 fi.mMsgType = FilterInfo.TYPE_EMAIL;
2282                 String where = setWhereFilter(folderElement, fi, ap);
2283 
2284                 if (!where.isEmpty()) {
2285                     Log.d(TAG, "msgType: " + fi.mMsgType + " where: " + where);
2286                     Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2287                     emailCursor =
2288                             BluetoothMethodProxy.getInstance()
2289                                     .contentResolverQuery(
2290                                             mResolver,
2291                                             contentUri,
2292                                             BluetoothMapContract.BT_MESSAGE_PROJECTION,
2293                                             where,
2294                                             null,
2295                                             BluetoothMapContract.MessageColumns.DATE
2296                                                     + " DESC"
2297                                                     + limit);
2298                     if (emailCursor != null) {
2299                         BluetoothMapMessageListingElement e = null;
2300                         // store column index so we dont have to look them up anymore (optimization)
2301                         fi.setEmailMessageColumns(emailCursor);
2302                         Log.d(TAG, "Found " + emailCursor.getCount() + " email messages.");
2303                         while (emailCursor.moveToNext()) {
2304                             BluetoothMapUtils.printCursor(emailCursor);
2305                             e = element(emailCursor, fi, ap);
2306                             bmList.add(e);
2307                         }
2308                         //   emailCursor.close();
2309                     }
2310                 }
2311             }
2312 
2313             if (imSelected(ap) && folderElement.hasImContent()) {
2314                 if (ap.getFilterMessageType()
2315                         == (BluetoothMapAppParams.FILTER_NO_MMS
2316                                 | BluetoothMapAppParams.FILTER_NO_SMS_CDMA
2317                                 | BluetoothMapAppParams.FILTER_NO_SMS_GSM
2318                                 | BluetoothMapAppParams.FILTER_NO_EMAIL)) {
2319                     // set real limit and offset if only this type is used
2320                     // (only if offset/limit is used)
2321                     limit = " LIMIT " + ap.getMaxListCount() + " OFFSET " + ap.getStartOffset();
2322                     Log.d(TAG, "IM Limit => " + limit);
2323                     offsetNum = 0;
2324                 }
2325                 fi.mMsgType = FilterInfo.TYPE_IM;
2326                 String where = setWhereFilter(folderElement, fi, ap);
2327                 Log.d(TAG, "msgType: " + fi.mMsgType + " where: " + where);
2328 
2329                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2330                 imCursor =
2331                         BluetoothMethodProxy.getInstance()
2332                                 .contentResolverQuery(
2333                                         mResolver,
2334                                         contentUri,
2335                                         BluetoothMapContract.BT_INSTANT_MESSAGE_PROJECTION,
2336                                         where,
2337                                         null,
2338                                         BluetoothMapContract.MessageColumns.DATE + " DESC" + limit);
2339                 if (imCursor != null) {
2340                     BluetoothMapMessageListingElement e = null;
2341                     // store column index so we dont have to look them up anymore (optimization)
2342                     fi.setImMessageColumns(imCursor);
2343                     Log.d(TAG, "Found " + imCursor.getCount() + " im messages.");
2344                     while (imCursor.moveToNext()) {
2345                         BluetoothMapUtils.printCursor(imCursor);
2346                         e = element(imCursor, fi, ap);
2347                         bmList.add(e);
2348                     }
2349                 }
2350             }
2351 
2352             /* Enable this if post sorting and segmenting needed */
2353             bmList.sort();
2354             bmList.segment(ap.getMaxListCount(), offsetNum);
2355             List<BluetoothMapMessageListingElement> list = bmList.getList();
2356             int listSize = list.size();
2357             Cursor tmpCursor = null;
2358             for (int x = 0; x < listSize; x++) {
2359                 BluetoothMapMessageListingElement ele = list.get(x);
2360                 /* If OBEX "GET" request header includes "ParameterMask" with 'Type' NOT set,
2361                  * then ele.getType() returns "null" even for a valid cursor.
2362                  * Avoid NullPointerException in equals() check when 'mType' value is "null" */
2363                 TYPE tmpType = ele.getType();
2364                 if (smsCursor != null
2365                         && ((TYPE.SMS_GSM).equals(tmpType) || (TYPE.SMS_CDMA).equals(tmpType))) {
2366                     tmpCursor = smsCursor;
2367                     fi.mMsgType = FilterInfo.TYPE_SMS;
2368                 } else if (mmsCursor != null && (TYPE.MMS).equals(tmpType)) {
2369                     tmpCursor = mmsCursor;
2370                     fi.mMsgType = FilterInfo.TYPE_MMS;
2371                 } else if (emailCursor != null && ((TYPE.EMAIL).equals(tmpType))) {
2372                     tmpCursor = emailCursor;
2373                     fi.mMsgType = FilterInfo.TYPE_EMAIL;
2374                 } else if (imCursor != null && ((TYPE.IM).equals(tmpType))) {
2375                     tmpCursor = imCursor;
2376                     fi.mMsgType = FilterInfo.TYPE_IM;
2377                 }
2378                 if (tmpCursor != null) {
2379                     tmpCursor.moveToPosition(ele.getCursorIndex());
2380                     setSenderAddressing(ele, tmpCursor, fi, ap);
2381                     setSenderName(ele, tmpCursor, fi, ap);
2382                     setRecipientAddressing(ele, tmpCursor, fi, ap);
2383                     setRecipientName(ele, tmpCursor, fi, ap);
2384                     setSubject(ele, tmpCursor, fi, ap);
2385                     setSize(ele, tmpCursor, fi, ap);
2386                     setText(ele, tmpCursor, fi, ap);
2387                     setPriority(ele, tmpCursor, fi, ap);
2388                     setSent(ele, tmpCursor, fi, ap);
2389                     setProtected(ele, tmpCursor, fi, ap);
2390                     setReceptionStatus(ele, ap);
2391                     setAttachment(ele, tmpCursor, fi, ap);
2392 
2393                     if (mMsgListingVersion > BluetoothMapUtils.MAP_MESSAGE_LISTING_FORMAT_V10) {
2394                         setDeliveryStatus(ele, tmpCursor, fi, ap);
2395                         setThreadId(ele, tmpCursor, fi, ap);
2396                         setThreadName(ele, tmpCursor, fi, ap);
2397                     }
2398                 }
2399             }
2400         } finally {
2401             if (emailCursor != null) {
2402                 emailCursor.close();
2403             }
2404             if (smsCursor != null) {
2405                 smsCursor.close();
2406             }
2407             if (mmsCursor != null) {
2408                 mmsCursor.close();
2409             }
2410             if (imCursor != null) {
2411                 imCursor.close();
2412             }
2413         }
2414 
2415         Log.d(TAG, "msgListing end");
2416         return bmList;
2417     }
2418 
2419     /**
2420      * Get the size of the message listing
2421      *
2422      * @param folderElement Must contain a valid folder string != null
2423      * @param ap Parameters specifying message content and filters
2424      * @return Integer equal to message listing size
2425      */
msgListingSize(BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap)2426     public int msgListingSize(BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap) {
2427         Log.d(TAG, "msgListingSize: folder = " + folderElement.getName());
2428         int cnt = 0;
2429 
2430         /* Cache some info used throughout filtering */
2431         FilterInfo fi = new FilterInfo();
2432         setFilterInfo(fi);
2433 
2434         if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
2435             fi.mMsgType = FilterInfo.TYPE_SMS;
2436             String where = setWhereFilter(folderElement, fi, ap);
2437             Cursor c =
2438                     BluetoothMethodProxy.getInstance()
2439                             .contentResolverQuery(
2440                                     mResolver,
2441                                     Sms.CONTENT_URI,
2442                                     SMS_PROJECTION,
2443                                     where,
2444                                     null,
2445                                     Sms.DATE + " DESC");
2446             try {
2447                 if (c != null) {
2448                     cnt = c.getCount();
2449                 }
2450             } finally {
2451                 if (c != null) {
2452                     c.close();
2453                 }
2454             }
2455         }
2456 
2457         if (mmsSelected(ap) && folderElement.hasSmsMmsContent()) {
2458             fi.mMsgType = FilterInfo.TYPE_MMS;
2459             String where = setWhereFilter(folderElement, fi, ap);
2460             Cursor c =
2461                     BluetoothMethodProxy.getInstance()
2462                             .contentResolverQuery(
2463                                     mResolver,
2464                                     Mms.CONTENT_URI,
2465                                     MMS_PROJECTION,
2466                                     where,
2467                                     null,
2468                                     Mms.DATE + " DESC");
2469             try {
2470                 if (c != null) {
2471                     cnt += c.getCount();
2472                 }
2473             } finally {
2474                 if (c != null) {
2475                     c.close();
2476                 }
2477             }
2478         }
2479 
2480         if (emailSelected(ap) && folderElement.hasEmailContent()) {
2481             fi.mMsgType = FilterInfo.TYPE_EMAIL;
2482             String where = setWhereFilter(folderElement, fi, ap);
2483             if (!where.isEmpty()) {
2484                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2485                 Cursor c =
2486                         BluetoothMethodProxy.getInstance()
2487                                 .contentResolverQuery(
2488                                         mResolver,
2489                                         contentUri,
2490                                         BluetoothMapContract.BT_MESSAGE_PROJECTION,
2491                                         where,
2492                                         null,
2493                                         BluetoothMapContract.MessageColumns.DATE + " DESC");
2494                 try {
2495                     if (c != null) {
2496                         cnt += c.getCount();
2497                     }
2498                 } finally {
2499                     if (c != null) {
2500                         c.close();
2501                     }
2502                 }
2503             }
2504         }
2505 
2506         if (imSelected(ap) && folderElement.hasImContent()) {
2507             fi.mMsgType = FilterInfo.TYPE_IM;
2508             String where = setWhereFilter(folderElement, fi, ap);
2509             if (!where.isEmpty()) {
2510                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2511                 Cursor c =
2512                         BluetoothMethodProxy.getInstance()
2513                                 .contentResolverQuery(
2514                                         mResolver,
2515                                         contentUri,
2516                                         BluetoothMapContract.BT_INSTANT_MESSAGE_PROJECTION,
2517                                         where,
2518                                         null,
2519                                         BluetoothMapContract.MessageColumns.DATE + " DESC");
2520                 try {
2521                     if (c != null) {
2522                         cnt += c.getCount();
2523                     }
2524                 } finally {
2525                     if (c != null) {
2526                         c.close();
2527                     }
2528                 }
2529             }
2530         }
2531 
2532         Log.d(TAG, "msgListingSize: size = " + cnt);
2533         return cnt;
2534     }
2535 
2536     /**
2537      * Return true if there are unread messages in the requested list of messages
2538      *
2539      * @param folderElement folder where the message listing should come from
2540      * @param ap application parameter object
2541      * @return true if unread messages are in the list, else false
2542      */
msgListingHasUnread( BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap)2543     public boolean msgListingHasUnread(
2544             BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap) {
2545         Log.d(TAG, "msgListingHasUnread: folder = " + folderElement.getName());
2546         int cnt = 0;
2547 
2548         /* Cache some info used throughout filtering */
2549         FilterInfo fi = new FilterInfo();
2550         setFilterInfo(fi);
2551 
2552         if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
2553             fi.mMsgType = FilterInfo.TYPE_SMS;
2554             String where =
2555                     setWhereFilterFolderType(folderElement, fi)
2556                             + " AND "
2557                             + Sms.READ
2558                             + "=0 "
2559                             + setWhereFilterPeriod(ap, fi);
2560             Cursor c =
2561                     BluetoothMethodProxy.getInstance()
2562                             .contentResolverQuery(
2563                                     mResolver,
2564                                     Sms.CONTENT_URI,
2565                                     SMS_PROJECTION,
2566                                     where,
2567                                     null,
2568                                     Sms.DATE + " DESC");
2569             try {
2570                 if (c != null) {
2571                     cnt = c.getCount();
2572                 }
2573             } finally {
2574                 if (c != null) {
2575                     c.close();
2576                 }
2577             }
2578         }
2579 
2580         if (mmsSelected(ap) && folderElement.hasSmsMmsContent()) {
2581             fi.mMsgType = FilterInfo.TYPE_MMS;
2582             String where =
2583                     setWhereFilterFolderType(folderElement, fi)
2584                             + " AND "
2585                             + Mms.READ
2586                             + "=0 "
2587                             + setWhereFilterPeriod(ap, fi);
2588             Cursor c =
2589                     BluetoothMethodProxy.getInstance()
2590                             .contentResolverQuery(
2591                                     mResolver,
2592                                     Mms.CONTENT_URI,
2593                                     MMS_PROJECTION,
2594                                     where,
2595                                     null,
2596                                     Sms.DATE + " DESC");
2597             try {
2598                 if (c != null) {
2599                     cnt += c.getCount();
2600                 }
2601             } finally {
2602                 if (c != null) {
2603                     c.close();
2604                 }
2605             }
2606         }
2607 
2608         if (emailSelected(ap) && folderElement.getFolderId() != -1) {
2609             fi.mMsgType = FilterInfo.TYPE_EMAIL;
2610             String where = setWhereFilterFolderType(folderElement, fi);
2611             if (!where.isEmpty()) {
2612                 where =
2613                         where
2614                                 + " AND "
2615                                 + BluetoothMapContract.MessageColumns.FLAG_READ
2616                                 + "=0 "
2617                                 + setWhereFilterPeriod(ap, fi);
2618                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2619                 Cursor c =
2620                         BluetoothMethodProxy.getInstance()
2621                                 .contentResolverQuery(
2622                                         mResolver,
2623                                         contentUri,
2624                                         BluetoothMapContract.BT_MESSAGE_PROJECTION,
2625                                         where,
2626                                         null,
2627                                         BluetoothMapContract.MessageColumns.DATE + " DESC");
2628                 try {
2629                     if (c != null) {
2630                         cnt += c.getCount();
2631                     }
2632                 } finally {
2633                     if (c != null) {
2634                         c.close();
2635                     }
2636                 }
2637             }
2638         }
2639 
2640         if (imSelected(ap) && folderElement.hasImContent()) {
2641             fi.mMsgType = FilterInfo.TYPE_IM;
2642             String where = setWhereFilter(folderElement, fi, ap);
2643             if (!where.isEmpty()) {
2644                 where =
2645                         where
2646                                 + " AND "
2647                                 + BluetoothMapContract.MessageColumns.FLAG_READ
2648                                 + "=0 "
2649                                 + setWhereFilterPeriod(ap, fi);
2650                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2651                 Cursor c =
2652                         BluetoothMethodProxy.getInstance()
2653                                 .contentResolverQuery(
2654                                         mResolver,
2655                                         contentUri,
2656                                         BluetoothMapContract.BT_INSTANT_MESSAGE_PROJECTION,
2657                                         where,
2658                                         null,
2659                                         BluetoothMapContract.MessageColumns.DATE + " DESC");
2660                 try {
2661                     if (c != null) {
2662                         cnt += c.getCount();
2663                     }
2664                 } finally {
2665                     if (c != null) {
2666                         c.close();
2667                     }
2668                 }
2669             }
2670         }
2671 
2672         Log.d(TAG, "msgListingHasUnread: numUnread = " + cnt);
2673         return cnt > 0;
2674     }
2675 
2676     /**
2677      * Build the conversation listing.
2678      *
2679      * @param ap The Application Parameters
2680      * @param sizeOnly TRUE: don't populate the list members, only build the list to get the size.
2681      * @return the BluetoothMapConvoListing
2682      */
convoListing(BluetoothMapAppParams ap, boolean sizeOnly)2683     BluetoothMapConvoListing convoListing(BluetoothMapAppParams ap, boolean sizeOnly) {
2684 
2685         Log.d(TAG, "convoListing: " + " messageType = " + ap.getFilterMessageType());
2686         BluetoothMapConvoListing convoList = new BluetoothMapConvoListing();
2687 
2688         /* We overwrite the parameter mask here if it is 0 or not present, as this
2689          * should cause all parameters to be included in the message list. */
2690         if (ap.getConvoParameterMask() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER
2691                 || ap.getConvoParameterMask() == 0) {
2692             ap.setConvoParameterMask(CONVO_PARAMETER_MASK_DEFAULT);
2693             Log.d(
2694                     TAG,
2695                     "convoListing(): appParameterMask is zero or not present, "
2696                             + "changing to default: "
2697                             + ap.getConvoParameterMask());
2698         }
2699 
2700         /* Possible filters:
2701          *  - Recipient name (contacts DB) or id (for SMS/MMS this is the thread-id contact-id)
2702          *  - Activity start/begin
2703          *  - Read status
2704          *  - Thread_id
2705          * The strategy for SMS/MMS
2706          *   With no filter on name - use limit and offset.
2707          *   With a filter on name - build the complete list of conversations and create a filter
2708          *                           mechanism
2709          *
2710          * The strategy for IM:
2711          *   Join the conversation table with the contacts table in a way that makes it possible to
2712          *   get the data needed in a single query.
2713          *   Manually handle limit/offset
2714          * */
2715 
2716         /* Cache some info used throughout filtering */
2717         FilterInfo fi = new FilterInfo();
2718         setFilterInfo(fi);
2719         Cursor smsMmsCursor = null;
2720         Cursor imEmailCursor = null;
2721         int offsetNum;
2722         if (sizeOnly) {
2723             offsetNum = 0;
2724         } else {
2725             offsetNum = ap.getStartOffset();
2726         }
2727         // Inverse meaning - hence a 1 is include.
2728         int msgTypesInclude =
2729                 ((~ap.getFilterMessageType()) & BluetoothMapAppParams.FILTER_MSG_TYPE_MASK);
2730         int maxThreads = ap.getMaxListCount() + ap.getStartOffset();
2731 
2732         try {
2733             if (smsSelected(fi, ap) || mmsSelected(ap)) {
2734                 String limit = "";
2735                 if ((!sizeOnly)
2736                         && (ap.getMaxListCount() > 0)
2737                         && (ap.getFilterRecipient() == null)) {
2738                     /* We can only use limit if we do not have a contacts filter */
2739                     limit = " LIMIT " + maxThreads;
2740                 }
2741                 StringBuilder sortOrder = new StringBuilder(Threads.DATE + " DESC");
2742                 if ((!sizeOnly)
2743                         && (((msgTypesInclude
2744                                                 & ~(BluetoothMapAppParams.FILTER_NO_SMS_GSM
2745                                                         | BluetoothMapAppParams.FILTER_NO_SMS_CDMA))
2746                                         | BluetoothMapAppParams.FILTER_NO_MMS)
2747                                 == 0)
2748                         && ap.getFilterRecipient() == null) {
2749                     // SMS/MMS messages only and no recipient filter - use optimization.
2750                     limit = " LIMIT " + ap.getMaxListCount() + " OFFSET " + ap.getStartOffset();
2751                     Log.d(TAG, "SMS Limit => " + limit);
2752                     offsetNum = 0;
2753                 }
2754                 StringBuilder selection = new StringBuilder(120); // This covers most cases
2755                 selection.append("1=1 "); // just to simplify building the where-clause
2756                 setConvoWhereFilterSmsMms(selection, fi, ap);
2757                 Uri uri =
2758                         Threads.CONTENT_URI
2759                                 .buildUpon()
2760                                 .appendQueryParameter("simple", "true")
2761                                 .build();
2762                 sortOrder.append(limit);
2763                 Log.d(
2764                         TAG,
2765                         "Query using selection: "
2766                                 + selection.toString()
2767                                 + " - sortOrder: "
2768                                 + sortOrder.toString());
2769                 // TODO: Optimize: Reduce projection based on convo parameter mask
2770                 smsMmsCursor =
2771                         BluetoothMethodProxy.getInstance()
2772                                 .contentResolverQuery(
2773                                         mResolver,
2774                                         uri,
2775                                         MMS_SMS_THREAD_PROJECTION,
2776                                         selection.toString(),
2777                                         null,
2778                                         sortOrder.toString());
2779                 if (smsMmsCursor != null) {
2780                     // store column index so we don't have to look them up anymore (optimization)
2781                     Log.d(TAG, "Found " + smsMmsCursor.getCount() + " sms/mms conversations.");
2782                     BluetoothMapConvoListingElement convoElement = null;
2783                     smsMmsCursor.moveToPosition(-1);
2784                     if (ap.getFilterRecipient() == null) {
2785                         int count = 0;
2786                         // We have no Recipient filter, add contacts after the list is reduced
2787                         while (smsMmsCursor.moveToNext()) {
2788                             convoElement = createConvoElement(smsMmsCursor, fi);
2789                             convoList.add(convoElement);
2790                             count++;
2791                             if (!sizeOnly && count >= maxThreads) {
2792                                 break;
2793                             }
2794                         }
2795                     } else {
2796                         // We must be able to filter on recipient, add contacts now
2797                         SmsMmsContacts contacts = new SmsMmsContacts();
2798                         while (smsMmsCursor.moveToNext()) {
2799                             int count = 0;
2800                             convoElement = createConvoElement(smsMmsCursor, fi);
2801                             String idsStr =
2802                                     smsMmsCursor.getString(MMS_SMS_THREAD_COL_RECIPIENT_IDS);
2803                             // Add elements only if we do find a contact - if not we cannot apply
2804                             // the filter, hence the item is irrelevant
2805                             // TODO: Perhaps the spec. should be changes to be able to search on
2806                             //       phone number as well?
2807                             if (addSmsMmsContacts(
2808                                     convoElement, contacts, idsStr, ap.getFilterRecipient(), ap)) {
2809                                 convoList.add(convoElement);
2810                                 if (!sizeOnly && count >= maxThreads) {
2811                                     break;
2812                                 }
2813                             }
2814                         }
2815                     }
2816                 }
2817             }
2818 
2819             if (emailSelected(ap) || imSelected(ap)) {
2820                 int count = 0;
2821                 if (emailSelected(ap)) {
2822                     fi.mMsgType = FilterInfo.TYPE_EMAIL;
2823                 } else if (imSelected(ap)) {
2824                     fi.mMsgType = FilterInfo.TYPE_IM;
2825                 }
2826                 Log.d(TAG, "msgType: " + fi.mMsgType);
2827                 Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_CONVERSATION);
2828 
2829                 contentUri = appendConvoListQueryParameters(ap, contentUri);
2830                 Log.v(TAG, "URI with parameters: " + contentUri.toString());
2831                 // TODO: Optimize: Reduce projection based on convo parameter mask
2832                 imEmailCursor =
2833                         BluetoothMethodProxy.getInstance()
2834                                 .contentResolverQuery(
2835                                         mResolver,
2836                                         contentUri,
2837                                         BluetoothMapContract.BT_CONVERSATION_PROJECTION,
2838                                         null,
2839                                         null,
2840                                         BluetoothMapContract.ConversationColumns
2841                                                         .LAST_THREAD_ACTIVITY
2842                                                 + " DESC, "
2843                                                 + BluetoothMapContract.ConversationColumns.THREAD_ID
2844                                                 + " ASC");
2845                 if (imEmailCursor != null) {
2846                     BluetoothMapConvoListingElement e = null;
2847                     // store column index so we don't have to look them up anymore (optimization)
2848                     // Here we rely on only a single account-based message type for each MAS.
2849                     fi.setEmailImConvoColumns(imEmailCursor);
2850                     boolean isValid = imEmailCursor.moveToNext();
2851                     Log.d(
2852                             TAG,
2853                             "Found "
2854                                     + imEmailCursor.getCount()
2855                                     + " EMAIL/IM conversations. isValid = "
2856                                     + isValid);
2857                     while (isValid && ((sizeOnly) || (count < maxThreads))) {
2858                         long threadId = imEmailCursor.getLong(fi.mConvoColConvoId);
2859                         long nextThreadId;
2860                         count++;
2861                         e = createConvoElement(imEmailCursor, fi);
2862                         convoList.add(e);
2863 
2864                         do {
2865                             nextThreadId = imEmailCursor.getLong(fi.mConvoColConvoId);
2866                             Log.v(
2867                                     TAG,
2868                                     "  threadId = " + threadId + " newThreadId = " + nextThreadId);
2869                             // TODO: This seems rather inefficient in the case where we do not need
2870                             //       to reduce the list.
2871                         } while ((nextThreadId == threadId)
2872                                 && (isValid = imEmailCursor.moveToNext()));
2873                     }
2874                 }
2875             }
2876 
2877             Log.d(TAG, "Done adding conversations - list size:" + convoList.getCount());
2878 
2879             // If sizeOnly - we are all done here - return the list as is - no need to populate the
2880             // list.
2881             if (sizeOnly) {
2882                 return convoList;
2883             }
2884 
2885             /* Enable this if post sorting and segmenting needed */
2886             /* This is too early */
2887             convoList.sort();
2888             convoList.segment(ap.getMaxListCount(), offsetNum);
2889             List<BluetoothMapConvoListingElement> list = convoList.getList();
2890             int listSize = list.size();
2891             Log.v(TAG, "List Size:" + listSize);
2892             Cursor tmpCursor = null;
2893             SmsMmsContacts contacts = new SmsMmsContacts();
2894             for (int x = 0; x < listSize; x++) {
2895                 BluetoothMapConvoListingElement ele = list.get(x);
2896                 TYPE type = ele.getType();
2897                 switch (type) {
2898                     case SMS_CDMA:
2899                     case SMS_GSM:
2900                     case MMS:
2901                         {
2902                             tmpCursor = null; // SMS/MMS needs special treatment
2903                             if (smsMmsCursor != null) {
2904                                 populateSmsMmsConvoElement(ele, smsMmsCursor, ap, contacts);
2905                             }
2906                             break;
2907                         }
2908                     case EMAIL:
2909                         tmpCursor = imEmailCursor;
2910                         fi.mMsgType = FilterInfo.TYPE_EMAIL;
2911                         break;
2912                     case IM:
2913                         tmpCursor = imEmailCursor;
2914                         fi.mMsgType = FilterInfo.TYPE_IM;
2915                         break;
2916                     default:
2917                         tmpCursor = null;
2918                         break;
2919                 }
2920 
2921                 Log.d(TAG, "Working on cursor of type " + fi.mMsgType);
2922 
2923                 if (tmpCursor != null) {
2924                     populateImEmailConvoElement(ele, tmpCursor, ap, fi);
2925                 } else {
2926                     // No, it will be for SMS/MMS at the moment
2927                     Log.d(
2928                             TAG,
2929                             "tmpCursor is Null - something is wrong - or the message is"
2930                                     + " of type SMS/MMS");
2931                 }
2932             }
2933         } finally {
2934             if (imEmailCursor != null) {
2935                 imEmailCursor.close();
2936             }
2937             if (smsMmsCursor != null) {
2938                 smsMmsCursor.close();
2939             }
2940             Log.d(TAG, "conversation end");
2941         }
2942         return convoList;
2943     }
2944 
2945     /**
2946      * Refreshes the entire list of SMS/MMS conversation version counters. Use it to generate a new
2947      * ConvoListVersionCounter in mSmsMmsConvoListVersion
2948      *
2949      * @return true if a list change has been detected
2950      */
refreshSmsMmsConvoVersions()2951     boolean refreshSmsMmsConvoVersions() {
2952         boolean listChangeDetected = false;
2953         Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
2954         Cursor cursor =
2955                 mResolver.query(uri, MMS_SMS_THREAD_PROJECTION, null, null, Threads.DATE + " DESC");
2956         try {
2957             if (cursor != null) {
2958                 // store column index so we don't have to look them up anymore (optimization)
2959                 Log.d(TAG, "Found " + cursor.getCount() + " sms/mms conversations.");
2960                 BluetoothMapConvoListingElement convoElement = null;
2961                 cursor.moveToPosition(-1);
2962                 synchronized (getSmsMmsConvoList()) {
2963                     int size = Math.max(getSmsMmsConvoList().size(), cursor.getCount());
2964                     Map<Long, BluetoothMapConvoListingElement> newList =
2965                             new HashMap<Long, BluetoothMapConvoListingElement>(size);
2966                     while (cursor.moveToNext()) {
2967                         // TODO: Extract to function, that can be called at listing, which returns
2968                         //       the versionCounter(existing or new).
2969                         boolean convoChanged = false;
2970                         Long id = cursor.getLong(MMS_SMS_THREAD_COL_ID);
2971                         convoElement = getSmsMmsConvoList().remove(id);
2972                         if (convoElement == null) {
2973                             // New conversation added
2974                             convoElement = new BluetoothMapConvoListingElement();
2975                             convoElement.setConvoId(BluetoothMapUtils.CONVO_ID_TYPE_SMS_MMS, id);
2976                             listChangeDetected = true;
2977                             convoElement.setVersionCounter(0);
2978                         }
2979                         // Currently we only need to compare name, lastActivity and read_status, and
2980                         // name is not used for SMS/MMS.
2981                         // msg delete will be handled by update folderVersionCounter().
2982                         long lastActivity = cursor.getLong(MMS_SMS_THREAD_COL_DATE);
2983                         boolean read = cursor.getInt(MMS_SMS_THREAD_COL_READ) == 1;
2984 
2985                         if (lastActivity != convoElement.getLastActivity()) {
2986                             convoChanged = true;
2987                             convoElement.setLastActivity(lastActivity);
2988                         }
2989 
2990                         if (read != convoElement.getReadBool()) {
2991                             convoChanged = true;
2992                             convoElement.setRead(read, false);
2993                         }
2994 
2995                         String idsStr = cursor.getString(MMS_SMS_THREAD_COL_RECIPIENT_IDS);
2996                         if (!idsStr.equals(convoElement.getSmsMmsContacts())) {
2997                             // This should not trigger a change in conversationVersionCounter
2998                             // only the
2999                             // ConvoListVersionCounter.
3000                             listChangeDetected = true;
3001                             convoElement.setSmsMmsContacts(idsStr);
3002                         }
3003 
3004                         if (convoChanged) {
3005                             listChangeDetected = true;
3006                             convoElement.incrementVersionCounter();
3007                         }
3008                         newList.put(id, convoElement);
3009                     }
3010                     // If we still have items on the old list, something was deleted
3011                     if (getSmsMmsConvoList().size() != 0) {
3012                         listChangeDetected = true;
3013                     }
3014                     setSmsMmsConvoList(newList);
3015                 }
3016 
3017                 if (listChangeDetected) {
3018                     mMasInstance.updateSmsMmsConvoListVersionCounter();
3019                 }
3020             }
3021         } finally {
3022             if (cursor != null) {
3023                 cursor.close();
3024             }
3025         }
3026         return listChangeDetected;
3027     }
3028 
3029     /**
3030      * Refreshes the entire list of SMS/MMS conversation version counters. Use it to generate a new
3031      * ConvoListVersionCounter in mSmsMmsConvoListVersion
3032      *
3033      * @return true if a list change has been detected
3034      */
refreshImEmailConvoVersions()3035     boolean refreshImEmailConvoVersions() {
3036         boolean listChangeDetected = false;
3037         FilterInfo fi = new FilterInfo();
3038 
3039         Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_CONVERSATION);
3040 
3041         Log.v(TAG, "URI with parameters: " + contentUri.toString());
3042         Cursor imEmailCursor =
3043                 mResolver.query(
3044                         contentUri,
3045                         CONVO_VERSION_PROJECTION,
3046                         null,
3047                         null,
3048                         BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY
3049                                 + " DESC, "
3050                                 + BluetoothMapContract.ConversationColumns.THREAD_ID
3051                                 + " ASC");
3052         try {
3053             if (imEmailCursor != null) {
3054                 BluetoothMapConvoListingElement convoElement = null;
3055                 // store column index so we don't have to look them up anymore (optimization)
3056                 // Here we rely on only a single account-based message type for each MAS.
3057                 fi.setEmailImConvoColumns(imEmailCursor);
3058                 boolean isValid = imEmailCursor.moveToNext();
3059                 Log.v(
3060                         TAG,
3061                         "Found "
3062                                 + imEmailCursor.getCount()
3063                                 + " EMAIL/IM conversations. isValid = "
3064                                 + isValid);
3065                 synchronized (getImEmailConvoList()) {
3066                     int size = Math.max(getImEmailConvoList().size(), imEmailCursor.getCount());
3067                     boolean convoChanged = false;
3068                     Map<Long, BluetoothMapConvoListingElement> newList =
3069                             new HashMap<Long, BluetoothMapConvoListingElement>(size);
3070                     while (isValid) {
3071                         long id = imEmailCursor.getLong(fi.mConvoColConvoId);
3072                         long nextThreadId;
3073                         convoElement = getImEmailConvoList().remove(id);
3074                         if (convoElement == null) {
3075                             // New conversation added
3076                             convoElement = new BluetoothMapConvoListingElement();
3077                             convoElement.setConvoId(BluetoothMapUtils.CONVO_ID_TYPE_EMAIL_IM, id);
3078                             listChangeDetected = true;
3079                             convoElement.setVersionCounter(0);
3080                         }
3081                         String name = imEmailCursor.getString(fi.mConvoColName);
3082                         String summary = imEmailCursor.getString(fi.mConvoColSummary);
3083                         long lastActivity = imEmailCursor.getLong(fi.mConvoColLastActivity);
3084                         boolean read = imEmailCursor.getInt(fi.mConvoColRead) == 1;
3085 
3086                         if (lastActivity != convoElement.getLastActivity()) {
3087                             convoChanged = true;
3088                             convoElement.setLastActivity(lastActivity);
3089                         }
3090 
3091                         if (read != convoElement.getReadBool()) {
3092                             convoChanged = true;
3093                             convoElement.setRead(read, false);
3094                         }
3095 
3096                         if (name != null && !name.equals(convoElement.getName())) {
3097                             convoChanged = true;
3098                             convoElement.setName(name);
3099                         }
3100 
3101                         if (summary != null && !summary.equals(convoElement.getFullSummary())) {
3102                             convoChanged = true;
3103                             convoElement.setSummary(summary);
3104                         }
3105                         /* If the query returned one row for each contact, skip all the
3106                         duplicates */
3107                         do {
3108                             nextThreadId = imEmailCursor.getLong(fi.mConvoColConvoId);
3109                             Log.v(TAG, "  threadId = " + id + " newThreadId = " + nextThreadId);
3110                         } while ((nextThreadId == id) && (isValid = imEmailCursor.moveToNext()));
3111 
3112                         if (convoChanged) {
3113                             listChangeDetected = true;
3114                             convoElement.incrementVersionCounter();
3115                         }
3116                         newList.put(id, convoElement);
3117                     }
3118                     // If we still have items on the old list, something was deleted
3119                     if (getImEmailConvoList().size() != 0) {
3120                         listChangeDetected = true;
3121                     }
3122                     setImEmailConvoList(newList);
3123                 }
3124             }
3125         } finally {
3126             if (imEmailCursor != null) {
3127                 imEmailCursor.close();
3128             }
3129         }
3130 
3131         if (listChangeDetected) {
3132             mMasInstance.updateImEmailConvoListVersionCounter();
3133         }
3134         return listChangeDetected;
3135     }
3136 
3137     /**
3138      * Update the convoVersionCounter within the element passed as parameter. This function has the
3139      * side effect to update the ConvoListVersionCounter if needed. This function ignores changes to
3140      * contacts as this shall not change the convoVersionCounter, only the convoListVersion counter,
3141      * which will be updated upon request.
3142      *
3143      * @param ele Element to update shall not be null.
3144      */
updateSmsMmsConvoVersion(Cursor cursor, BluetoothMapConvoListingElement ele)3145     private void updateSmsMmsConvoVersion(Cursor cursor, BluetoothMapConvoListingElement ele) {
3146         long id = ele.getCpConvoId();
3147         BluetoothMapConvoListingElement convoElement = getSmsMmsConvoList().get(id);
3148         boolean listChangeDetected = false;
3149         boolean convoChanged = false;
3150         if (convoElement == null) {
3151             // New conversation added
3152             convoElement = new BluetoothMapConvoListingElement();
3153             getSmsMmsConvoList().put(id, convoElement);
3154             convoElement.setConvoId(BluetoothMapUtils.CONVO_ID_TYPE_SMS_MMS, id);
3155             listChangeDetected = true;
3156             convoElement.setVersionCounter(0);
3157         }
3158         long lastActivity = cursor.getLong(MMS_SMS_THREAD_COL_DATE);
3159         boolean read = cursor.getInt(MMS_SMS_THREAD_COL_READ) == 1;
3160 
3161         if (lastActivity != convoElement.getLastActivity()) {
3162             convoChanged = true;
3163             convoElement.setLastActivity(lastActivity);
3164         }
3165 
3166         if (read != convoElement.getReadBool()) {
3167             convoChanged = true;
3168             convoElement.setRead(read, false);
3169         }
3170 
3171         if (convoChanged) {
3172             listChangeDetected = true;
3173             convoElement.incrementVersionCounter();
3174         }
3175         if (listChangeDetected) {
3176             mMasInstance.updateSmsMmsConvoListVersionCounter();
3177         }
3178         ele.setVersionCounter(convoElement.getVersionCounter());
3179     }
3180 
3181     /**
3182      * Update the convoVersionCounter within the element passed as parameter. This function has the
3183      * side effect to update the ConvoListVersionCounter if needed. This function ignores changes to
3184      * contacts as this shall not change the convoVersionCounter, only the convoListVersion counter,
3185      * which will be updated upon request.
3186      *
3187      * @param ele Element to update shall not be null.
3188      */
updateImEmailConvoVersion( Cursor cursor, FilterInfo fi, BluetoothMapConvoListingElement ele)3189     private void updateImEmailConvoVersion(
3190             Cursor cursor, FilterInfo fi, BluetoothMapConvoListingElement ele) {
3191         long id = ele.getCpConvoId();
3192         BluetoothMapConvoListingElement convoElement = getImEmailConvoList().get(id);
3193         boolean listChangeDetected = false;
3194         boolean convoChanged = false;
3195         if (convoElement == null) {
3196             // New conversation added
3197             Log.v(TAG, "Added new conversation with ID = " + id);
3198             convoElement = new BluetoothMapConvoListingElement();
3199             convoElement.setConvoId(BluetoothMapUtils.CONVO_ID_TYPE_EMAIL_IM, id);
3200             getImEmailConvoList().put(id, convoElement);
3201             listChangeDetected = true;
3202             convoElement.setVersionCounter(0);
3203         }
3204         String name = cursor.getString(fi.mConvoColName);
3205         long lastActivity = cursor.getLong(fi.mConvoColLastActivity);
3206         boolean read = cursor.getInt(fi.mConvoColRead) == 1;
3207 
3208         if (lastActivity != convoElement.getLastActivity()) {
3209             convoChanged = true;
3210             convoElement.setLastActivity(lastActivity);
3211         }
3212 
3213         if (read != convoElement.getReadBool()) {
3214             convoChanged = true;
3215             convoElement.setRead(read, false);
3216         }
3217 
3218         if (name != null && !name.equals(convoElement.getName())) {
3219             convoChanged = true;
3220             convoElement.setName(name);
3221         }
3222 
3223         if (convoChanged) {
3224             listChangeDetected = true;
3225             Log.v(TAG, "conversation with ID = " + id + " changed");
3226             convoElement.incrementVersionCounter();
3227         }
3228         if (listChangeDetected) {
3229             mMasInstance.updateImEmailConvoListVersionCounter();
3230         }
3231         ele.setVersionCounter(convoElement.getVersionCounter());
3232     }
3233 
populateSmsMmsConvoElement( BluetoothMapConvoListingElement ele, Cursor smsMmsCursor, BluetoothMapAppParams ap, SmsMmsContacts contacts)3234     private void populateSmsMmsConvoElement(
3235             BluetoothMapConvoListingElement ele,
3236             Cursor smsMmsCursor,
3237             BluetoothMapAppParams ap,
3238             SmsMmsContacts contacts) {
3239         smsMmsCursor.moveToPosition(ele.getCursorIndex());
3240         // TODO: If we ever get beyond 31 bit, change to long
3241         int parameterMask = (int) ap.getConvoParameterMask(); // We always set a default value
3242 
3243         // TODO: How to determine whether the convo-IDs can be used across message
3244         //       types?
3245         ele.setConvoId(
3246                 BluetoothMapUtils.CONVO_ID_TYPE_SMS_MMS,
3247                 smsMmsCursor.getLong(MMS_SMS_THREAD_COL_ID));
3248 
3249         boolean read = smsMmsCursor.getInt(MMS_SMS_THREAD_COL_READ) == 1;
3250         if ((parameterMask & CONVO_PARAM_MASK_CONVO_READ_STATUS) != 0) {
3251             ele.setRead(read, true);
3252         } else {
3253             ele.setRead(read, false);
3254         }
3255 
3256         if ((parameterMask & CONVO_PARAM_MASK_CONVO_LAST_ACTIVITY) != 0) {
3257             long timeStamp = smsMmsCursor.getLong(MMS_SMS_THREAD_COL_DATE);
3258             ele.setLastActivity(timeStamp);
3259         } else {
3260             // We need to delete the time stamp, if it was added for multi msg-type
3261             ele.setLastActivity(-1);
3262         }
3263 
3264         if ((parameterMask & CONVO_PARAM_MASK_CONVO_VERSION_COUNTER) != 0) {
3265             updateSmsMmsConvoVersion(smsMmsCursor, ele);
3266         }
3267 
3268         if ((parameterMask & CONVO_PARAM_MASK_CONVO_NAME) != 0) {
3269             ele.setName(""); // We never have a thread name for SMS/MMS
3270         }
3271 
3272         if ((parameterMask & CONVO_PARAM_MASK_CONVO_SUMMARY) != 0) {
3273             String summary = smsMmsCursor.getString(MMS_SMS_THREAD_COL_SNIPPET);
3274             String cs = smsMmsCursor.getString(MMS_SMS_THREAD_COL_SNIPPET_CS);
3275             if (summary != null && cs != null && !cs.equals("UTF-8")) {
3276                 try {
3277                     // TODO: Not sure this is how to convert to UTF-8
3278                     summary = new String(summary.getBytes(cs), StandardCharsets.UTF_8);
3279                 } catch (UnsupportedEncodingException e) {
3280                     ContentProfileErrorReportUtils.report(
3281                             BluetoothProfile.MAP,
3282                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
3283                             BluetoothStatsLog
3284                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3285                             3);
3286                     Log.e(TAG, "populateSmsMmsConvoElement: " + e);
3287                 }
3288             }
3289             ele.setSummary(summary);
3290         }
3291 
3292         if ((parameterMask & CONVO_PARAM_MASK_PARTICIPANTS) != 0) {
3293             if (ap.getFilterRecipient() == null) {
3294                 // Add contacts only if not already added
3295                 String idsStr = smsMmsCursor.getString(MMS_SMS_THREAD_COL_RECIPIENT_IDS);
3296                 addSmsMmsContacts(ele, contacts, idsStr, null, ap);
3297             }
3298         }
3299     }
3300 
populateImEmailConvoElement( BluetoothMapConvoListingElement ele, Cursor tmpCursor, BluetoothMapAppParams ap, FilterInfo fi)3301     private void populateImEmailConvoElement(
3302             BluetoothMapConvoListingElement ele,
3303             Cursor tmpCursor,
3304             BluetoothMapAppParams ap,
3305             FilterInfo fi) {
3306         tmpCursor.moveToPosition(ele.getCursorIndex());
3307         // TODO: If we ever get beyond 31 bit, change to long
3308         int parameterMask = (int) ap.getConvoParameterMask(); // We always set a default value
3309         long threadId = tmpCursor.getLong(fi.mConvoColConvoId);
3310 
3311         // Mandatory field
3312         ele.setConvoId(BluetoothMapUtils.CONVO_ID_TYPE_EMAIL_IM, threadId);
3313 
3314         if ((parameterMask & CONVO_PARAM_MASK_CONVO_NAME) != 0) {
3315             ele.setName(tmpCursor.getString(fi.mConvoColName));
3316         }
3317 
3318         boolean reportRead = false;
3319         if ((parameterMask & CONVO_PARAM_MASK_CONVO_READ_STATUS) != 0) {
3320             reportRead = true;
3321         }
3322         ele.setRead((1 == tmpCursor.getInt(fi.mConvoColRead)), reportRead);
3323 
3324         long timestamp = tmpCursor.getLong(fi.mConvoColLastActivity);
3325         if ((parameterMask & CONVO_PARAM_MASK_CONVO_LAST_ACTIVITY) != 0) {
3326             ele.setLastActivity(timestamp);
3327         } else {
3328             // We need to delete the time stamp, if it was added for multi msg-type
3329             ele.setLastActivity(-1);
3330         }
3331 
3332         if ((parameterMask & CONVO_PARAM_MASK_CONVO_VERSION_COUNTER) != 0) {
3333             updateImEmailConvoVersion(tmpCursor, fi, ele);
3334         }
3335         if ((parameterMask & CONVO_PARAM_MASK_CONVO_SUMMARY) != 0) {
3336             ele.setSummary(tmpCursor.getString(fi.mConvoColSummary));
3337         }
3338         // TODO: For optimization, we could avoid joining the contact and convo tables
3339         //       if we have no filter nor this bit is set.
3340         if ((parameterMask & CONVO_PARAM_MASK_PARTICIPANTS) != 0) {
3341             do {
3342                 BluetoothMapConvoContactElement c = new BluetoothMapConvoContactElement();
3343                 if ((parameterMask & CONVO_PARAM_MASK_PART_X_BT_UID) != 0) {
3344                     c.setBtUid(new SignedLongLong(tmpCursor.getLong(fi.mContactColBtUid), 0));
3345                 }
3346                 if ((parameterMask & CONVO_PARAM_MASK_PART_CHAT_STATE) != 0) {
3347                     c.setChatState(tmpCursor.getInt(fi.mContactColChatState));
3348                 }
3349                 if ((parameterMask & CONVO_PARAM_MASK_PART_PRESENCE) != 0) {
3350                     c.setPresenceAvailability(tmpCursor.getInt(fi.mContactColPresenceState));
3351                 }
3352                 if ((parameterMask & CONVO_PARAM_MASK_PART_PRESENCE_TEXT) != 0) {
3353                     c.setPresenceStatus(tmpCursor.getString(fi.mContactColPresenceText));
3354                 }
3355                 if ((parameterMask & CONVO_PARAM_MASK_PART_PRIORITY) != 0) {
3356                     c.setPriority(tmpCursor.getInt(fi.mContactColPriority));
3357                 }
3358                 if ((parameterMask & CONVO_PARAM_MASK_PART_DISP_NAME) != 0) {
3359                     c.setDisplayName(tmpCursor.getString(fi.mContactColNickname));
3360                 }
3361                 if ((parameterMask & CONVO_PARAM_MASK_PART_UCI) != 0) {
3362                     c.setContactId(tmpCursor.getString(fi.mContactColContactUci));
3363                 }
3364                 if ((parameterMask & CONVO_PARAM_MASK_PART_LAST_ACTIVITY) != 0) {
3365                     c.setLastActivity(tmpCursor.getLong(fi.mContactColLastActive));
3366                 }
3367                 if ((parameterMask & CONVO_PARAM_MASK_PART_NAME) != 0) {
3368                     c.setName(tmpCursor.getString(fi.mContactColName));
3369                 }
3370                 ele.addContact(c);
3371             } while (tmpCursor.moveToNext() && tmpCursor.getLong(fi.mConvoColConvoId) == threadId);
3372         }
3373     }
3374 
3375     /**
3376      * Extract the ConvoList parameters from appParams and build the matching URI with query
3377      * parameters.
3378      *
3379      * @param ap the appParams from the request
3380      * @param contentUri the URI to append parameters to
3381      * @return the new URI with the appended parameters (if any)
3382      */
appendConvoListQueryParameters(BluetoothMapAppParams ap, Uri contentUri)3383     private static Uri appendConvoListQueryParameters(BluetoothMapAppParams ap, Uri contentUri) {
3384         Uri.Builder newUri = contentUri.buildUpon();
3385         String str = ap.getFilterRecipient();
3386         if (str != null) {
3387             str = str.trim();
3388             str = str.replace("*", "%");
3389             newUri.appendQueryParameter(BluetoothMapContract.FILTER_ORIGINATOR_SUBSTRING, str);
3390         }
3391         long time = ap.getFilterLastActivityBegin();
3392         if (time > 0) {
3393             newUri.appendQueryParameter(
3394                     BluetoothMapContract.FILTER_PERIOD_BEGIN, Long.toString(time));
3395         }
3396         time = ap.getFilterLastActivityEnd();
3397         if (time > 0) {
3398             newUri.appendQueryParameter(
3399                     BluetoothMapContract.FILTER_PERIOD_END, Long.toString(time));
3400         }
3401         int readStatus = ap.getFilterReadStatus();
3402         if (readStatus > 0) {
3403             if (readStatus == 1) {
3404                 // Conversations with Unread messages only
3405                 newUri.appendQueryParameter(BluetoothMapContract.FILTER_READ_STATUS, "false");
3406             } else if (readStatus == 2) {
3407                 // Conversations with all read messages only
3408                 newUri.appendQueryParameter(BluetoothMapContract.FILTER_READ_STATUS, "true");
3409             }
3410             // if both are set it will be the same as requesting an empty list, but
3411             // as it makes no sense with such a structure in a bit mask, we treat
3412             // requesting both the same as no filtering.
3413         }
3414         long convoId = -1;
3415         if (ap.getFilterConvoId() != null) {
3416             convoId = ap.getFilterConvoId().leastSignificantBits();
3417         }
3418         if (convoId > 0) {
3419             newUri.appendQueryParameter(
3420                     BluetoothMapContract.FILTER_THREAD_ID, Long.toString(convoId));
3421         }
3422         return newUri.build();
3423     }
3424 
3425     /**
3426      * Procedure if we have a filter: - loop through all ids to examine if there is a match (this
3427      * will build the cache) - If there is a match loop again to add all contacts.
3428      *
3429      * <p>Procedure if we don't have a filter - Add all contacts
3430      */
addSmsMmsContacts( BluetoothMapConvoListingElement convoElement, SmsMmsContacts contacts, String idsStr, String recipientFilter, BluetoothMapAppParams ap)3431     private boolean addSmsMmsContacts(
3432             BluetoothMapConvoListingElement convoElement,
3433             SmsMmsContacts contacts,
3434             String idsStr,
3435             String recipientFilter,
3436             BluetoothMapAppParams ap) {
3437         BluetoothMapConvoContactElement contactElement;
3438         int parameterMask = (int) ap.getConvoParameterMask(); // We always set a default value
3439         boolean foundContact = false;
3440         String[] ids = idsStr.split(" ");
3441         long[] longIds = new long[ids.length];
3442         if (recipientFilter != null) {
3443             recipientFilter = recipientFilter.trim();
3444         }
3445 
3446         for (int i = 0; i < ids.length; i++) {
3447             long longId;
3448             try {
3449                 longId = Long.parseLong(ids[i]);
3450                 longIds[i] = longId;
3451                 if (recipientFilter == null) {
3452                     // If there is not filter, all we need to do is to parse the ids
3453                     foundContact = true;
3454                     continue;
3455                 }
3456                 String addr = contacts.getPhoneNumber(mResolver, longId);
3457                 if (addr == null) {
3458                     // This can only happen if all messages from a contact is deleted while
3459                     // performing the query.
3460                     continue;
3461                 }
3462                 MapContact contact =
3463                         contacts.getContactNameFromPhone(addr, mResolver, recipientFilter);
3464                 Log.d(
3465                         TAG,
3466                         "id: "
3467                                 + longId
3468                                 + ", addr: "
3469                                 + addr
3470                                 + ", contact name: "
3471                                 + (contact != null
3472                                         ? contact.name() + ", X-BT-UID: " + contact.getXBtUid()
3473                                         : "null"));
3474                 if (contact == null) {
3475                     continue;
3476                 }
3477                 foundContact = true;
3478             } catch (NumberFormatException ex) {
3479                 ContentProfileErrorReportUtils.report(
3480                         BluetoothProfile.MAP,
3481                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
3482                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3483                         4);
3484             }
3485         }
3486 
3487         if (foundContact) {
3488             foundContact = false;
3489             for (long id : longIds) {
3490                 String addr = contacts.getPhoneNumber(mResolver, id);
3491                 if (addr == null) {
3492                     // This can only happen if all messages from a contact is deleted while
3493                     // performing the query.
3494                     continue;
3495                 }
3496                 foundContact = true;
3497                 MapContact contact = contacts.getContactNameFromPhone(addr, mResolver);
3498 
3499                 if (contact == null) {
3500                     // We do not have a contact, we need to manually add one
3501                     contactElement = new BluetoothMapConvoContactElement();
3502                     if ((parameterMask & CONVO_PARAM_MASK_PART_NAME) != 0) {
3503                         contactElement.setName(addr); // Use the phone number as name
3504                     }
3505                     if ((parameterMask & CONVO_PARAM_MASK_PART_UCI) != 0) {
3506                         contactElement.setContactId(addr);
3507                     }
3508                 } else {
3509                     contactElement =
3510                             BluetoothMapConvoContactElement.createFromMapContact(contact, addr);
3511                     // Remove the parameters not to be reported
3512                     if ((parameterMask & CONVO_PARAM_MASK_PART_UCI) == 0) {
3513                         contactElement.setContactId(null);
3514                     }
3515                     if ((parameterMask & CONVO_PARAM_MASK_PART_X_BT_UID) == 0) {
3516                         contactElement.setBtUid(null);
3517                     }
3518                     if ((parameterMask & CONVO_PARAM_MASK_PART_DISP_NAME) == 0) {
3519                         contactElement.setDisplayName(null);
3520                     }
3521                 }
3522                 convoElement.addContact(contactElement);
3523             }
3524         }
3525         return foundContact;
3526     }
3527 
3528     /**
3529      * Get the folder name of an SMS message or MMS message.
3530      *
3531      * @return the folder name.
3532      */
getFolderName(int type, int threadId)3533     private static String getFolderName(int type, int threadId) {
3534 
3535         if (threadId == -1) {
3536             return BluetoothMapContract.FOLDER_NAME_DELETED;
3537         }
3538 
3539         switch (type) {
3540             case 1:
3541                 return BluetoothMapContract.FOLDER_NAME_INBOX;
3542             case 2:
3543                 return BluetoothMapContract.FOLDER_NAME_SENT;
3544             case 3:
3545                 return BluetoothMapContract.FOLDER_NAME_DRAFT;
3546             case 4: // Just name outbox, failed and queued "outbox"
3547             case 5:
3548             case 6:
3549                 return BluetoothMapContract.FOLDER_NAME_OUTBOX;
3550         }
3551         return "";
3552     }
3553 
getMessage( String handle, BluetoothMapAppParams appParams, BluetoothMapFolderElement folderElement, String version)3554     public byte[] getMessage(
3555             String handle,
3556             BluetoothMapAppParams appParams,
3557             BluetoothMapFolderElement folderElement,
3558             String version)
3559             throws UnsupportedEncodingException {
3560         TYPE type = BluetoothMapUtils.getMsgTypeFromHandle(handle);
3561         mMessageVersion = version;
3562         long id = BluetoothMapUtils.getCpHandle(handle);
3563         if (appParams.getFractionRequest() == BluetoothMapAppParams.FRACTION_REQUEST_NEXT) {
3564             throw new IllegalArgumentException(
3565                     "FRACTION_REQUEST_NEXT does not make sense as"
3566                             + " we always return the full message.");
3567         }
3568         switch (type) {
3569             case SMS_GSM:
3570             case SMS_CDMA:
3571                 return getSmsMessage(id, appParams.getCharset());
3572             case MMS:
3573                 return getMmsMessage(id, appParams);
3574             case EMAIL:
3575                 return getEmailMessage(id, appParams, folderElement);
3576             case IM:
3577                 return getIMMessage(id, appParams, folderElement);
3578             default:
3579                 throw new IllegalArgumentException("Invalid message handle.");
3580         }
3581     }
3582 
setVCardFromPhoneNumber( BluetoothMapbMessage message, String phone, boolean incoming)3583     private String setVCardFromPhoneNumber(
3584             BluetoothMapbMessage message, String phone, boolean incoming) {
3585         String contactId = null, contactName = null;
3586         String[] phoneNumbers = new String[1];
3587         // Handle possible exception for empty phone address
3588         if (TextUtils.isEmpty(phone)) {
3589             return contactName;
3590         }
3591         //
3592         // Use only actual phone number, because the MCE cannot know which
3593         // number the message is from.
3594         //
3595         phoneNumbers[0] = phone;
3596         String[] emailAddresses = null;
3597         Cursor p;
3598 
3599         Uri uri =
3600                 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone));
3601 
3602         String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME};
3603         String selection = Contacts.IN_VISIBLE_GROUP + "=1";
3604         String orderBy = Contacts._ID + " ASC";
3605 
3606         // Get the contact _ID and name
3607         p =
3608                 BluetoothMethodProxy.getInstance()
3609                         .contentResolverQuery(mResolver, uri, projection, selection, null, orderBy);
3610         try {
3611             if (p != null && p.moveToFirst()) {
3612                 contactId = p.getString(p.getColumnIndex(Contacts._ID));
3613                 contactName = p.getString(p.getColumnIndex(Contacts.DISPLAY_NAME));
3614             }
3615         } finally {
3616             close(p);
3617         }
3618         // Bail out if we are unable to find a contact, based on the phone number
3619         if (contactId != null) {
3620             Cursor q = null;
3621             // Fetch the contact e-mail addresses
3622             try {
3623                 q =
3624                         BluetoothMethodProxy.getInstance()
3625                                 .contentResolverQuery(
3626                                         mResolver,
3627                                         ContactsContract.CommonDataKinds.Email.CONTENT_URI,
3628                                         null,
3629                                         ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
3630                                         new String[] {contactId},
3631                                         null);
3632                 if (q != null && q.moveToFirst()) {
3633                     int i = 0;
3634                     emailAddresses = new String[q.getCount()];
3635                     do {
3636                         String emailAddress =
3637                                 q.getString(
3638                                         q.getColumnIndex(
3639                                                 ContactsContract.CommonDataKinds.Email.ADDRESS));
3640                         emailAddresses[i++] = emailAddress;
3641                     } while (q != null && q.moveToNext());
3642                 }
3643             } finally {
3644                 close(q);
3645             }
3646         }
3647 
3648         if (incoming) {
3649             Log.v(TAG, "Adding originator for phone:" + phone);
3650             // Use version 3.0 as we only have a formatted name
3651             message.addOriginator(
3652                     contactName, contactName, phoneNumbers, emailAddresses, null, null);
3653         } else {
3654             Log.v(TAG, "Adding recipient for phone:" + phone);
3655             // Use version 3.0 as we only have a formatted name
3656             message.addRecipient(
3657                     contactName, contactName, phoneNumbers, emailAddresses, null, null);
3658         }
3659         return contactName;
3660     }
3661 
3662     public static final int MAP_MESSAGE_CHARSET_NATIVE = 0;
3663     public static final int MAP_MESSAGE_CHARSET_UTF8 = 1;
3664 
getSmsMessage(long id, int charset)3665     public byte[] getSmsMessage(long id, int charset) throws UnsupportedEncodingException {
3666         int type, threadId;
3667         long time = -1;
3668         String msgBody;
3669         BluetoothMapbMessageSms message = new BluetoothMapbMessageSms();
3670         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
3671 
3672         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, "_ID = " + id, null, null);
3673         if (c == null || !c.moveToFirst()) {
3674             throw new IllegalArgumentException("SMS handle not found");
3675         }
3676 
3677         try {
3678             if (c != null && c.moveToFirst()) {
3679                 Log.v(TAG, "c.count: " + c.getCount());
3680 
3681                 if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
3682                     message.setType(TYPE.SMS_CDMA);
3683                 } else {
3684                     // set SMS_GSM by default
3685                     message.setType(TYPE.SMS_GSM);
3686                 }
3687                 message.setVersionString(mMessageVersion);
3688                 String read = c.getString(c.getColumnIndex(Sms.READ));
3689                 if (read.equalsIgnoreCase("1")) {
3690                     message.setStatus(true);
3691                 } else {
3692                     message.setStatus(false);
3693                 }
3694 
3695                 type = c.getInt(c.getColumnIndex(Sms.TYPE));
3696                 threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
3697                 message.setFolder(getFolderName(type, threadId));
3698 
3699                 msgBody = c.getString(c.getColumnIndex(Sms.BODY));
3700 
3701                 String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
3702                 if ((phone == null) && type == Sms.MESSAGE_TYPE_DRAFT) {
3703                     // Fetch address for Drafts folder from "canonical_address" table
3704                     phone = getCanonicalAddressSms(mResolver, threadId);
3705                 }
3706                 time = c.getLong(c.getColumnIndex(Sms.DATE));
3707                 if (type == 1) { // Inbox message needs to set the vCard as originator
3708                     setVCardFromPhoneNumber(message, phone, true);
3709                 } else { // Other messages sets the vCard as the recipient
3710                     setVCardFromPhoneNumber(message, phone, false);
3711                 }
3712                 if (charset == MAP_MESSAGE_CHARSET_NATIVE) {
3713                     if (type == 1) { // Inbox
3714                         message.setSmsBodyPdus(
3715                                 BluetoothMapSmsPdu.getDeliverPdus(mContext, msgBody, phone, time));
3716                     } else {
3717                         message.setSmsBodyPdus(
3718                                 BluetoothMapSmsPdu.getSubmitPdus(mContext, msgBody, phone));
3719                     }
3720                 } else /*if (charset == MAP_MESSAGE_CHARSET_UTF8)*/ {
3721                     message.setSmsBody(msgBody);
3722                 }
3723                 return message.encode();
3724             }
3725         } finally {
3726             if (c != null) {
3727                 c.close();
3728             }
3729         }
3730 
3731         return message.encode();
3732     }
3733 
3734     @VisibleForTesting
extractMmsAddresses(long id, BluetoothMapbMessageMime message)3735     void extractMmsAddresses(long id, BluetoothMapbMessageMime message) {
3736         final String[] projection = null;
3737         String selection = new String(Mms.Addr.MSG_ID + "=" + id);
3738         String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/addr");
3739         Uri uriAddress = Uri.parse(uriStr);
3740         String contactName = null;
3741 
3742         Cursor c =
3743                 BluetoothMethodProxy.getInstance()
3744                         .contentResolverQuery(
3745                                 mResolver, uriAddress, projection, selection, null, null);
3746         try {
3747             if (c.moveToFirst()) {
3748                 do {
3749                     String address = c.getString(c.getColumnIndex(Mms.Addr.ADDRESS));
3750                     if (address.equals(INSERT_ADDRESS_TOKEN)) {
3751                         continue;
3752                     }
3753                     Integer type = c.getInt(c.getColumnIndex(Mms.Addr.TYPE));
3754                     switch (type) {
3755                         case MMS_FROM:
3756                             contactName = setVCardFromPhoneNumber(message, address, true);
3757                             message.addFrom(contactName, address);
3758                             break;
3759                         case MMS_TO:
3760                             contactName = setVCardFromPhoneNumber(message, address, false);
3761                             message.addTo(contactName, address);
3762                             break;
3763                         case MMS_CC:
3764                             contactName = setVCardFromPhoneNumber(message, address, false);
3765                             message.addCc(contactName, address);
3766                             break;
3767                         case MMS_BCC:
3768                             contactName = setVCardFromPhoneNumber(message, address, false);
3769                             message.addBcc(contactName, address);
3770                             break;
3771                         default:
3772                             break;
3773                     }
3774                 } while (c.moveToNext());
3775             }
3776         } finally {
3777             if (c != null) {
3778                 c.close();
3779             }
3780         }
3781     }
3782 
3783     /**
3784      * Read out a mime data part and return the data in a byte array.
3785      *
3786      * @param contentPartUri TODO
3787      * @param partid the content provider id of the Mime Part.
3788      */
readRawDataPart(Uri contentPartUri, long partid)3789     private byte[] readRawDataPart(Uri contentPartUri, long partid) {
3790         String uriStr = new String(contentPartUri + "/" + partid);
3791         Uri uriAddress = Uri.parse(uriStr);
3792         InputStream is = null;
3793         ByteArrayOutputStream os = new ByteArrayOutputStream();
3794         int bufferSize = 8192;
3795         byte[] buffer = new byte[bufferSize];
3796         byte[] retVal = null;
3797 
3798         try {
3799             is = mResolver.openInputStream(uriAddress);
3800             int len = 0;
3801             while ((len = is.read(buffer)) != -1) {
3802                 os.write(buffer, 0, len); // We need to specify the len, as it can be != bufferSize
3803             }
3804             retVal = os.toByteArray();
3805         } catch (IOException e) {
3806             ContentProfileErrorReportUtils.report(
3807                     BluetoothProfile.MAP,
3808                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
3809                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3810                     5);
3811             // do nothing for now
3812             Log.w(TAG, "Error reading part data", e);
3813         } finally {
3814             close(os);
3815             close(is);
3816         }
3817         return retVal;
3818     }
3819 
3820     /**
3821      * Read out the mms parts and update the bMessage object provided i {@code message}
3822      *
3823      * @param id the content provider ID of the message
3824      * @param message the bMessage object to add the information to
3825      */
3826     @VisibleForTesting
extractMmsParts(long id, BluetoothMapbMessageMime message)3827     void extractMmsParts(long id, BluetoothMapbMessageMime message) {
3828         final String[] projection = null;
3829         String selection = new String(Mms.Part.MSG_ID + "=" + id);
3830         String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/part");
3831         Uri uriAddress = Uri.parse(uriStr);
3832         BluetoothMapbMessageMime.MimePart part;
3833         Cursor c =
3834                 BluetoothMethodProxy.getInstance()
3835                         .contentResolverQuery(
3836                                 mResolver, uriAddress, projection, selection, null, null);
3837         try {
3838             if (c.moveToFirst()) {
3839                 do {
3840                     Long partId = c.getLong(c.getColumnIndex(BaseColumns._ID));
3841                     String contentType = c.getString(c.getColumnIndex(Mms.Part.CONTENT_TYPE));
3842                     String name = c.getString(c.getColumnIndex(Mms.Part.NAME));
3843                     String charset = c.getString(c.getColumnIndex(Mms.Part.CHARSET));
3844                     String filename = c.getString(c.getColumnIndex(Mms.Part.FILENAME));
3845                     String text = c.getString(c.getColumnIndex(Mms.Part.TEXT));
3846                     Integer fd = c.getInt(c.getColumnIndex(Mms.Part._DATA));
3847                     String cid = c.getString(c.getColumnIndex(Mms.Part.CONTENT_ID));
3848                     String cl = c.getString(c.getColumnIndex(Mms.Part.CONTENT_LOCATION));
3849                     String cdisp = c.getString(c.getColumnIndex(Mms.Part.CONTENT_DISPOSITION));
3850 
3851                     Log.v(
3852                             TAG,
3853                             "     _id : "
3854                                     + partId
3855                                     + "\n     ct : "
3856                                     + contentType
3857                                     + "\n     partname : "
3858                                     + name
3859                                     + "\n     charset : "
3860                                     + charset
3861                                     + "\n     filename : "
3862                                     + filename
3863                                     + "\n     text : "
3864                                     + text
3865                                     + "\n     fd : "
3866                                     + fd
3867                                     + "\n     cid : "
3868                                     + cid
3869                                     + "\n     cl : "
3870                                     + cl
3871                                     + "\n     cdisp : "
3872                                     + cdisp);
3873 
3874                     part = message.addMimePart();
3875                     part.mContentType = contentType;
3876                     part.mPartName = name;
3877                     part.mContentId = cid;
3878                     part.mContentLocation = cl;
3879                     part.mContentDisposition = cdisp;
3880 
3881                     // Filtering out non-text parts (e.g., an image) when attachments are to be
3882                     // excluded is currently handled within the "message" object's encoding
3883                     // function (c.f., BluetoothMapbMessageMime.encodeMime()), where the
3884                     // attachment is replaced with a text string containing the part name or
3885                     // filename.
3886                     // However, replacing with text during encoding is too late, as charset
3887                     // information does not get properly set and propagated. For example, if a MMS
3888                     // consists only of a GIF, it's mimetype is "image/gif" and not "text", so
3889                     // according to spec, "charset" should not be set. However, if the attachment
3890                     // is replaced with a text string, the bMessage now contains text and should
3891                     // have charset set to UTF-8 according to spec.
3892                     if (!part.mContentType.toUpperCase(Locale.ROOT).contains("TEXT")
3893                             && !message.getIncludeAttachments()) {
3894                         StringBuilder sb = new StringBuilder();
3895                         part.encodePlainText(sb);
3896                         // Each time {@code encodePlainText} is called, it adds {@code "\r\n"}
3897                         // to the string. {@code encodePlainText} is called here to replace
3898                         // an image with a string, but later on, when we encode the entire
3899                         // bMessage in {@link BluetoothMapbMessageMime#encode()},
3900                         // {@code encodePlainText} will be called again on this {@code
3901                         // MimePart} (as text this time), adding a second {@code "\r\n"}. So
3902                         // we remove the extra newline from the end.
3903                         int newlineIndex = sb.lastIndexOf("\r\n");
3904                         if (newlineIndex != -1) sb.delete(newlineIndex, newlineIndex + 4);
3905                         text = sb.toString();
3906                         part.mContentType = "text";
3907                     }
3908 
3909                     try {
3910                         if (text != null) {
3911                             part.mData = text.getBytes(StandardCharsets.UTF_8);
3912                             part.mCharsetName = "utf-8";
3913                         } else {
3914                             part.mData =
3915                                     readRawDataPart(Uri.parse(Mms.CONTENT_URI + "/part"), partId);
3916                             if (charset != null) {
3917                                 part.mCharsetName =
3918                                         CharacterSets.getMimeName(Integer.parseInt(charset));
3919                             }
3920                         }
3921                     } catch (NumberFormatException e) {
3922                         ContentProfileErrorReportUtils.report(
3923                                 BluetoothProfile.MAP,
3924                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
3925                                 BluetoothStatsLog
3926                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3927                                 7);
3928                         Log.d(TAG, "extractMmsParts", e);
3929                         part.mData = null;
3930                         part.mCharsetName = null;
3931                     } catch (UnsupportedEncodingException e) {
3932                         ContentProfileErrorReportUtils.report(
3933                                 BluetoothProfile.MAP,
3934                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
3935                                 BluetoothStatsLog
3936                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3937                                 8);
3938                         Log.d(TAG, "extractMmsParts", e);
3939                         part.mData = null;
3940                         part.mCharsetName = null;
3941                     }
3942                     part.mFileName = filename;
3943                 } while (c.moveToNext());
3944                 message.updateCharset();
3945             }
3946 
3947         } finally {
3948             if (c != null) {
3949                 c.close();
3950             }
3951         }
3952     }
3953 
3954     /**
3955      * @param id the content provider id for the message to fetch.
3956      * @param appParams The application parameter object received from the client.
3957      * @return a byte[] containing the utf-8 encoded bMessage to send to the client.
3958      * @throws UnsupportedEncodingException if UTF-8 is not supported, which is guaranteed to be
3959      *     supported on an android device
3960      */
getMmsMessage(long id, BluetoothMapAppParams appParams)3961     public byte[] getMmsMessage(long id, BluetoothMapAppParams appParams)
3962             throws UnsupportedEncodingException {
3963         int msgBox, threadId;
3964         if (appParams.getCharset() == MAP_MESSAGE_CHARSET_NATIVE) {
3965             throw new IllegalArgumentException(
3966                     "MMS charset native not allowed for MMS" + " - must be utf-8");
3967         }
3968 
3969         BluetoothMapbMessageMime message = new BluetoothMapbMessageMime();
3970         Cursor c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION, "_ID = " + id, null, null);
3971         try {
3972             if (c != null && c.moveToFirst()) {
3973                 message.setType(TYPE.MMS);
3974                 message.setVersionString(mMessageVersion);
3975 
3976                 // The MMS info:
3977                 String read = c.getString(c.getColumnIndex(Mms.READ));
3978                 if (read.equalsIgnoreCase("1")) {
3979                     message.setStatus(true);
3980                 } else {
3981                     message.setStatus(false);
3982                 }
3983 
3984                 msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
3985                 threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
3986                 message.setFolder(getFolderName(msgBox, threadId));
3987                 message.setSubject(c.getString(c.getColumnIndex(Mms.SUBJECT)));
3988                 message.setMessageId(c.getString(c.getColumnIndex(Mms.MESSAGE_ID)));
3989                 message.setContentType(c.getString(c.getColumnIndex(Mms.CONTENT_TYPE)));
3990                 message.setDate(c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L);
3991                 message.setTextOnly(c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)) != 0);
3992                 message.setIncludeAttachments(appParams.getAttachment() != 0);
3993                 // c.getLong(c.getColumnIndex(Mms.DATE_SENT)); - this is never used
3994                 // c.getInt(c.getColumnIndex(Mms.STATUS)); - don't know what this is
3995 
3996                 // The parts
3997                 extractMmsParts(id, message);
3998 
3999                 // The addresses
4000                 extractMmsAddresses(id, message);
4001 
4002                 return message.encode();
4003             }
4004         } finally {
4005             if (c != null) {
4006                 c.close();
4007             }
4008         }
4009 
4010         return message.encode();
4011     }
4012 
4013     /**
4014      * @param id the content provider id for the message to fetch.
4015      * @param appParams The application parameter object received from the client.
4016      * @return a byte[] containing the utf-8 encoded bMessage to send to the client.
4017      * @throws UnsupportedEncodingException if UTF-8 is not supported, which is guaranteed to be
4018      *     supported on an android device
4019      */
getEmailMessage( long id, BluetoothMapAppParams appParams, BluetoothMapFolderElement currentFolder)4020     public byte[] getEmailMessage(
4021             long id, BluetoothMapAppParams appParams, BluetoothMapFolderElement currentFolder)
4022             throws UnsupportedEncodingException {
4023         // Log print out of application parameters set
4024         if (appParams != null) {
4025             Log.d(
4026                     TAG,
4027                     "TYPE_MESSAGE (GET): Attachment = "
4028                             + appParams.getAttachment()
4029                             + ", Charset = "
4030                             + appParams.getCharset()
4031                             + ", FractionRequest = "
4032                             + appParams.getFractionRequest());
4033         }
4034 
4035         // Throw exception if requester NATIVE charset for Email
4036         // Exception is caught by MapObexServer sendGetMessageResp
4037         if (appParams.getCharset() == MAP_MESSAGE_CHARSET_NATIVE) {
4038             throw new IllegalArgumentException("EMAIL charset not UTF-8");
4039         }
4040 
4041         BluetoothMapbMessageEmail message = new BluetoothMapbMessageEmail();
4042         Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
4043         Cursor c =
4044                 BluetoothMethodProxy.getInstance()
4045                         .contentResolverQuery(
4046                                 mResolver,
4047                                 contentUri,
4048                                 BluetoothMapContract.BT_MESSAGE_PROJECTION,
4049                                 "_ID = " + id,
4050                                 null,
4051                                 null);
4052         try {
4053             if (c != null && c.moveToFirst()) {
4054                 BluetoothMapFolderElement folderElement;
4055                 FileInputStream is = null;
4056                 ParcelFileDescriptor fd = null;
4057                 try {
4058                     // Handle fraction requests
4059                     int fractionRequest = appParams.getFractionRequest();
4060                     if (fractionRequest != BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
4061                         // Fraction requested
4062                         String fractionStr = (fractionRequest == 0) ? "FIRST" : "NEXT";
4063                         Log.v(
4064                                 TAG,
4065                                 "getEmailMessage - FractionRequest "
4066                                         + fractionStr
4067                                         + " - send compete message");
4068                         // Check if message is complete and if not - request message from server
4069                         if (!c.getString(
4070                                         c.getColumnIndex(
4071                                                 BluetoothMapContract.MessageColumns
4072                                                         .RECEPTION_STATE))
4073                                 .equalsIgnoreCase(BluetoothMapContract.RECEPTION_STATE_COMPLETE)) {
4074                             // TODO: request message from server
4075                             Log.w(
4076                                     TAG,
4077                                     "getEmailMessage - receptionState not COMPLETE -  Not "
4078                                             + "Implemented!");
4079                             ContentProfileErrorReportUtils.report(
4080                                     BluetoothProfile.MAP,
4081                                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4082                                     BluetoothStatsLog
4083                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4084                                     9);
4085                         }
4086                     }
4087                     // Set read status:
4088                     String read =
4089                             c.getString(
4090                                     c.getColumnIndex(
4091                                             BluetoothMapContract.MessageColumns.FLAG_READ));
4092                     if (read != null && read.equalsIgnoreCase("1")) {
4093                         message.setStatus(true);
4094                     } else {
4095                         message.setStatus(false);
4096                     }
4097 
4098                     // Set message type:
4099                     message.setType(TYPE.EMAIL);
4100                     message.setVersionString(mMessageVersion);
4101                     // Set folder:
4102                     long folderId =
4103                             c.getLong(
4104                                     c.getColumnIndex(
4105                                             BluetoothMapContract.MessageColumns.FOLDER_ID));
4106                     folderElement = currentFolder.getFolderById(folderId);
4107                     message.setCompleteFolder(folderElement.getFullPath());
4108 
4109                     // Set recipient:
4110                     String nameEmail =
4111                             c.getString(
4112                                     c.getColumnIndex(BluetoothMapContract.MessageColumns.TO_LIST));
4113                     Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(nameEmail);
4114                     if (tokens.length != 0) {
4115                         Log.d(TAG, "Recipient count= " + tokens.length);
4116                         int i = 0;
4117                         while (i < tokens.length) {
4118                             Log.v(TAG, "Recipient = " + tokens[i].toString());
4119                             String[] emails = new String[1];
4120                             emails[0] = tokens[i].getAddress();
4121                             String name = tokens[i].getName();
4122                             message.addRecipient(name, name, null, emails, null, null);
4123                             i++;
4124                         }
4125                     }
4126 
4127                     // Set originator:
4128                     nameEmail =
4129                             c.getString(
4130                                     c.getColumnIndex(
4131                                             BluetoothMapContract.MessageColumns.FROM_LIST));
4132                     tokens = Rfc822Tokenizer.tokenize(nameEmail);
4133                     if (tokens.length != 0) {
4134                         Log.d(TAG, "Originator count= " + tokens.length);
4135                         int i = 0;
4136                         while (i < tokens.length) {
4137                             Log.v(TAG, "Originator = " + tokens[i].toString());
4138                             String[] emails = new String[1];
4139                             emails[0] = tokens[i].getAddress();
4140                             String name = tokens[i].getName();
4141                             message.addOriginator(name, name, null, emails, null, null);
4142                             i++;
4143                         }
4144                     }
4145                 } finally {
4146                     if (c != null) {
4147                         c.close();
4148                     }
4149                 }
4150                 // Find out if we get attachments
4151                 String attStr =
4152                         (appParams.getAttachment() == 0)
4153                                 ? "/" + BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS
4154                                 : "";
4155                 Uri uri = Uri.parse(contentUri + "/" + id + attStr);
4156 
4157                 // Get email message body content
4158                 int count = 0;
4159                 try {
4160                     fd =
4161                             BluetoothMethodProxy.getInstance()
4162                                     .contentResolverOpenFileDescriptor(mResolver, uri, "r");
4163                     is = new FileInputStream(fd.getFileDescriptor());
4164                     StringBuilder email = new StringBuilder("");
4165                     byte[] buffer = new byte[1024];
4166                     while ((count = is.read(buffer)) != -1) {
4167                         // TODO: Handle breaks within a UTF8 character
4168                         email.append(new String(buffer, 0, count));
4169                         Log.v(
4170                                 TAG,
4171                                 "Email part = " + new String(buffer, 0, count) + " count=" + count);
4172                     }
4173                     // Set email message body:
4174                     message.setEmailBody(email.toString());
4175                 } catch (FileNotFoundException e) {
4176                     ContentProfileErrorReportUtils.report(
4177                             BluetoothProfile.MAP,
4178                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4179                             BluetoothStatsLog
4180                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4181                             10);
4182                     Log.w(TAG, e);
4183                 } catch (NullPointerException e) {
4184                     ContentProfileErrorReportUtils.report(
4185                             BluetoothProfile.MAP,
4186                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4187                             BluetoothStatsLog
4188                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4189                             11);
4190                     Log.w(TAG, e);
4191                 } catch (IOException e) {
4192                     ContentProfileErrorReportUtils.report(
4193                             BluetoothProfile.MAP,
4194                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4195                             BluetoothStatsLog
4196                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4197                             12);
4198                     Log.w(TAG, e);
4199                 } finally {
4200                     try {
4201                         if (is != null) {
4202                             is.close();
4203                         }
4204                     } catch (IOException e) {
4205                         ContentProfileErrorReportUtils.report(
4206                                 BluetoothProfile.MAP,
4207                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4208                                 BluetoothStatsLog
4209                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4210                                 13);
4211                         Log.w(TAG, e);
4212                     }
4213                     try {
4214                         if (fd != null) {
4215                             fd.close();
4216                         }
4217                     } catch (IOException e) {
4218                         ContentProfileErrorReportUtils.report(
4219                                 BluetoothProfile.MAP,
4220                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT,
4221                                 BluetoothStatsLog
4222                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4223                                 14);
4224                         Log.w(TAG, e);
4225                     }
4226                 }
4227                 return message.encode();
4228             }
4229         } finally {
4230             if (c != null) {
4231                 c.close();
4232             }
4233         }
4234         throw new IllegalArgumentException("EMAIL handle not found");
4235     }
4236 
4237     /**
4238      * @param id the content provider id for the message to fetch.
4239      * @param appParams The application parameter object received from the client.
4240      * @return a byte[] containing the utf-8 encoded bMessage to send to the client.
4241      */
getIMMessage( long id, BluetoothMapAppParams appParams, BluetoothMapFolderElement folderElement)4242     public byte[] getIMMessage(
4243             long id, BluetoothMapAppParams appParams, BluetoothMapFolderElement folderElement) {
4244         long threadId, folderId;
4245 
4246         if (appParams.getCharset() == MAP_MESSAGE_CHARSET_NATIVE) {
4247             throw new IllegalArgumentException(
4248                     "IM charset native not allowed for IM - must be utf-8");
4249         }
4250 
4251         BluetoothMapbMessageMime message = new BluetoothMapbMessageMime();
4252         Uri contentUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_MESSAGE);
4253         Cursor c =
4254                 BluetoothMethodProxy.getInstance()
4255                         .contentResolverQuery(
4256                                 mResolver,
4257                                 contentUri,
4258                                 BluetoothMapContract.BT_MESSAGE_PROJECTION,
4259                                 "_ID = " + id,
4260                                 null,
4261                                 null);
4262         Cursor contacts = null;
4263         try {
4264             if (c != null && c.moveToFirst()) {
4265                 message.setType(TYPE.IM);
4266                 message.setVersionString(mMessageVersion);
4267 
4268                 // The IM message info:
4269                 int read =
4270                         c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ));
4271                 if (read == 1) {
4272                     message.setStatus(true);
4273                 } else {
4274                     message.setStatus(false);
4275                 }
4276 
4277                 threadId =
4278                         c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.THREAD_ID));
4279                 folderId =
4280                         c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));
4281                 folderElement = folderElement.getFolderById(folderId);
4282                 message.setCompleteFolder(folderElement.getFullPath());
4283                 message.setSubject(
4284                         c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.SUBJECT)));
4285                 message.setMessageId(
4286                         c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID)));
4287                 message.setDate(
4288                         c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns.DATE)));
4289                 message.setTextOnly(
4290                         c.getInt(
4291                                         c.getColumnIndex(
4292                                                 BluetoothMapContract.MessageColumns
4293                                                         .ATTACHMENT_SIZE))
4294                                 == 0);
4295 
4296                 message.setIncludeAttachments(appParams.getAttachment() != 0);
4297 
4298                 // c.getLong(c.getColumnIndex(Mms.DATE_SENT)); - this is never used
4299                 // c.getInt(c.getColumnIndex(Mms.STATUS)); - don't know what this is
4300 
4301                 // The parts
4302 
4303                 // FIXME next few lines are temporary code
4304                 MimePart part = message.addMimePart();
4305                 part.mData =
4306                         c.getString((c.getColumnIndex(BluetoothMapContract.MessageColumns.BODY)))
4307                                 .getBytes(StandardCharsets.UTF_8);
4308                 part.mCharsetName = "utf-8";
4309                 part.mContentId = "0";
4310                 part.mContentType = "text/plain";
4311                 message.updateCharset();
4312                 // FIXME end temp code
4313 
4314                 Uri contactsUri = Uri.parse(mBaseUri + BluetoothMapContract.TABLE_CONVOCONTACT);
4315                 contacts =
4316                         BluetoothMethodProxy.getInstance()
4317                                 .contentResolverQuery(
4318                                         mResolver,
4319                                         contactsUri,
4320                                         BluetoothMapContract.BT_CONTACT_PROJECTION,
4321                                         BluetoothMapContract.ConvoContactColumns.CONVO_ID
4322                                                 + " = "
4323                                                 + threadId,
4324                                         null,
4325                                         null);
4326                 // TODO this will not work for group-chats
4327                 if (contacts != null && contacts.moveToFirst()) {
4328                     String name =
4329                             contacts.getString(
4330                                     contacts.getColumnIndex(
4331                                             BluetoothMapContract.ConvoContactColumns.NAME));
4332                     String[] btUid = new String[1];
4333                     btUid[0] =
4334                             contacts.getString(
4335                                     contacts.getColumnIndex(
4336                                             BluetoothMapContract.ConvoContactColumns.X_BT_UID));
4337                     String nickname =
4338                             contacts.getString(
4339                                     contacts.getColumnIndex(
4340                                             BluetoothMapContract.ConvoContactColumns.NICKNAME));
4341                     String[] btUci = new String[1];
4342                     String[] btOwnUci = new String[1];
4343                     btOwnUci[0] = mAccount.getUciFull();
4344                     btUci[0] =
4345                             contacts.getString(
4346                                     contacts.getColumnIndex(
4347                                             BluetoothMapContract.ConvoContactColumns.UCI));
4348                     if (folderId == BluetoothMapContract.FOLDER_ID_SENT
4349                             || folderId == BluetoothMapContract.FOLDER_ID_OUTBOX) {
4350                         message.addRecipient(nickname, name, null, null, btUid, btUci);
4351                         message.addOriginator(null, btOwnUci);
4352 
4353                     } else {
4354                         message.addOriginator(nickname, name, null, null, btUid, btUci);
4355                         message.addRecipient(null, btOwnUci);
4356                     }
4357                 }
4358                 return message.encode();
4359             }
4360         } finally {
4361             if (c != null) {
4362                 c.close();
4363             }
4364             if (contacts != null) {
4365                 contacts.close();
4366             }
4367         }
4368 
4369         throw new IllegalArgumentException("IM handle not found");
4370     }
4371 
setRemoteFeatureMask(int featureMask)4372     public void setRemoteFeatureMask(int featureMask) {
4373         this.mRemoteFeatureMask = featureMask;
4374         Log.v(TAG, "setRemoteFeatureMask");
4375         if ((this.mRemoteFeatureMask & BluetoothMapUtils.MAP_FEATURE_MESSAGE_LISTING_FORMAT_V11_BIT)
4376                 == BluetoothMapUtils.MAP_FEATURE_MESSAGE_LISTING_FORMAT_V11_BIT) {
4377             Log.v(TAG, "setRemoteFeatureMask MAP_MESSAGE_LISTING_FORMAT_V11");
4378             this.mMsgListingVersion = BluetoothMapUtils.MAP_MESSAGE_LISTING_FORMAT_V11;
4379         }
4380     }
4381 
getRemoteFeatureMask()4382     public int getRemoteFeatureMask() {
4383         return this.mRemoteFeatureMask;
4384     }
4385 
getSmsMmsConvoList()4386     Map<Long, BluetoothMapConvoListingElement> getSmsMmsConvoList() {
4387         return mMasInstance.getSmsMmsConvoList();
4388     }
4389 
setSmsMmsConvoList(Map<Long, BluetoothMapConvoListingElement> smsMmsConvoList)4390     void setSmsMmsConvoList(Map<Long, BluetoothMapConvoListingElement> smsMmsConvoList) {
4391         mMasInstance.setSmsMmsConvoList(smsMmsConvoList);
4392     }
4393 
getImEmailConvoList()4394     Map<Long, BluetoothMapConvoListingElement> getImEmailConvoList() {
4395         return mMasInstance.getImEmailConvoList();
4396     }
4397 
setImEmailConvoList(Map<Long, BluetoothMapConvoListingElement> imEmailConvoList)4398     void setImEmailConvoList(Map<Long, BluetoothMapConvoListingElement> imEmailConvoList) {
4399         mMasInstance.setImEmailConvoList(imEmailConvoList);
4400     }
4401 }
4402