Loading android/app/src/com/android/bluetooth/mapclient/MapClientContent.java +241 −12 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.provider.Telephony; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; Loading @@ -45,7 +46,12 @@ import com.android.vcard.VCardProperty; import com.google.android.mms.pdu.PduHeaders; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; Loading @@ -58,6 +64,20 @@ class MapClientContent { private static final int ORIGINATOR_ADDRESS_TYPE = 137; private static final int RECIPIENT_ADDRESS_TYPE = 151; private static final int NUM_RECENT_MSGS_TO_DUMP = 5; private enum Type { UNKNOWN, SMS, MMS } private enum Folder { UNKNOWN, INBOX, SENT } final BluetoothDevice mDevice; private final Context mContext; private final Callbacks mCallbacks; Loading @@ -81,18 +101,16 @@ class MapClientContent { /** * MapClientContent manages all interactions between Bluetooth and the messaging provider. * * Changes to the database are mirrored between the remote and local providers, specifically new * messages, changes to read status, and removal of messages. * <p>Changes to the database are mirrored between the remote and local providers, specifically * new messages, changes to read status, and removal of messages. * * Object is invalid after cleanUp() is called. * <p>Object is invalid after cleanUp() is called. * * context: the context that all content provider interactions are conducted * MceStateMachine: the interface to send outbound updates such as when a message is read * locally * device: the associated Bluetooth device used for associating messages with a subscription * <p>context: the context that all content provider interactions are conducted MceStateMachine: * the interface to send outbound updates such as when a message is read locally device: the * associated Bluetooth device used for associating messages with a subscription */ MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) { MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) { mContext = context; mDevice = device; mCallbacks = callbacks; Loading Loading @@ -187,9 +205,20 @@ class MapClientContent { * The handle is used to associate the local message with the remote message. */ void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) { logI("storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp + ", handle=" + handle + ", type=" + message.getType() + ", folder=" + message.getFolder()); logI( "storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp + "[" + toDatetimeString(timestamp) + "]" + ", handle=" + handle + ", type=" + message.getType() + ", folder=" + message.getFolder()); switch (message.getType()) { case MMS: Loading Loading @@ -609,6 +638,146 @@ class MapClientContent { return count; } private List<MessageDumpElement> getRecentMessagesFromFolder(Folder folder) { Uri smsUri = null; Uri mmsUri = null; if (folder == Folder.INBOX) { smsUri = Sms.Inbox.CONTENT_URI; mmsUri = Mms.Inbox.CONTENT_URI; } else if (folder == Folder.SENT) { smsUri = Sms.Sent.CONTENT_URI; mmsUri = Mms.Sent.CONTENT_URI; } else { Log.w(TAG, "getRecentMessagesFromFolder: Failed, unsupported folder=" + folder); return null; } ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>(); for (Uri uri : new Uri[] {smsUri, mmsUri}) { messages.addAll(getMessagesFromUri(uri)); } logV( "getRecentMessagesFromFolder: " + folder + ", " + messages.size() + " messages found."); Collections.sort(messages); if (messages.size() > NUM_RECENT_MSGS_TO_DUMP) { return messages.subList(0, NUM_RECENT_MSGS_TO_DUMP); } return messages; } private List<MessageDumpElement> getMessagesFromUri(Uri uri) { logD("getMessagesFromUri: uri=" + uri); ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>(); if (mSubscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.w(TAG, "getMessagesFromUri: Failed, no subscription ID"); return messages; } Type type = getMessageTypeFromUri(uri); if (type == Type.UNKNOWN) { Log.w(TAG, "getMessagesFromUri: unknown message type"); return messages; } String[] selectionArgs = new String[] {Integer.toString(mSubscriptionId)}; String limit = " LIMIT " + NUM_RECENT_MSGS_TO_DUMP; String[] projection = null; String selectionClause = null; String threadIdColumnName = null; String timestampColumnName = null; if (type == Type.SMS) { projection = new String[] {BaseColumns._ID, Sms.THREAD_ID, Sms.DATE}; selectionClause = Sms.SUBSCRIPTION_ID + " =? "; threadIdColumnName = Sms.THREAD_ID; timestampColumnName = Sms.DATE; } else if (type == Type.MMS) { projection = new String[] {BaseColumns._ID, Mms.THREAD_ID, Mms.DATE}; selectionClause = Mms.SUBSCRIPTION_ID + " =? "; threadIdColumnName = Mms.THREAD_ID; timestampColumnName = Mms.DATE; } Cursor cursor = mResolver.query( uri, projection, selectionClause, selectionArgs, timestampColumnName + " DESC" + limit); try { if (cursor == null) { Log.w(TAG, "getMessagesFromUri: null cursor for uri=" + uri); return messages; } logV("Number of rows in cursor = " + cursor.getCount() + ", for uri=" + uri); cursor.moveToPosition(-1); while (cursor.moveToNext()) { // Even though {@link storeSms} and {@link storeMms} use Uris that contain the // folder name (e.g., {@code Sms.Inbox.CONTENT_URI}), the Uri returned by // {@link ContentResolver#insert} does not (e.g., {@code Sms.CONTENT_URI}). // Therefore, the Uris in the keyset of {@code mUriToHandleMap} do not contain // the folder name, but unfortunately, the Uri passed in to query the database // does contains the folder name, so we can't simply append messageId to the // passed-in Uri. String messageId = cursor.getString(cursor.getColumnIndex(BaseColumns._ID)); Uri messageUri = Uri.withAppendedPath( type == Type.SMS ? Sms.CONTENT_URI : Mms.CONTENT_URI, messageId); MessageStatus handleAndStatus = mUriToHandleMap.get(messageUri); String messageHandle = "<unknown>"; if (handleAndStatus == null) { Log.w(TAG, "getMessagesFromUri: no entry for message uri=" + messageUri); } else { messageHandle = handleAndStatus.mHandle; } long timestamp = cursor.getLong(cursor.getColumnIndex(timestampColumnName)); // TODO: why does `storeMms` truncate down to the seconds instead of keeping it // millisec, like `storeSms`? if (type == Type.MMS) { timestamp *= 1000L; } messages.add( new MessageDumpElement( messageHandle, messageUri, timestamp, cursor.getLong(cursor.getColumnIndex(threadIdColumnName)), type)); } } catch (Exception e) { Log.w(TAG, "Exception when querying db for dumpsys", e); } finally { cursor.close(); } return messages; } private Type getMessageTypeFromUri(Uri uri) { if (Sms.CONTENT_URI.equals(uri) || Sms.Inbox.CONTENT_URI.equals(uri) || Sms.Sent.CONTENT_URI.equals(uri)) { return Type.SMS; } else if (Mms.CONTENT_URI.equals(uri) || Mms.Inbox.CONTENT_URI.equals(uri) || Mms.Sent.CONTENT_URI.equals(uri)) { return Type.MMS; } else { return Type.UNKNOWN; } } public void dump(StringBuilder sb) { sb.append(" Device Message DB:"); sb.append("\n Subscription ID: " + mSubscriptionId); Loading @@ -624,6 +793,17 @@ class MapClientContent { + " / " + getStoredMessagesCount(Mms.CONTENT_URI)); sb.append("\n Threads: " + getStoredMessagesCount(Threads.CONTENT_URI)); sb.append("\n Most recent 'Sent' messages:"); sb.append("\n " + MessageDumpElement.getFormattedColumnNames()); for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.SENT)) { sb.append("\n " + e); } sb.append("\n Most recent 'Inbox' messages:"); sb.append("\n " + MessageDumpElement.getFormattedColumnNames()); for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.INBOX)) { sb.append("\n " + e); } } sb.append("\n"); } Loading @@ -650,4 +830,53 @@ class MapClientContent { .equals(mHandle)); } } @SuppressWarnings("GoodTime") // Use system time zone to render times for logging private static String toDatetimeString(long epochMillis) { return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS") .format( Instant.ofEpochMilli(epochMillis) .atZone(ZoneId.systemDefault()) .toLocalDateTime()); } private static class MessageDumpElement implements Comparable<MessageDumpElement> { private String mMessageHandle; private long mTimestamp; private Type mType; private long mThreadId; private Uri mUri; MessageDumpElement(String handle, Uri uri, long timestamp, long threadId, Type type) { mMessageHandle = handle; mTimestamp = timestamp; mUri = uri; mThreadId = threadId; mType = type; } public static String getFormattedColumnNames() { return String.format( "%-19s %s %-16s %s %s", "Timestamp", "ThreadId", "Handle", "Type", "Uri"); } @Override public String toString() { return String.format( "%-19s %8d %-16s %-4s %s", toDatetimeString(mTimestamp), mThreadId, mMessageHandle, mType, mUri); } @Override public int compareTo(MessageDumpElement e) { // we want reverse chronological. if (this.mTimestamp < e.mTimestamp) { return 1; } else if (this.mTimestamp > e.mTimestamp) { return -1; } else { return 0; } } } } android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientContentTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -462,6 +462,20 @@ public class MapClientContentTest { MapClientContent.clearAllContent(mMockContext); } /** * Test verifying dumpsys does not cause Bluetooth to crash (esp since we're querying the * database to generate dump). */ @Test public void testDumpsysDoesNotCauseCrash() { testStoreOneSMSOneMMS(); // mMapClientContent is set in testStoreOneSMSOneMMS StringBuilder sb = new StringBuilder("Hello world!\n"); mMapClientContent.dump(sb); assertThat(sb.toString()).isNotNull(); } void createTestMessages() { mOriginator = new VCardEntry(); VCardProperty property = new VCardProperty(); Loading Loading
android/app/src/com/android/bluetooth/mapclient/MapClientContent.java +241 −12 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.provider.Telephony; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; Loading @@ -45,7 +46,12 @@ import com.android.vcard.VCardProperty; import com.google.android.mms.pdu.PduHeaders; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; Loading @@ -58,6 +64,20 @@ class MapClientContent { private static final int ORIGINATOR_ADDRESS_TYPE = 137; private static final int RECIPIENT_ADDRESS_TYPE = 151; private static final int NUM_RECENT_MSGS_TO_DUMP = 5; private enum Type { UNKNOWN, SMS, MMS } private enum Folder { UNKNOWN, INBOX, SENT } final BluetoothDevice mDevice; private final Context mContext; private final Callbacks mCallbacks; Loading @@ -81,18 +101,16 @@ class MapClientContent { /** * MapClientContent manages all interactions between Bluetooth and the messaging provider. * * Changes to the database are mirrored between the remote and local providers, specifically new * messages, changes to read status, and removal of messages. * <p>Changes to the database are mirrored between the remote and local providers, specifically * new messages, changes to read status, and removal of messages. * * Object is invalid after cleanUp() is called. * <p>Object is invalid after cleanUp() is called. * * context: the context that all content provider interactions are conducted * MceStateMachine: the interface to send outbound updates such as when a message is read * locally * device: the associated Bluetooth device used for associating messages with a subscription * <p>context: the context that all content provider interactions are conducted MceStateMachine: * the interface to send outbound updates such as when a message is read locally device: the * associated Bluetooth device used for associating messages with a subscription */ MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) { MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) { mContext = context; mDevice = device; mCallbacks = callbacks; Loading Loading @@ -187,9 +205,20 @@ class MapClientContent { * The handle is used to associate the local message with the remote message. */ void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) { logI("storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp + ", handle=" + handle + ", type=" + message.getType() + ", folder=" + message.getFolder()); logI( "storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp + "[" + toDatetimeString(timestamp) + "]" + ", handle=" + handle + ", type=" + message.getType() + ", folder=" + message.getFolder()); switch (message.getType()) { case MMS: Loading Loading @@ -609,6 +638,146 @@ class MapClientContent { return count; } private List<MessageDumpElement> getRecentMessagesFromFolder(Folder folder) { Uri smsUri = null; Uri mmsUri = null; if (folder == Folder.INBOX) { smsUri = Sms.Inbox.CONTENT_URI; mmsUri = Mms.Inbox.CONTENT_URI; } else if (folder == Folder.SENT) { smsUri = Sms.Sent.CONTENT_URI; mmsUri = Mms.Sent.CONTENT_URI; } else { Log.w(TAG, "getRecentMessagesFromFolder: Failed, unsupported folder=" + folder); return null; } ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>(); for (Uri uri : new Uri[] {smsUri, mmsUri}) { messages.addAll(getMessagesFromUri(uri)); } logV( "getRecentMessagesFromFolder: " + folder + ", " + messages.size() + " messages found."); Collections.sort(messages); if (messages.size() > NUM_RECENT_MSGS_TO_DUMP) { return messages.subList(0, NUM_RECENT_MSGS_TO_DUMP); } return messages; } private List<MessageDumpElement> getMessagesFromUri(Uri uri) { logD("getMessagesFromUri: uri=" + uri); ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>(); if (mSubscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.w(TAG, "getMessagesFromUri: Failed, no subscription ID"); return messages; } Type type = getMessageTypeFromUri(uri); if (type == Type.UNKNOWN) { Log.w(TAG, "getMessagesFromUri: unknown message type"); return messages; } String[] selectionArgs = new String[] {Integer.toString(mSubscriptionId)}; String limit = " LIMIT " + NUM_RECENT_MSGS_TO_DUMP; String[] projection = null; String selectionClause = null; String threadIdColumnName = null; String timestampColumnName = null; if (type == Type.SMS) { projection = new String[] {BaseColumns._ID, Sms.THREAD_ID, Sms.DATE}; selectionClause = Sms.SUBSCRIPTION_ID + " =? "; threadIdColumnName = Sms.THREAD_ID; timestampColumnName = Sms.DATE; } else if (type == Type.MMS) { projection = new String[] {BaseColumns._ID, Mms.THREAD_ID, Mms.DATE}; selectionClause = Mms.SUBSCRIPTION_ID + " =? "; threadIdColumnName = Mms.THREAD_ID; timestampColumnName = Mms.DATE; } Cursor cursor = mResolver.query( uri, projection, selectionClause, selectionArgs, timestampColumnName + " DESC" + limit); try { if (cursor == null) { Log.w(TAG, "getMessagesFromUri: null cursor for uri=" + uri); return messages; } logV("Number of rows in cursor = " + cursor.getCount() + ", for uri=" + uri); cursor.moveToPosition(-1); while (cursor.moveToNext()) { // Even though {@link storeSms} and {@link storeMms} use Uris that contain the // folder name (e.g., {@code Sms.Inbox.CONTENT_URI}), the Uri returned by // {@link ContentResolver#insert} does not (e.g., {@code Sms.CONTENT_URI}). // Therefore, the Uris in the keyset of {@code mUriToHandleMap} do not contain // the folder name, but unfortunately, the Uri passed in to query the database // does contains the folder name, so we can't simply append messageId to the // passed-in Uri. String messageId = cursor.getString(cursor.getColumnIndex(BaseColumns._ID)); Uri messageUri = Uri.withAppendedPath( type == Type.SMS ? Sms.CONTENT_URI : Mms.CONTENT_URI, messageId); MessageStatus handleAndStatus = mUriToHandleMap.get(messageUri); String messageHandle = "<unknown>"; if (handleAndStatus == null) { Log.w(TAG, "getMessagesFromUri: no entry for message uri=" + messageUri); } else { messageHandle = handleAndStatus.mHandle; } long timestamp = cursor.getLong(cursor.getColumnIndex(timestampColumnName)); // TODO: why does `storeMms` truncate down to the seconds instead of keeping it // millisec, like `storeSms`? if (type == Type.MMS) { timestamp *= 1000L; } messages.add( new MessageDumpElement( messageHandle, messageUri, timestamp, cursor.getLong(cursor.getColumnIndex(threadIdColumnName)), type)); } } catch (Exception e) { Log.w(TAG, "Exception when querying db for dumpsys", e); } finally { cursor.close(); } return messages; } private Type getMessageTypeFromUri(Uri uri) { if (Sms.CONTENT_URI.equals(uri) || Sms.Inbox.CONTENT_URI.equals(uri) || Sms.Sent.CONTENT_URI.equals(uri)) { return Type.SMS; } else if (Mms.CONTENT_URI.equals(uri) || Mms.Inbox.CONTENT_URI.equals(uri) || Mms.Sent.CONTENT_URI.equals(uri)) { return Type.MMS; } else { return Type.UNKNOWN; } } public void dump(StringBuilder sb) { sb.append(" Device Message DB:"); sb.append("\n Subscription ID: " + mSubscriptionId); Loading @@ -624,6 +793,17 @@ class MapClientContent { + " / " + getStoredMessagesCount(Mms.CONTENT_URI)); sb.append("\n Threads: " + getStoredMessagesCount(Threads.CONTENT_URI)); sb.append("\n Most recent 'Sent' messages:"); sb.append("\n " + MessageDumpElement.getFormattedColumnNames()); for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.SENT)) { sb.append("\n " + e); } sb.append("\n Most recent 'Inbox' messages:"); sb.append("\n " + MessageDumpElement.getFormattedColumnNames()); for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.INBOX)) { sb.append("\n " + e); } } sb.append("\n"); } Loading @@ -650,4 +830,53 @@ class MapClientContent { .equals(mHandle)); } } @SuppressWarnings("GoodTime") // Use system time zone to render times for logging private static String toDatetimeString(long epochMillis) { return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS") .format( Instant.ofEpochMilli(epochMillis) .atZone(ZoneId.systemDefault()) .toLocalDateTime()); } private static class MessageDumpElement implements Comparable<MessageDumpElement> { private String mMessageHandle; private long mTimestamp; private Type mType; private long mThreadId; private Uri mUri; MessageDumpElement(String handle, Uri uri, long timestamp, long threadId, Type type) { mMessageHandle = handle; mTimestamp = timestamp; mUri = uri; mThreadId = threadId; mType = type; } public static String getFormattedColumnNames() { return String.format( "%-19s %s %-16s %s %s", "Timestamp", "ThreadId", "Handle", "Type", "Uri"); } @Override public String toString() { return String.format( "%-19s %8d %-16s %-4s %s", toDatetimeString(mTimestamp), mThreadId, mMessageHandle, mType, mUri); } @Override public int compareTo(MessageDumpElement e) { // we want reverse chronological. if (this.mTimestamp < e.mTimestamp) { return 1; } else if (this.mTimestamp > e.mTimestamp) { return -1; } else { return 0; } } } }
android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientContentTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -462,6 +462,20 @@ public class MapClientContentTest { MapClientContent.clearAllContent(mMockContext); } /** * Test verifying dumpsys does not cause Bluetooth to crash (esp since we're querying the * database to generate dump). */ @Test public void testDumpsysDoesNotCauseCrash() { testStoreOneSMSOneMMS(); // mMapClientContent is set in testStoreOneSMSOneMMS StringBuilder sb = new StringBuilder("Hello world!\n"); mMapClientContent.dump(sb); assertThat(sb.toString()).isNotNull(); } void createTestMessages() { mOriginator = new VCardEntry(); VCardProperty property = new VCardProperty(); Loading