Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a0fb1c0c authored by Andrew Cheng's avatar Andrew Cheng
Browse files

mapclient: human-readable timestamp logging and dumpsys

Make logging of timestamps human-readable instead of `long`. Also add
recent messages (including timestamps) of `Inbox` and `Sent` folders to
dumpsys.

Bug: 321281779
Test: atest MapClientContentTest
Test: adb shell dumpsys bluetooth_manager
Flag: EXEMPT, change to logging only
Change-Id: Ica9f68c6081582c4c3be8ef4d84ad3c0bfb292f5
parent ba211efa
Loading
Loading
Loading
Loading
+241 −12
Original line number Original line Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.Context;
import android.database.ContentObserver;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.Cursor;
import android.net.Uri;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.Telephony;
import android.provider.Telephony;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.MmsSms;
@@ -45,7 +46,12 @@ import com.android.vcard.VCardProperty;


import com.google.android.mms.pdu.PduHeaders;
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.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashMap;
import java.util.List;
import java.util.List;
import java.util.Set;
import java.util.Set;
@@ -58,6 +64,20 @@ class MapClientContent {
    private static final int ORIGINATOR_ADDRESS_TYPE = 137;
    private static final int ORIGINATOR_ADDRESS_TYPE = 137;
    private static final int RECIPIENT_ADDRESS_TYPE = 151;
    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;
    final BluetoothDevice mDevice;
    private final Context mContext;
    private final Context mContext;
    private final Callbacks mCallbacks;
    private final Callbacks mCallbacks;
@@ -81,18 +101,16 @@ class MapClientContent {
    /**
    /**
     * MapClientContent manages all interactions between Bluetooth and the messaging provider.
     * MapClientContent manages all interactions between Bluetooth and the messaging provider.
     *
     *
     * Changes to the database are mirrored between the remote and local providers, specifically new
     * <p>Changes to the database are mirrored between the remote and local providers, specifically
     * messages, changes to read status, and removal of messages.
     * 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
     * <p>context: the context that all content provider interactions are conducted MceStateMachine:
     * MceStateMachine:  the interface to send outbound updates such as when a message is read
     * the interface to send outbound updates such as when a message is read locally device: the
     * locally
     * associated Bluetooth device used for associating messages with a subscription
     * device: the associated Bluetooth device used for associating messages with a subscription
     */
     */
    MapClientContent(Context context, Callbacks callbacks,
    MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) {
            BluetoothDevice device) {
        mContext = context;
        mContext = context;
        mDevice = device;
        mDevice = device;
        mCallbacks = callbacks;
        mCallbacks = callbacks;
@@ -187,9 +205,20 @@ class MapClientContent {
     * The handle is used to associate the local message with the remote message.
     * The handle is used to associate the local message with the remote message.
     */
     */
    void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) {
    void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) {
        logI("storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp
        logI(
                + ", handle=" + handle + ", type=" + message.getType()
                "storeMessage(device="
                + ", folder=" + message.getFolder());
                        + Utils.getLoggableAddress(mDevice)
                        + ", time="
                        + timestamp
                        + "["
                        + toDatetimeString(timestamp)
                        + "]"
                        + ", handle="
                        + handle
                        + ", type="
                        + message.getType()
                        + ", folder="
                        + message.getFolder());


        switch (message.getType()) {
        switch (message.getType()) {
            case MMS:
            case MMS:
@@ -609,6 +638,146 @@ class MapClientContent {
        return count;
        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) {
    public void dump(StringBuilder sb) {
        sb.append("    Device Message DB:");
        sb.append("    Device Message DB:");
        sb.append("\n      Subscription ID: " + mSubscriptionId);
        sb.append("\n      Subscription ID: " + mSubscriptionId);
@@ -624,6 +793,17 @@ class MapClientContent {
                    + " / " + getStoredMessagesCount(Mms.CONTENT_URI));
                    + " / " + getStoredMessagesCount(Mms.CONTENT_URI));


            sb.append("\n      Threads: " + getStoredMessagesCount(Threads.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");
        sb.append("\n");
    }
    }
@@ -650,4 +830,53 @@ class MapClientContent {
                    .equals(mHandle));
                    .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;
            }
        }
    }
}
}
+14 −0
Original line number Original line Diff line number Diff line
@@ -462,6 +462,20 @@ public class MapClientContentTest {
        MapClientContent.clearAllContent(mMockContext);
        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() {
    void createTestMessages() {
        mOriginator = new VCardEntry();
        mOriginator = new VCardEntry();
        VCardProperty property = new VCardProperty();
        VCardProperty property = new VCardProperty();