2323import java .util .HashMap ;
2424import java .util .Map ;
2525
26- import static android .provider .Telephony .TextBasedSmsColumns .ADDRESS ;
27- import static android .provider .Telephony .TextBasedSmsColumns .BODY ;
28- import static android .provider .Telephony .TextBasedSmsColumns .DATE ;
29- import static android .provider .Telephony .TextBasedSmsColumns .READ ;
30- import static android .provider .Telephony .TextBasedSmsColumns .THREAD_ID ;
31- import static android .provider .Telephony .TextBasedSmsColumns .TYPE ;
26+ import static android .provider .Telephony .TextBasedSmsColumns .*;
3227
28+ import androidx .annotation .Nullable ;
29+
30+ /**
31+ * **See Also:**
32+ * - https://developer.android.com/reference/android/provider/Telephony
33+ * - https://developer.android.com/reference/android/provider/Telephony.Sms.Conversations
34+ * - https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns
35+ * - https://developer.android.com/reference/android/provider/BaseColumns
36+ */
3337public class SmsInboxAPI {
3438
3539 private static final String [] DISPLAY_NAME_PROJECTION = {PhoneLookup .DISPLAY_NAME };
@@ -39,60 +43,202 @@ public class SmsInboxAPI {
3943 public static void onReceive (TermuxApiReceiver apiReceiver , final Context context , Intent intent ) {
4044 Logger .logDebug (LOG_TAG , "onReceive" );
4145
42- final int offset = intent .getIntExtra ("offset" , 0 );
43- final int limit = intent .getIntExtra ("limit" , 10 );
44- final String number = intent .hasExtra ("from" ) ? intent .getStringExtra ("from" ):"" ;
45- final boolean conversation_list = intent .getBooleanExtra ("conversation-list" , false );
46- final Uri contentURI = conversation_list ? typeToContentURI (0 ) :
47- typeToContentURI (number ==null || number .isEmpty () ?
48- intent .getIntExtra ("type" , TextBasedSmsColumns .MESSAGE_TYPE_INBOX ): 0 );
46+ String value ;
47+
48+ final boolean conversationList = intent .getBooleanExtra ("conversation-list" , false );
49+
50+ final boolean conversationReturnMultipleMessages = intent .getBooleanExtra ("conversation-return-multiple-messages" , false );
51+ final boolean conversationReturnNestedView = intent .getBooleanExtra ("conversation-return-nested-view" , false );
52+ final boolean conversationReturnNoOrderReverse = intent .getBooleanExtra ("conversation-return-no-order-reverse" , false );
53+
54+ final int conversationOffset = intent .getIntExtra ("conversation-offset" , -1 );
55+ final int conversationLimit = intent .getIntExtra ("conversation-limit" , -1 );
56+ final String conversationSelection = intent .getStringExtra ("conversation-selection" );
57+
58+ /*
59+ NOTE: When conversation or messages are queried from the Android database, first the
60+ sort order is applied, and then any offset and limit values are used to filter the
61+ entries. Since the default sort order is 'date DESC', Android returns the latest dated
62+ conversations or messages first, but the API reverses the order by default (with
63+ `Cursor.moveToLast()`/`Cursor.moveToPrevious()`) so that the latest entries are printed
64+ at the end. If the order should not be reversed, then pass the respective
65+ `*-return-no-order-reverse` extras.
66+ */
67+ value = intent .getStringExtra ("conversation-sort-order" );
68+ if (value == null || value .isEmpty ()) {
69+ value = "date DESC" ;
70+ }
71+ final String conversationSortOrder = value ;
72+
73+
74+ final int messageOffset = intent .getIntExtra ("offset" , 0 );
75+ final int messageLimit = intent .getIntExtra ("limit" , 10 );
76+ final int messageTypeColumn = intent .getIntExtra ("type" , TextBasedSmsColumns .MESSAGE_TYPE_INBOX );
77+ final String messageSelection = intent .getStringExtra ("message-selection" );
78+
79+ value = intent .getStringExtra ("from" );
80+ if (value == null || value .isEmpty ()) {
81+ value = null ;
82+ }
83+ final String messageAddress = value ;
84+
85+ value = intent .getStringExtra ("message-sort-order" );
86+ if (value == null || value .isEmpty ()) {
87+ value = "date DESC" ;
88+ }
89+ final String messageSortOrder = value ;
90+
91+ final boolean messageReturnNoOrderReverse = intent .getBooleanExtra ("message-return-no-order-reverse" , false );
92+
93+ Uri contentURI ;
94+ if (conversationList ) {
95+ contentURI = typeToContentURI (TextBasedSmsColumns .MESSAGE_TYPE_ALL );
96+ } else {
97+ contentURI = typeToContentURI (messageAddress == null ?
98+ messageTypeColumn : TextBasedSmsColumns .MESSAGE_TYPE_ALL );
99+ }
49100
50101 ResultReturner .returnData (apiReceiver , intent , new ResultJsonWriter () {
51102 @ Override
52103 public void writeJson (JsonWriter out ) throws Exception {
53- if (conversation_list ) getConversations (context , out , offset , limit );
54- else getAllSms (context , out , offset , limit , number , contentURI );
104+ if (conversationList ) {
105+ getConversations (context , out ,
106+ conversationOffset , conversationLimit ,
107+ conversationSelection ,
108+ conversationSortOrder ,
109+ conversationReturnMultipleMessages ,conversationReturnNestedView ,
110+ conversationReturnNoOrderReverse ,
111+ messageOffset , messageLimit ,
112+ messageSelection ,
113+ messageSortOrder ,
114+ messageReturnNoOrderReverse );
115+ } else {
116+ getAllSms (context , out , contentURI ,
117+ messageOffset , messageLimit ,
118+ messageSelection , messageAddress ,
119+ messageSortOrder ,
120+ messageReturnNoOrderReverse );
121+ }
55122 }
56123 });
57124 }
58125
59126 @ SuppressLint ("SimpleDateFormat" )
60- public static void getConversations (Context context , JsonWriter out , int offset , int limit ) throws IOException {
127+ public static void getConversations (Context context , JsonWriter out ,
128+ int conversationOffset , int conversationLimit ,
129+ String conversationSelection ,
130+ String conversationSortOrder ,
131+ boolean conversationReturnMultipleMessages , boolean conversationReturnNestedView ,
132+ boolean conversationReturnNoOrderReverse ,
133+ int messageOffset , int messageLimit ,
134+ String messageSelection ,
135+ String messageSortOrder ,
136+ boolean messageReturnNoOrderReverse ) throws IOException {
61137 ContentResolver cr = context .getContentResolver ();
62- String sortOrder = "date DESC" ;
63- try (Cursor c = cr .query (Conversations .CONTENT_URI , null , null , null , sortOrder )) {
64- c .moveToLast ();
138+
139+ // `THREAD_ID` is used to select messages for a conversation, so do not allow caller to pass it.
140+ if (messageSelection != null && messageSelection .matches ("^(.*[ \t \n ])?" + THREAD_ID + "[ \t \n ].*$" )) {
141+ throw new IllegalArgumentException (
142+ "The 'conversation-selection' cannot contain '" + THREAD_ID + "': `" + messageSelection + "`" );
143+ }
144+
145+ conversationSortOrder = getSortOrder (conversationSortOrder , conversationOffset , conversationLimit );
146+ messageSortOrder = getSortOrder (messageSortOrder , messageOffset , messageLimit );
147+
148+ int index ;
149+ try (Cursor conversationCursor = cr .query (Conversations .CONTENT_URI ,
150+ null , conversationSelection , null , conversationSortOrder )) {
151+ int conversationCount = conversationCursor .getCount ();
152+ if (conversationReturnNoOrderReverse ) {
153+ conversationCursor .moveToFirst ();
154+ } else {
155+ conversationCursor .moveToLast ();
156+ }
65157
66158 Map <String , String > nameCache = new HashMap <>();
67159
68- out . beginArray ();
69- for ( int i = 0 , count = c . getCount (); i < count ; i ++) {
70- int id = c . getInt ( c . getColumnIndex ( THREAD_ID ));
71-
72- Cursor cc = cr . query ( Sms . CONTENT_URI , null ,
73- THREAD_ID + " == '" + id + "'" ,
74- null , "date DESC" );
75- if (cc . getCount () == 0 ) {
76- c . moveToNext ();
160+ if ( conversationReturnNestedView ) {
161+ out . beginObject ();
162+ } else {
163+ out . beginArray ();
164+ }
165+ for ( int i = 0 ; i < conversationCount ; i ++) {
166+ index = conversationCursor . getColumnIndex ( THREAD_ID );
167+ if (index < 0 ) {
168+ conversationCursor . moveToPrevious ();
77169 continue ;
78170 }
79- cc .moveToFirst ();
80- writeElement (cc , out , nameCache , context );
81- cc .close ();
82- c .moveToPrevious ();
171+
172+ int id = conversationCursor .getInt (index );
173+
174+ if (conversationReturnNestedView ) {
175+ out .name (String .valueOf (id ));
176+ out .beginArray ();
177+ }
178+
179+ String [] messageSelectionArgs = null ;
180+ if (messageSelection == null || messageSelection .isEmpty ()) {
181+ messageSelection = "" ;
182+ } else {
183+ messageSelection += " " ;
184+ }
185+
186+ Cursor messageCursor = cr .query (Sms .CONTENT_URI , null ,
187+ messageSelection + THREAD_ID + " == '" + id +"'" , messageSelectionArgs ,
188+ messageSortOrder );
189+
190+ int messageCount = messageCursor .getCount ();
191+ if (messageCount > 0 ) {
192+ if (conversationReturnMultipleMessages ) {
193+ if (messageReturnNoOrderReverse ) {
194+ messageCursor .moveToFirst ();
195+ } else {
196+ messageCursor .moveToLast ();
197+ }
198+
199+ for (int j = 0 ; j < messageCount ; j ++) {
200+ writeElement (messageCursor , out , nameCache , context );
201+
202+ if (messageReturnNoOrderReverse ) {
203+ messageCursor .moveToNext ();
204+ } else {
205+ messageCursor .moveToPrevious ();
206+ }
207+ }
208+ } else {
209+ messageCursor .moveToFirst ();
210+ writeElement (messageCursor , out , nameCache , context );
211+ }
212+ }
213+
214+ messageCursor .close ();
215+
216+ if (conversationReturnNestedView ) {
217+ out .endArray ();
218+ }
219+
220+ if (conversationReturnNoOrderReverse ) {
221+ conversationCursor .moveToNext ();
222+ } else {
223+ conversationCursor .moveToPrevious ();
224+ }
225+ }
226+ if (conversationReturnNestedView ) {
227+ out .endObject ();
228+ } else {
229+ out .endArray ();
83230 }
84- out .endArray ();
85231 }
86232 }
87233
88234 @ SuppressLint ("SimpleDateFormat" )
89235 private static void writeElement (Cursor c , JsonWriter out , Map <String , String > nameCache , Context context ) throws IOException {
90236 SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );
91237
238+ int index ;
92239 int threadID = c .getInt (c .getColumnIndexOrThrow (THREAD_ID ));
93240 String smsAddress = c .getString (c .getColumnIndexOrThrow (ADDRESS ));
94241 String smsBody = c .getString (c .getColumnIndexOrThrow (BODY ));
95- boolean read = (c .getInt (c .getColumnIndex (READ )) != 0 );
96242 long smsReceivedDate = c .getLong (c .getColumnIndexOrThrow (DATE ));
97243 // long smsSentDate = c.getLong(c.getColumnIndexOrThrow(TextBasedSmsColumns.DATE_SENT));
98244 int smsID = c .getInt (c .getColumnIndexOrThrow ("_id" ));
@@ -103,7 +249,11 @@ private static void writeElement(Cursor c, JsonWriter out, Map<String, String> n
103249 out .beginObject ();
104250 out .name ("threadid" ).value (threadID );
105251 out .name ("type" ).value (messageType );
106- out .name ("read" ).value (read );
252+
253+ index = c .getColumnIndex (READ );
254+ if (index >= 0 ) {
255+ out .name ("read" ).value (c .getInt (index ) != 0 );
256+ }
107257
108258 if (smsSenderName != null ) {
109259 if (messageType .equals ("inbox" )) {
@@ -131,31 +281,67 @@ private static void writeElement(Cursor c, JsonWriter out, Map<String, String> n
131281
132282
133283 @ SuppressLint ("SimpleDateFormat" )
134- public static void getAllSms (Context context , JsonWriter out , int offset , int limit , String number , Uri contentURI ) throws IOException {
284+ public static void getAllSms (Context context , JsonWriter out ,
285+ Uri contentURI ,
286+ int messageOffset , int messageLimit ,
287+ String messageSelection , String messageAddress ,
288+ String messageSortOrder ,
289+ boolean messageReturnNoOrderReverse ) throws IOException {
135290 ContentResolver cr = context .getContentResolver ();
136- String sortOrder = "date DESC LIMIT + " + limit + " OFFSET " + offset ;
137- try (Cursor c = cr .query (contentURI , null ,
138- ADDRESS + " LIKE '%" + number + "%'" , null , sortOrder )) {
139- c .moveToLast ();
291+
292+ String [] messageSelectionArgs = null ;
293+ if (messageSelection == null || messageSelection .isEmpty ()) {
294+ if (messageAddress != null && !messageAddress .isEmpty ()) {
295+ messageSelection = ADDRESS + " LIKE '%?%'" ;
296+ messageSelectionArgs = new String []{messageAddress };
297+ }
298+ }
299+
300+ messageSortOrder = getSortOrder (messageSortOrder , messageOffset , messageLimit );
301+
302+ try (Cursor messageCursor = cr .query (contentURI , null ,
303+ messageSelection , messageSelectionArgs ,
304+ messageSortOrder )) {
305+ int messageCount = messageCursor .getCount ();
306+ if (messageReturnNoOrderReverse ) {
307+ messageCursor .moveToFirst ();
308+ } else {
309+ messageCursor .moveToLast ();
310+ }
140311
141312 new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );
142313 Map <String , String > nameCache = new HashMap <>();
143314
144315 out .beginArray ();
145- for (int i = 0 , count = c .getCount (); i < count ; i ++) {
146- writeElement (c , out , nameCache , context );
147- c .moveToPrevious ();
316+ for (int i = 0 ; i < messageCount ; i ++) {
317+ writeElement (messageCursor , out , nameCache , context );
318+
319+ if (messageReturnNoOrderReverse ) {
320+ messageCursor .moveToNext ();
321+ } else {
322+ messageCursor .moveToPrevious ();
323+ }
148324 }
149325 out .endArray ();
150326 }
151327 }
152328
153329 private static String getContactNameFromNumber (Map <String , String > cache , Context context , String number ) {
154- if (cache .containsKey (number ))
330+ if (cache .containsKey (number )) {
155331 return cache .get (number );
332+ }
333+
334+ int index ;
156335 Uri contactUri = Uri .withAppendedPath (PhoneLookup .CONTENT_FILTER_URI , Uri .encode (number ));
157336 try (Cursor c = context .getContentResolver ().query (contactUri , DISPLAY_NAME_PROJECTION , null , null , null )) {
158- String name = c .moveToFirst () ? c .getString (c .getColumnIndex (PhoneLookup .DISPLAY_NAME )) : null ;
337+ String name = null ;
338+ if (c .moveToFirst ()) {
339+ index = c .getColumnIndex (PhoneLookup .DISPLAY_NAME );
340+ if (index >= 0 ) {
341+ name = c .getString (index );
342+ }
343+ }
344+
159345 cache .put (number , name );
160346 return name ;
161347 }
@@ -195,4 +381,21 @@ private static Uri typeToContentURI(int type) {
195381 }
196382 }
197383
384+ @ Nullable
385+ private static String getSortOrder (String sortOrder , int offset , int limit ) {
386+ if (sortOrder == null ) {
387+ sortOrder = "" ;
388+ }
389+ if (limit >= 0 ) {
390+ sortOrder += " LIMIT " + limit ;
391+ }
392+ if (offset >= 0 ) {
393+ sortOrder += " OFFSET " + offset ;
394+ }
395+ if (sortOrder .isEmpty ()) {
396+ sortOrder = null ;
397+ }
398+ return sortOrder ;
399+ }
400+
198401}
0 commit comments