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