Skip to content

Commit

Permalink
Added|Fixed(SmsInboxAPI): Enhance API with new options and fix errors…
Browse files Browse the repository at this point in the history
… 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'.
  • Loading branch information
agnostic-apollo committed Jan 24, 2025
1 parent 603f6d7 commit 1b60b23
Showing 1 changed file with 248 additions and 45 deletions.
293 changes: 248 additions & 45 deletions app/src/main/java/com/termux/api/apis/SmsInboxAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@
import java.util.HashMap;
import java.util.Map;

import static android.provider.Telephony.TextBasedSmsColumns.ADDRESS;
import static android.provider.Telephony.TextBasedSmsColumns.BODY;
import static android.provider.Telephony.TextBasedSmsColumns.DATE;
import static android.provider.Telephony.TextBasedSmsColumns.READ;
import static android.provider.Telephony.TextBasedSmsColumns.THREAD_ID;
import static android.provider.Telephony.TextBasedSmsColumns.TYPE;
import static android.provider.Telephony.TextBasedSmsColumns.*;

import androidx.annotation.Nullable;

/**
* **See Also:**
* - https://developer.android.com/reference/android/provider/Telephony
* - https://developer.android.com/reference/android/provider/Telephony.Sms.Conversations
* - https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns
* - https://developer.android.com/reference/android/provider/BaseColumns
*/
public class SmsInboxAPI {

private static final String[] DISPLAY_NAME_PROJECTION = {PhoneLookup.DISPLAY_NAME};
Expand All @@ -39,60 +43,202 @@ public class SmsInboxAPI {
public static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) {
Logger.logDebug(LOG_TAG, "onReceive");

final int offset = intent.getIntExtra("offset", 0);
final int limit = intent.getIntExtra("limit", 10);
final String number = intent.hasExtra("from") ? intent.getStringExtra("from"):"";
final boolean conversation_list = intent.getBooleanExtra("conversation-list", false);
final Uri contentURI = conversation_list ? typeToContentURI(0) :
typeToContentURI(number==null || number.isEmpty() ?
intent.getIntExtra("type", TextBasedSmsColumns.MESSAGE_TYPE_INBOX): 0);
String value;

final boolean conversationList = intent.getBooleanExtra("conversation-list", false);

final boolean conversationReturnMultipleMessages = intent.getBooleanExtra("conversation-return-multiple-messages", false);
final boolean conversationReturnNestedView = intent.getBooleanExtra("conversation-return-nested-view", false);
final boolean conversationReturnNoOrderReverse = intent.getBooleanExtra("conversation-return-no-order-reverse", false);

final int conversationOffset = intent.getIntExtra("conversation-offset", -1);
final int conversationLimit = intent.getIntExtra("conversation-limit", -1);
final String conversationSelection = intent.getStringExtra("conversation-selection");

/*
NOTE: When conversation or messages are queried from the Android database, first the
sort order is applied, and then any offset and limit values are used to filter the
entries. Since the default sort order is 'date DESC', Android returns the latest dated
conversations or messages first, but the API reverses the order by default (with
`Cursor.moveToLast()`/`Cursor.moveToPrevious()`) so that the latest entries are printed
at the end. If the order should not be reversed, then pass the respective
`*-return-no-order-reverse` extras.
*/
value = intent.getStringExtra("conversation-sort-order");
if (value == null || value.isEmpty()) {
value = "date DESC";
}
final String conversationSortOrder = value;


final int messageOffset = intent.getIntExtra("offset", 0);
final int messageLimit = intent.getIntExtra("limit", 10);
final int messageTypeColumn = intent.getIntExtra("type", TextBasedSmsColumns.MESSAGE_TYPE_INBOX);
final String messageSelection = intent.getStringExtra("message-selection");

value = intent.getStringExtra("from");
if (value == null || value.isEmpty()) {
value = null;
}
final String messageAddress = value;

value = intent.getStringExtra("message-sort-order");
if (value == null || value.isEmpty()) {
value = "date DESC";
}
final String messageSortOrder = value;

final boolean messageReturnNoOrderReverse = intent.getBooleanExtra("message-return-no-order-reverse", false);

Uri contentURI;
if (conversationList) {
contentURI = typeToContentURI(TextBasedSmsColumns.MESSAGE_TYPE_ALL);
} else {
contentURI = typeToContentURI(messageAddress == null ?
messageTypeColumn : TextBasedSmsColumns.MESSAGE_TYPE_ALL);
}

ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() {
@Override
public void writeJson(JsonWriter out) throws Exception {
if (conversation_list) getConversations(context, out, offset, limit);
else getAllSms(context, out, offset, limit, number, contentURI);
if (conversationList) {
getConversations(context, out,
conversationOffset, conversationLimit,
conversationSelection,
conversationSortOrder,
conversationReturnMultipleMessages,conversationReturnNestedView,
conversationReturnNoOrderReverse,
messageOffset, messageLimit,
messageSelection,
messageSortOrder,
messageReturnNoOrderReverse);
} else {
getAllSms(context, out, contentURI,
messageOffset, messageLimit,
messageSelection, messageAddress,
messageSortOrder,
messageReturnNoOrderReverse);
}
}
});
}

@SuppressLint("SimpleDateFormat")
public static void getConversations(Context context, JsonWriter out, int offset, int limit) throws IOException {
public static void getConversations(Context context, JsonWriter out,
int conversationOffset, int conversationLimit,
String conversationSelection,
String conversationSortOrder,
boolean conversationReturnMultipleMessages, boolean conversationReturnNestedView,
boolean conversationReturnNoOrderReverse,
int messageOffset, int messageLimit,
String messageSelection,
String messageSortOrder,
boolean messageReturnNoOrderReverse) throws IOException {
ContentResolver cr = context.getContentResolver();
String sortOrder = "date DESC";
try (Cursor c = cr.query(Conversations.CONTENT_URI, null, null, null , sortOrder)) {
c.moveToLast();

// `THREAD_ID` is used to select messages for a conversation, so do not allow caller to pass it.
if (messageSelection != null && messageSelection.matches("^(.*[ \t\n])?" + THREAD_ID + "[ \t\n].*$")) {
throw new IllegalArgumentException(
"The 'conversation-selection' cannot contain '" + THREAD_ID + "': `" + messageSelection + "`");
}

conversationSortOrder = getSortOrder(conversationSortOrder, conversationOffset, conversationLimit);
messageSortOrder = getSortOrder(messageSortOrder, messageOffset, messageLimit);

int index;
try (Cursor conversationCursor = cr.query(Conversations.CONTENT_URI,
null, conversationSelection, null , conversationSortOrder)) {
int conversationCount = conversationCursor.getCount();
if (conversationReturnNoOrderReverse) {
conversationCursor.moveToFirst();
} else {
conversationCursor.moveToLast();
}

Map<String, String> nameCache = new HashMap<>();

out.beginArray();
for (int i = 0, count = c.getCount(); i < count; i++) {
int id = c.getInt(c.getColumnIndex(THREAD_ID));

Cursor cc = cr.query(Sms.CONTENT_URI, null,
THREAD_ID + " == '" + id +"'",
null, "date DESC");
if (cc.getCount() == 0) {
c.moveToNext();
if (conversationReturnNestedView) {
out.beginObject();
} else {
out.beginArray();
}
for (int i = 0; i < conversationCount; i++) {
index = conversationCursor.getColumnIndex(THREAD_ID);
if (index < 0) {
conversationCursor.moveToPrevious();
continue;
}
cc.moveToFirst();
writeElement(cc, out, nameCache, context);
cc.close();
c.moveToPrevious();

int id = conversationCursor.getInt(index);

if (conversationReturnNestedView) {
out.name(String.valueOf(id));
out.beginArray();
}

String[] messageSelectionArgs = null;
if (messageSelection == null || messageSelection.isEmpty()) {
messageSelection = "";
} else {
messageSelection += " ";
}

Cursor messageCursor = cr.query(Sms.CONTENT_URI, null,
messageSelection + THREAD_ID + " == '" + id +"'", messageSelectionArgs,
messageSortOrder);

int messageCount = messageCursor.getCount();
if (messageCount > 0) {
if (conversationReturnMultipleMessages) {
if (messageReturnNoOrderReverse) {
messageCursor.moveToFirst();
} else {
messageCursor.moveToLast();
}

for (int j = 0; j < messageCount; j++) {
writeElement(messageCursor, out, nameCache, context);

if (messageReturnNoOrderReverse) {
messageCursor.moveToNext();
} else {
messageCursor.moveToPrevious();
}
}
} else {
messageCursor.moveToFirst();
writeElement(messageCursor, out, nameCache, context);
}
}

messageCursor.close();

if (conversationReturnNestedView) {
out.endArray();
}

if (conversationReturnNoOrderReverse) {
conversationCursor.moveToNext();
} else {
conversationCursor.moveToPrevious();
}
}
if (conversationReturnNestedView) {
out.endObject();
} else {
out.endArray();
}
out.endArray();
}
}

@SuppressLint("SimpleDateFormat")
private static void writeElement(Cursor c, JsonWriter out, Map<String, String> nameCache, Context context) throws IOException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

int index;
int threadID = c.getInt(c.getColumnIndexOrThrow(THREAD_ID));
String smsAddress = c.getString(c.getColumnIndexOrThrow(ADDRESS));
String smsBody = c.getString(c.getColumnIndexOrThrow(BODY));
boolean read = (c.getInt(c.getColumnIndex(READ)) != 0);
long smsReceivedDate = c.getLong(c.getColumnIndexOrThrow(DATE));
// long smsSentDate = c.getLong(c.getColumnIndexOrThrow(TextBasedSmsColumns.DATE_SENT));
int smsID = c.getInt(c.getColumnIndexOrThrow("_id"));
Expand All @@ -103,7 +249,11 @@ private static void writeElement(Cursor c, JsonWriter out, Map<String, String> n
out.beginObject();
out.name("threadid").value(threadID);
out.name("type").value(messageType);
out.name("read").value(read);

index = c.getColumnIndex(READ);
if (index >= 0) {
out.name("read").value(c.getInt(index) != 0);
}

if (smsSenderName != null) {
if (messageType.equals("inbox")) {
Expand Down Expand Up @@ -131,31 +281,67 @@ private static void writeElement(Cursor c, JsonWriter out, Map<String, String> n


@SuppressLint("SimpleDateFormat")
public static void getAllSms(Context context, JsonWriter out, int offset, int limit, String number, Uri contentURI) throws IOException {
public static void getAllSms(Context context, JsonWriter out,
Uri contentURI,
int messageOffset, int messageLimit,
String messageSelection, String messageAddress,
String messageSortOrder,
boolean messageReturnNoOrderReverse) throws IOException {
ContentResolver cr = context.getContentResolver();
String sortOrder = "date DESC LIMIT + " + limit + " OFFSET " + offset;
try (Cursor c = cr.query(contentURI, null,
ADDRESS + " LIKE '%" + number + "%'", null, sortOrder)) {
c.moveToLast();

String[] messageSelectionArgs = null;
if (messageSelection == null || messageSelection.isEmpty()) {
if (messageAddress != null && !messageAddress.isEmpty()) {
messageSelection = ADDRESS + " LIKE '%?%'";
messageSelectionArgs = new String[]{messageAddress};
}
}

messageSortOrder = getSortOrder(messageSortOrder, messageOffset, messageLimit);

try (Cursor messageCursor = cr.query(contentURI, null,
messageSelection, messageSelectionArgs,
messageSortOrder)) {
int messageCount = messageCursor.getCount();
if (messageReturnNoOrderReverse) {
messageCursor.moveToFirst();
} else {
messageCursor.moveToLast();
}

new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Map<String, String> nameCache = new HashMap<>();

out.beginArray();
for (int i = 0, count = c.getCount(); i < count; i++) {
writeElement(c, out, nameCache, context);
c.moveToPrevious();
for (int i = 0; i < messageCount; i++) {
writeElement(messageCursor, out, nameCache, context);

if (messageReturnNoOrderReverse) {
messageCursor.moveToNext();
} else {
messageCursor.moveToPrevious();
}
}
out.endArray();
}
}

private static String getContactNameFromNumber(Map<String, String> cache, Context context, String number) {
if (cache.containsKey(number))
if (cache.containsKey(number)) {
return cache.get(number);
}

int index;
Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
try (Cursor c = context.getContentResolver().query(contactUri, DISPLAY_NAME_PROJECTION, null, null, null)) {
String name = c.moveToFirst() ? c.getString(c.getColumnIndex(PhoneLookup.DISPLAY_NAME)) : null;
String name = null;
if (c.moveToFirst()) {
index = c.getColumnIndex(PhoneLookup.DISPLAY_NAME);
if (index >= 0) {
name = c.getString(index);
}
}

cache.put(number, name);
return name;
}
Expand Down Expand Up @@ -195,4 +381,21 @@ private static Uri typeToContentURI(int type) {
}
}

@Nullable
private static String getSortOrder(String sortOrder, int offset, int limit) {
if (sortOrder == null) {
sortOrder = "";
}
if (limit >= 0) {
sortOrder += " LIMIT " + limit;
}
if (offset >= 0) {
sortOrder += " OFFSET " + offset;
}
if (sortOrder.isEmpty()) {
sortOrder = null;
}
return sortOrder;
}

}

2 comments on commit 1b60b23

@256-1
Copy link

@256-1 256-1 commented on 1b60b23 Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an error when trying to call "termux-sms-list -f xxxxx", enclosed with the log.

termux-plugin_execution_command.log

@agnostic-apollo
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the report, forgot to test that among so many changes. Fixed by 12e7b65.

Please sign in to comment.