1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.mapclient; 18 19 import static java.lang.Math.min; 20 21 import android.telephony.PhoneNumberUtils; 22 import android.util.Log; 23 24 import com.android.bluetooth.ObexAppParameters; 25 import com.android.internal.annotations.VisibleForTesting; 26 import com.android.obex.ClientSession; 27 import com.android.obex.HeaderSet; 28 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.List; 34 import java.util.Locale; 35 36 /** 37 * Request to get a listing of messages in directory. Listing is used to determine the remote 38 * device's own phone number. Searching the SENT folder is the most reliable way since there should 39 * only be one Originator (From:), as opposed to the INBOX folder, where there can be multiple 40 * Recipients (To: and Cc:). 41 * 42 * <p>Ideally, only a single message is needed; however, the Originator (From:) field in the listing 43 * is optional (not required by specs). Hence, a geometrically increasing sliding window is used to 44 * request additional message listings until either a number is found or folders have been 45 * exhausted. 46 * 47 * <p>The sliding window is automated (i.e., offset and size, transitions across folders). Simply 48 * use the same {@link RequestGetMessagesListingForOwnNumber} repeatedly with {@link 49 * MasClient#makeRequest}. {@link #isSearchCompleted} indicates when the search is complete, i.e., 50 * the object cannot be used further. 51 */ 52 class RequestGetMessagesListingForOwnNumber extends Request { 53 private static final String TAG = RequestGetMessagesListingForOwnNumber.class.getSimpleName(); 54 55 private static final String TYPE = "x-bt/MAP-msg-listing"; 56 57 // Search for sent messages (MMS or SMS) first. If that fails, search for received SMS. 58 @VisibleForTesting 59 static final List<String> FOLDERS_TO_SEARCH = 60 new ArrayList<>( 61 Arrays.asList(MceStateMachine.FOLDER_SENT, MceStateMachine.FOLDER_INBOX)); 62 63 private static final int MAX_LIST_COUNT_INITIAL = 1; 64 // NOTE: the value is not "final" so that it can be modified in the unit tests 65 @VisibleForTesting static int sMaxListCountUpperLimit = 65535; 66 private static final int LIST_START_OFFSET_INITIAL = 0; 67 // NOTE: the value is not "final" so that it can be modified in the unit tests 68 @VisibleForTesting static int sListStartOffsetUpperLimit = 65535; 69 70 /** 71 * A geometrically increasing sliding window for messages to list. 72 * 73 * <p>E.g., if we don't find the phone number in the 1st message, try the next 2, then the next 74 * 4, then the next 8, etc. 75 */ 76 private static class MessagesSlidingWindow { 77 private int mListStartOffset; 78 private int mMaxListCount; 79 MessagesSlidingWindow()80 MessagesSlidingWindow() { 81 reset(); 82 } 83 84 /** Returns false if start of window exceeds range; o.w. returns true. */ moveWindow()85 public boolean moveWindow() { 86 if (mListStartOffset > sListStartOffsetUpperLimit) { 87 return false; 88 } 89 mListStartOffset = mListStartOffset + mMaxListCount; 90 if (mListStartOffset > sListStartOffsetUpperLimit) { 91 return false; 92 } 93 mMaxListCount = min(2 * mMaxListCount, sMaxListCountUpperLimit); 94 logD( 95 String.format( 96 Locale.US, 97 "MessagesSlidingWindow, moveWindow: startOffset=%d, maxCount=%d", 98 mListStartOffset, 99 mMaxListCount)); 100 return true; 101 } 102 reset()103 public void reset() { 104 mListStartOffset = LIST_START_OFFSET_INITIAL; 105 mMaxListCount = MAX_LIST_COUNT_INITIAL; 106 } 107 getStartOffset()108 public int getStartOffset() { 109 return mListStartOffset; 110 } 111 getMaxCount()112 public int getMaxCount() { 113 return mMaxListCount; 114 } 115 } 116 117 private MessagesSlidingWindow mMessageListingWindow; 118 119 private ObexAppParameters mOap; 120 121 private int mFolderCounter; 122 private boolean mSearchCompleted; 123 private String mPhoneNumber; 124 RequestGetMessagesListingForOwnNumber()125 RequestGetMessagesListingForOwnNumber() { 126 mHeaderSet.setHeader(HeaderSet.TYPE, TYPE); 127 mOap = new ObexAppParameters(); 128 129 mMessageListingWindow = new MessagesSlidingWindow(); 130 131 mFolderCounter = 0; 132 setupCurrentFolderForSearch(); 133 134 mSearchCompleted = false; 135 mPhoneNumber = null; 136 } 137 138 @Override readResponse(InputStream stream)139 protected void readResponse(InputStream stream) { 140 if (mSearchCompleted) { 141 return; 142 } 143 144 MessagesListing response = new MessagesListing(stream); 145 146 if (response == null) { 147 // This shouldn't have happened; move on to the next window 148 logD("readResponse: null Response, moving to next window"); 149 moveToNextWindow(); 150 return; 151 } 152 153 ArrayList<Message> messageListing = response.getList(); 154 if (messageListing == null || messageListing.isEmpty()) { 155 // No more messages in this folder; move on to the next folder; 156 logD("readResponse: no messages, moving to next folder"); 157 moveToNextFolder(); 158 return; 159 } 160 161 // Search through message listing for own phone number. 162 // Message listings by spec arrive ordered newest first. 163 String folderName = FOLDERS_TO_SEARCH.get(mFolderCounter); 164 logD( 165 String.format( 166 Locale.US, 167 "readResponse: Folder=%s, # of msgs=%d, startOffset=%d, maxCount=%d", 168 folderName, 169 messageListing.size(), 170 mMessageListingWindow.getStartOffset(), 171 mMessageListingWindow.getMaxCount())); 172 String number = null; 173 for (int i = 0; i < messageListing.size(); i++) { 174 Message msg = messageListing.get(i); 175 if (MceStateMachine.FOLDER_INBOX.equals(folderName)) { 176 number = PhoneNumberUtils.extractNetworkPortion(msg.getRecipientAddressing()); 177 } else if (MceStateMachine.FOLDER_SENT.equals(folderName)) { 178 number = PhoneNumberUtils.extractNetworkPortion(msg.getSenderAddressing()); 179 } 180 if (number != null && !number.trim().isEmpty()) { 181 // Search is completed when a phone number is found 182 mPhoneNumber = number; 183 mSearchCompleted = true; 184 logD(String.format("readResponse: phone number found = %s", mPhoneNumber)); 185 return; 186 } 187 } 188 189 // If a number hasn't been found, move on to the next window. 190 if (!mSearchCompleted) { 191 logD("readResponse: number hasn't been found, moving to next window"); 192 moveToNextWindow(); 193 } 194 } 195 196 /** 197 * Move on to next folder to start searching (sliding window). 198 * 199 * <p>Overall search for own-phone-number is completed when we run out of folders to search. 200 */ moveToNextFolder()201 private void moveToNextFolder() { 202 if (mFolderCounter < FOLDERS_TO_SEARCH.size() - 1) { 203 mFolderCounter += 1; 204 setupCurrentFolderForSearch(); 205 } else { 206 logD("moveToNextFolder: folders exhausted, search complete"); 207 mSearchCompleted = true; 208 } 209 } 210 211 /** 212 * Tries sliding the window in the current folder. - If successful (didn't exceed range), update 213 * the headers to reflect new window's offset and size. - If fails (exceeded range), move on to 214 * the next folder. 215 */ moveToNextWindow()216 private void moveToNextWindow() { 217 if (mMessageListingWindow.moveWindow()) { 218 setListOffsetAndMaxCountInHeaderSet( 219 mMessageListingWindow.getMaxCount(), mMessageListingWindow.getStartOffset()); 220 } else { 221 // Can't slide window anymore, exceeded range; move on to next folder 222 logD("moveToNextWindow: can't slide window anymore, folder complete"); 223 moveToNextFolder(); 224 } 225 } 226 227 /** 228 * Set up the current folder for searching: 1. Updates headers to reflect new folder name. 2. 229 * Resets the sliding window. 3. Updates headers to reflect new window's offset and size. 230 */ setupCurrentFolderForSearch()231 private void setupCurrentFolderForSearch() { 232 String folderName = FOLDERS_TO_SEARCH.get(mFolderCounter); 233 mHeaderSet.setHeader(HeaderSet.NAME, folderName); 234 235 byte filter = messageTypeBasedOnFolder(folderName); 236 mOap.add(OAP_TAGID_FILTER_MESSAGE_TYPE, filter); 237 mOap.addToHeaderSet(mHeaderSet); 238 239 mMessageListingWindow.reset(); 240 int maxCount = mMessageListingWindow.getMaxCount(); 241 int offset = mMessageListingWindow.getStartOffset(); 242 setListOffsetAndMaxCountInHeaderSet(maxCount, offset); 243 logD( 244 String.format( 245 Locale.US, 246 "setupCurrentFolderForSearch: folder=%s, filter=%d, offset=%d, maxCount=%d", 247 folderName, 248 filter, 249 maxCount, 250 offset)); 251 } 252 messageTypeBasedOnFolder(String folderName)253 private byte messageTypeBasedOnFolder(String folderName) { 254 byte messageType = 255 (byte) 256 (MessagesFilter.MESSAGE_TYPE_SMS_GSM 257 | MessagesFilter.MESSAGE_TYPE_SMS_CDMA 258 | MessagesFilter.MESSAGE_TYPE_MMS); 259 260 // If trying to grab own number from messages received by the remote device, 261 // only use SMS messages since SMS will only have one recipient (the remote device), 262 // whereas MMS may have more than one recipient (e.g., group MMS or if the originator 263 // is also CC-ed as a recipient). Even if there is only one recipient presented to 264 // Bluetooth in a group MMS, it may not necessarily correspond to the remote device; 265 // there is no specification governing the `To:` and `Cc:` fields in the MMS specs. 266 if (MceStateMachine.FOLDER_INBOX.equals(folderName)) { 267 messageType = 268 (byte) 269 (MessagesFilter.MESSAGE_TYPE_SMS_GSM 270 | MessagesFilter.MESSAGE_TYPE_SMS_CDMA); 271 } 272 273 return messageType; 274 } 275 setListOffsetAndMaxCountInHeaderSet(int maxListCount, int listStartOffset)276 private void setListOffsetAndMaxCountInHeaderSet(int maxListCount, int listStartOffset) { 277 mOap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount); 278 mOap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset); 279 280 mOap.addToHeaderSet(mHeaderSet); 281 } 282 283 /** 284 * Returns {@code null} if {@code readResponse} has not completed or if no phone number was 285 * obtained from the Message Listing. 286 * 287 * <p>Otherwise, returns the remote device's own phone number. 288 */ getOwnNumber()289 public String getOwnNumber() { 290 return mPhoneNumber; 291 } 292 isSearchCompleted()293 public boolean isSearchCompleted() { 294 return mSearchCompleted; 295 } 296 297 @Override execute(ClientSession session)298 public void execute(ClientSession session) throws IOException { 299 executeGet(session); 300 } 301 logD(String message)302 private static void logD(String message) { 303 Log.d(TAG, message); 304 } 305 } 306