• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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