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

Commit ad52d3ae authored by Andrew Cheng's avatar Andrew Cheng Committed by Automerger Merge Worker
Browse files

Merge "Obtain remote device's own phonenumber from MessagesListing" am: eb71c44c

parents a56cfc0d eb71c44c
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