这是indexloc提供的服务,不要输入任何密码
Skip to content

Commit 1b60b23

Browse files
Added|Fixed(SmsInboxAPI): Enhance API with new options and fix errors for non existent columns
The following options have been added. Check `termux-sms-list --help` for examples and more info. - `conversation-limit=<limit>`: The SQL limit for returned SMS conversations. - `conversation-offset=<offset>`: The SQL offset for returned SMS conversations. - `conversation-return-multiple-messages`: Return multiple SMS messages per conversation. - `conversation-return-nested-view`: Return a nested object view of conversations where each conversation contains an array of SMS messages with the conversation id as the key. - `conversation-return-no-order-reverse`: Return SMS conversations without reversing order of conversation sort. - `conversation-selection=<selection>`: The SQL selection for returned SMS conversations. - `conversation-sort-order=<order>`: The SMS conversations sort order as per SQL 'ORDER BY col1, col2, ... ASC|DESC' clause. Default value: 'date DESC'. - `message-return-no-order-reverse`: Return SMS messages without reversing order of message sort. - `message-selection=<selection>`: The SQL selection for returned SMS messages. - `message-sort-order=<order>`: The SMS messages sort order as per SQL 'ORDER BY col1, col2, ... ASC|DESC' clause. Default value: 'date DESC'.
1 parent 603f6d7 commit 1b60b23

File tree

1 file changed

+248
-45
lines changed

1 file changed

+248
-45
lines changed

app/src/main/java/com/termux/api/apis/SmsInboxAPI.java

Lines changed: 248 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@
2323
import java.util.HashMap;
2424
import 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+
*/
3337
public 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

Comments
 (0)