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