• 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 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