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

Commit eb71c44c authored by Andrew Cheng's avatar Andrew Cheng Committed by Gerrit Code Review
Browse files

Merge "Obtain remote device's own phonenumber from MessagesListing"

parents 44402bff 0fe90f3b
Loading
Loading
Loading
Loading
+8 −21
Original line number Diff line number Diff line
@@ -45,7 +45,6 @@ import com.android.vcard.VCardProperty;

import com.google.android.mms.pdu.PduHeaders;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -167,26 +166,12 @@ class MapClientContent {
    }

    /**
     * parseLocalNumber
     *
     * Determine the connected phone's number by extracting it from an inbound or outbound mms
     * message.  This number is necessary such that group messages can be displayed correctly.
     * This number is necessary for thread_id to work properly. thread_id is needed for
     * (group) MMS messages to be displayed/stitched correctly.
     */
    void parseLocalNumber(Bmessage message) {
        if (mPhoneNumber != null) {
            return;
        }
        if (INBOX_PATH.equals(message.getFolder())) {
            ArrayList<VCardEntry> recipients = message.getRecipients();
            if (recipients != null && !recipients.isEmpty()) {
                mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(
                        getFirstRecipientNumber(message));
            }
        } else {
            mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
        }

        logV("Found phone number: " + mPhoneNumber);
    void setRemoteDeviceOwnNumber(String phoneNumber) {
        mPhoneNumber = phoneNumber;
        logV("Remote device " + mDevice.getAddress() + " phone number set to: " + mPhoneNumber);
    }

    /**
@@ -310,7 +295,6 @@ class MapClientContent {
        logD("storeMms");
        logV(message.toString());
        try {
            parseLocalNumber(message);
            ContentValues values = new ContentValues();
            long threadId = getThreadId(message);
            BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
@@ -464,6 +448,9 @@ class MapClientContent {
        if (messageContacts.isEmpty()) {
            return Telephony.Threads.COMMON_THREAD;
        } else if (messageContacts.size() > 1) {
            if (mPhoneNumber == null) {
                Log.w(TAG, "getThreadId called, mPhoneNumber never found.");
            }
            messageContacts.removeIf(number -> (PhoneNumberUtils.areSamePhoneNumber(number,
                    mPhoneNumber, mTelephonyManager.getNetworkCountryIso())));
        }
+14 −0
Original line number Diff line number Diff line
@@ -196,6 +196,20 @@ public class MasClient {
        return true;
    }

    /**
     * Invokes {@link Request#abort} and removes it from the Handler's message queue.
     *
     * @param request The {@link Request} to abort.
     */
    public void abortRequest(Request request) {
        if (DBG) {
            Log.d(TAG, "abortRequest called with: " + request);
        }

        request.abort();
        mHandler.removeMessages(REQUEST, request);
    }

    public void shutdown() {
        mHandler.obtainMessage(DISCONNECT).sendToTarget();
        mThread.quitSafely();
+99 −7
Original line number Diff line number Diff line
@@ -100,6 +100,7 @@ class MceStateMachine extends StateMachine {
    static final int MSG_GET_MESSAGE_LISTING = 2005;
    // Set message status to read or deleted
    static final int MSG_SET_MESSAGE_STATUS = 2006;
    static final int MSG_SEARCH_OWN_NUMBER_TIMEOUT = 2007;

    private static final String TAG = "MceStateMachine";
    private static final Boolean DBG = MapClientService.DBG;
@@ -118,11 +119,10 @@ class MceStateMachine extends StateMachine {
    private static final String FOLDER_TELECOM = "telecom";
    private static final String FOLDER_MSG = "msg";
    private static final String FOLDER_OUTBOX = "outbox";
    private static final String FOLDER_INBOX = "inbox";
    private static final String FOLDER_SENT = "sent";
    static final String FOLDER_INBOX = "inbox";
    static final String FOLDER_SENT = "sent";
    private static final String INBOX_PATH = "telecom/msg/inbox";


    // Connectivity States
    private int mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
    private State mDisconnected;
@@ -140,6 +140,13 @@ class MceStateMachine extends StateMachine {
            new HashMap<>(MAX_MESSAGES);
    private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA;

    // The amount of time for MCE to search for remote device's own phone number before:
    // (1) MCE registering itself for being notified of the arrival of new messages; and
    // (2) MCE start downloading existing messages off of MSE.
    // NOTE: the value is not "final" so that it can be modified in the unit tests
    @VisibleForTesting
    static int sOwnNumberSearchTimeoutMs = 3_000;

    /**
     * An object to hold the necessary meta-data for each message so we can broadcast it alongside
     * the message content.
@@ -200,7 +207,6 @@ class MceStateMachine extends StateMachine {
        mDisconnecting = new Disconnecting();
        mConnected = new Connected();


        addState(mDisconnected);
        addState(mConnecting);
        addState(mDisconnecting);
@@ -537,9 +543,19 @@ class MceStateMachine extends StateMachine {
            mMasClient.makeRequest(new RequestSetPath(FOLDER_INBOX));
            mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
            mMasClient.makeRequest(new RequestSetPath(false));
            mMasClient.makeRequest(new RequestSetNotificationRegistration(true));
            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT);
            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
            // Start searching for remote device's own phone number. Only until either:
            //   (a) the search completes (with or without finding the number), or
            //   (b) the timeout expires,
            // does the MCE:
            //   (a) register itself for being notified of the arrival of new messages, and
            //   (b) start downloading existing messages off of MSE.
            // In other words, the MCE shouldn't handle any messages (new or existing) until after
            // it has tried obtaining the remote's own phone number.
            RequestGetMessagesListingForOwnNumber requestForOwnNumber =
                    new RequestGetMessagesListingForOwnNumber();
            mMasClient.makeRequest(requestForOwnNumber);
            sendMessageDelayed(MSG_SEARCH_OWN_NUMBER_TIMEOUT, requestForOwnNumber,
                    sOwnNumberSearchTimeoutMs);
        }

        @Override
@@ -620,6 +636,9 @@ class MceStateMachine extends StateMachine {
                        processMessageListing((RequestGetMessagesListing) message.obj);
                    } else if (message.obj instanceof RequestSetMessageStatus) {
                        processSetMessageStatus((RequestSetMessageStatus) message.obj);
                    } else if (message.obj instanceof RequestGetMessagesListingForOwnNumber) {
                        processMessageListingForOwnNumber(
                                (RequestGetMessagesListingForOwnNumber) message.obj);
                    }
                    break;

@@ -630,6 +649,26 @@ class MceStateMachine extends StateMachine {
                    }
                    break;

                case MSG_SEARCH_OWN_NUMBER_TIMEOUT:
                    Log.w(TAG, "Timeout while searching for own phone number.");
                    // Abort any outstanding Request so it doesn't execute on MasClient
                    RequestGetMessagesListingForOwnNumber request =
                            (RequestGetMessagesListingForOwnNumber) message.obj;
                    mMasClient.abortRequest(request);
                    // Remove any executed/completed Request that MasClient has passed back to
                    // state machine. Note: {@link StateMachine} doesn't provide a {@code
                    // removeMessages(int what, Object obj)}, nor direct access to {@link
                    // mSmHandler}, so this will remove *all* {@code MSG_MAS_REQUEST_COMPLETED}
                    // messages. However, {@link RequestGetMessagesListingForOwnNumber} should be
                    // the only MAS Request enqueued at this point, since none of the other MAS
                    // Requests should trigger/start until after getOwnNumber has completed.
                    removeMessages(MSG_MAS_REQUEST_COMPLETED);
                    // If failed to complete search for remote device's own phone number,
                    // proceed without it (i.e., register MCE for MNS and start download
                    // of existing messages from MSE).
                    notificationRegistrationAndStartDownloadMessages();
                    break;

                default:
                    Log.w(TAG, "Unexpected message: " + message.what + " from state:"
                            + this.getName());
@@ -740,6 +779,59 @@ class MceStateMachine extends StateMachine {
            }
        }

        /**
         * Process the result of a MessageListing request that was made specifically to obtain
         * the remote device's own phone number.
         *
         * @param request - A request object that has been resolved and returned with:
         *   - a phone number (possibly null if a number wasn't found)
         *   - a flag indicating whether there are still messages that can be searched/requested.
         *   - the request will automatically update itself if a number wasn't found and there are
         *     still messages that can be searched.
         */
        private void processMessageListingForOwnNumber(
                RequestGetMessagesListingForOwnNumber request) {

            if (request.isSearchCompleted()) {
                if (DBG) {
                    Log.d(TAG, "processMessageListingForOwnNumber: search completed");
                }
                if (request.getOwnNumber() != null) {
                    // A phone number was found (should be the remote device's).
                    if (DBG) {
                        Log.d(TAG, "processMessageListingForOwnNumber: number found = "
                                + request.getOwnNumber());
                    }
                    mDatabase.setRemoteDeviceOwnNumber(request.getOwnNumber());
                }
                // Remove any outstanding timeouts from state machine queue
                removeDeferredMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
                removeMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
                // Move on to next stage of connection process
                notificationRegistrationAndStartDownloadMessages();
            } else {
                // A phone number wasn't found, but there are still additional messages that can
                // be requested and searched.
                if (DBG) {
                    Log.d(TAG, "processMessageListingForOwnNumber: continuing search");
                }
                mMasClient.makeRequest(request);
            }
        }

        /**
         * (1) MCE registering itself for being notified of the arrival of new messages; and
         * (2) MCE downloading existing messages of off MSE.
         */
        private void notificationRegistrationAndStartDownloadMessages() {
            if (DBG) {
                Log.d(TAG, "registering for notifications and starting downloads");
            }
            mMasClient.makeRequest(new RequestSetNotificationRegistration(true));
            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT);
            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
        }

        private void processSetMessageStatus(RequestSetMessageStatus request) {
            if (DBG) {
                Log.d(TAG, "processSetMessageStatus");
+27 −9
Original line number Diff line number Diff line
@@ -69,9 +69,11 @@ abstract class Request {
    protected static final byte TRANSPARENT_ON = 0x01;
    protected static final byte RETRY_OFF = 0x00;
    protected static final byte RETRY_ON = 0x01;
    protected HeaderSet mHeaderSet;

    protected HeaderSet mHeaderSet;
    protected int mResponseCode;
    private boolean mAborted = false;
    private ClientOperation mOp = null;

    Request() {
        mHeaderSet = new HeaderSet();
@@ -80,10 +82,14 @@ abstract class Request {
    public abstract void execute(ClientSession session) throws IOException;

    protected void executeGet(ClientSession session) throws IOException {
        ClientOperation op = null;
        /* in case request is aborted before can be executed */
        if (mAborted) {
            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
            return;
        }

        try {
            op = (ClientOperation) session.get(mHeaderSet);
            mOp = (ClientOperation) session.get(mHeaderSet);

            /*
             * MAP spec does not explicitly require that GET request should be
@@ -92,23 +98,23 @@ abstract class Request {
             * headers. So this is workaround, at least temporary. TODO: check
             * with PTS
             */
            op.setGetFinalFlag(true);
            mOp.setGetFinalFlag(true);

            /*
             * this will trigger ClientOperation to use non-buffered stream so
             * we can abort operation
             */
            op.continueOperation(true, false);
            mOp.continueOperation(true, false);

            readResponseHeaders(op.getReceivedHeader());
            readResponseHeaders(mOp.getReceivedHeader());

            InputStream is = op.openInputStream();
            InputStream is = mOp.openInputStream();
            readResponse(is);
            is.close();

            op.close();
            mOp.close();

            mResponseCode = op.getResponseCode();
            mResponseCode = mOp.getResponseCode();
        } catch (IOException e) {
            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;

@@ -139,6 +145,18 @@ abstract class Request {
        }
    }

    public void abort() {
        mAborted = true;

        if (mOp != null) {
            try {
                mOp.abort();
            } catch (IOException e) {
                // Do nothing
            }
        }
    }

    public final boolean isSuccess() {
        return (mResponseCode == ResponseCodes.OBEX_HTTP_OK);
    }
+300 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.bluetooth.mapclient;

import static java.lang.Math.min;

import android.telephony.PhoneNumberUtils;
import android.util.Log;

import com.android.bluetooth.ObexAppParameters;
import com.android.internal.annotations.VisibleForTesting;
import com.android.obex.ClientSession;
import com.android.obex.HeaderSet;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Request to get a listing of messages in directory. Listing is used to determine the
 * remote device's own phone number. Searching the SENT folder is the most reliable way
 * since there should only be one Originator (From:), as opposed to the INBOX folder,
 * where there can be multiple Recipients (To: and Cc:).
 *
 * Ideally, only a single message is needed; however, the Originator (From:) field in the listing
 * is optional (not required by specs). Hence, a geometrically increasing sliding window is used
 * to request additional message listings until either a number is found or folders have been
 * exhausted.
 *
 * The sliding window is automated (i.e., offset and size, transitions across folders). Simply use
 * the same {@link RequestGetMessagesListingForOwnNumber} repeatedly with {@link
 * MasClient#makeRequest}. {@link #isSearchCompleted} indicates when the search is complete,
 * i.e., the object cannot be used further.
 */
class RequestGetMessagesListingForOwnNumber extends Request {
    private static final String TAG = RequestGetMessagesListingForOwnNumber.class.getSimpleName();

    private static final String TYPE = "x-bt/MAP-msg-listing";

    // Search for sent messages (MMS or SMS) first. If that fails, search for received SMS.
    @VisibleForTesting
    static final List<String> FOLDERS_TO_SEARCH = new ArrayList<>(Arrays.asList(
            MceStateMachine.FOLDER_SENT,
            MceStateMachine.FOLDER_INBOX
    ));

    private static final int MAX_LIST_COUNT_INITIAL = 1;
    // NOTE: the value is not "final" so that it can be modified in the unit tests
    @VisibleForTesting
    static int sMaxListCountUpperLimit = 65535;
    private static final int LIST_START_OFFSET_INITIAL = 0;
    // NOTE: the value is not "final" so that it can be modified in the unit tests
    @VisibleForTesting
    static int sListStartOffsetUpperLimit = 65535;

    /**
     * A geometrically increasing sliding window for messages to list.
     *
     * E.g., if we don't find the phone number in the 1st message, try the next 2, then the next 4,
     * then the next 8, etc.
     */
    private static class MessagesSlidingWindow {
        private int mListStartOffset;
        private int mMaxListCount;

        MessagesSlidingWindow() {
            reset();
        }

        /**
         * Returns false if start of window exceeds range; o.w. returns true.
         */
        public boolean moveWindow() {
            if (mListStartOffset > sListStartOffsetUpperLimit) {
                return false;
            }
            mListStartOffset = mListStartOffset + mMaxListCount;
            if (mListStartOffset > sListStartOffsetUpperLimit) {
                return false;
            }
            mMaxListCount = min(2 * mMaxListCount, sMaxListCountUpperLimit);
            logD(String.format(Locale.US,
                    "MessagesSlidingWindow, moveWindow: startOffset=%d, maxCount=%d",
                    mListStartOffset, mMaxListCount));
            return true;
        }

        public void reset() {
            mListStartOffset = LIST_START_OFFSET_INITIAL;
            mMaxListCount = MAX_LIST_COUNT_INITIAL;
        }

        public int getStartOffset() {
            return mListStartOffset;
        }

        public int getMaxCount() {
            return mMaxListCount;
        }
    }
    private MessagesSlidingWindow mMessageListingWindow;

    private ObexAppParameters mOap;

    private int mFolderCounter;
    private boolean mSearchCompleted;
    private String mPhoneNumber;

    RequestGetMessagesListingForOwnNumber() {
        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
        mOap = new ObexAppParameters();

        mMessageListingWindow = new MessagesSlidingWindow();

        mFolderCounter = 0;
        setupCurrentFolderForSearch();

        mSearchCompleted = false;
        mPhoneNumber = null;
    }

    @Override
    protected void readResponse(InputStream stream) {
        if (mSearchCompleted) {
            return;
        }

        MessagesListing response = new MessagesListing(stream);

        if (response == null) {
            // This shouldn't have happened; move on to the next window
            logD("readResponse: null Response, moving to next window");
            moveToNextWindow();
            return;
        }

        ArrayList<Message> messageListing = response.getList();
        if (messageListing == null || messageListing.isEmpty()) {
            // No more messages in this folder; move on to the next folder;
            logD("readResponse: no messages, moving to next folder");
            moveToNextFolder();
            return;
        }

        // Search through message listing for own phone number.
        // Message listings by spec arrive ordered newest first.
        String folderName = FOLDERS_TO_SEARCH.get(mFolderCounter);
        logD(String.format(Locale.US,
                "readResponse: Folder=%s, # of msgs=%d, startOffset=%d, maxCount=%d",
                folderName, messageListing.size(),
                mMessageListingWindow.getStartOffset(), mMessageListingWindow.getMaxCount()));
        String number = null;
        for (int i = 0; i < messageListing.size(); i++) {
            Message msg = messageListing.get(i);
            if (MceStateMachine.FOLDER_INBOX.equals(folderName)) {
                number = PhoneNumberUtils.extractNetworkPortion(
                        msg.getRecipientAddressing());
            } else if (MceStateMachine.FOLDER_SENT.equals(folderName)) {
                number = PhoneNumberUtils.extractNetworkPortion(
                        msg.getSenderAddressing());
            }
            if (number != null && !number.trim().isEmpty()) {
                // Search is completed when a phone number is found
                mPhoneNumber = number;
                mSearchCompleted = true;
                logD(String.format("readResponse: phone number found = %s", mPhoneNumber));
                return;
            }
        }

        // If a number hasn't been found, move on to the next window.
        if (!mSearchCompleted) {
            logD("readResponse: number hasn't been found, moving to next window");
            moveToNextWindow();
        }
    }

    /**
     * Move on to next folder to start searching (sliding window).
     *
     * Overall search for own-phone-number is completed when we run out of folders to search.
     */
    private void moveToNextFolder() {
        if (mFolderCounter < FOLDERS_TO_SEARCH.size() - 1) {
            mFolderCounter += 1;
            setupCurrentFolderForSearch();
        } else {
            logD("moveToNextFolder: folders exhausted, search complete");
            mSearchCompleted = true;
        }
    }

    /**
     * Tries sliding the window in the current folder.
     *   - If successful (didn't exceed range), update the headers to reflect new window's
     *     offset and size.
     *   - If fails (exceeded range), move on to the next folder.
     */
    private void moveToNextWindow() {
        if (mMessageListingWindow.moveWindow()) {
            setListOffsetAndMaxCountInHeaderSet(mMessageListingWindow.getMaxCount(),
                    mMessageListingWindow.getStartOffset());
        } else {
            // Can't slide window anymore, exceeded range; move on to next folder
            logD("moveToNextWindow: can't slide window anymore, folder complete");
            moveToNextFolder();
        }
    }

    /**
     * Set up the current folder for searching:
     *   1. Updates headers to reflect new folder name.
     *   2. Resets the sliding window.
     *   3. Updates headers to reflect new window's offset and size.
     */
    private void setupCurrentFolderForSearch() {
        String folderName = FOLDERS_TO_SEARCH.get(mFolderCounter);
        mHeaderSet.setHeader(HeaderSet.NAME, folderName);

        byte filter = messageTypeBasedOnFolder(folderName);
        mOap.add(OAP_TAGID_FILTER_MESSAGE_TYPE, filter);
        mOap.addToHeaderSet(mHeaderSet);

        mMessageListingWindow.reset();
        int maxCount = mMessageListingWindow.getMaxCount();
        int offset = mMessageListingWindow.getStartOffset();
        setListOffsetAndMaxCountInHeaderSet(maxCount, offset);
        logD(String.format(Locale.US,
                "setupCurrentFolderForSearch: folder=%s, filter=%d, offset=%d, maxCount=%d",
                folderName, filter, maxCount, offset));
    }

    private byte messageTypeBasedOnFolder(String folderName) {
        byte messageType = (byte) (MessagesFilter.MESSAGE_TYPE_SMS_GSM
                | MessagesFilter.MESSAGE_TYPE_SMS_CDMA
                | MessagesFilter.MESSAGE_TYPE_MMS);

        // If trying to grab own number from messages received by the remote device,
        // only use SMS messages since SMS will only have one recipient (the remote device),
        // whereas MMS may have more than one recipient (e.g., group MMS or if the originator
        // is also CC-ed as a recipient). Even if there is only one recipient presented to
        // Bluetooth in a group MMS, it may not necessarily correspond to the remote device;
        // there is no specification governing the `To:` and `Cc:` fields in the MMS specs.
        if (MceStateMachine.FOLDER_INBOX.equals(folderName)) {
            messageType = (byte) (MessagesFilter.MESSAGE_TYPE_SMS_GSM
                    | MessagesFilter.MESSAGE_TYPE_SMS_CDMA);
        }

        return messageType;
    }

    private void setListOffsetAndMaxCountInHeaderSet(int maxListCount, int listStartOffset) {
        mOap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
        mOap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);

        mOap.addToHeaderSet(mHeaderSet);
    }

    /**
     * Returns {@code null} if {@code readResponse} has not completed or if no
     * phone number was obtained from the Message Listing.
     *
     * Otherwise, returns the remote device's own phone number.
     */
    public String getOwnNumber() {
        return mPhoneNumber;
    }

    public boolean isSearchCompleted() {
        return mSearchCompleted;
    }

    @Override
    public void execute(ClientSession session) throws IOException {
        executeGet(session);
    }

    private static void logD(String message) {
        if (MapClientService.DBG) {
            Log.d(TAG, message);
        }
    }
}
Loading