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