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