/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Bluetooth MAP MCE StateMachine (Disconnected) | ^ CONNECT | | DISCONNECTED V | (Connecting)
* (Disconnecting) | ^ CONNECTED | | DISCONNECT V | (Connected)
*
*
Valid Transitions: State + Event -> Transition:
*
*
Disconnected + CONNECT -> Connecting Connecting + CONNECTED -> Connected Connecting + TIMEOUT
* -> Disconnecting Connecting + DISCONNECT/CONNECT -> Defer Message Connected + DISCONNECT ->
* Disconnecting Connected + CONNECT -> Disconnecting + Defer Message Disconnecting + DISCONNECTED
* -> (Safe) Disconnected Disconnecting + TIMEOUT -> (Force) Disconnected Disconnecting +
* DISCONNECT/CONNECT : Defer Message
*/
package com.android.bluetooth.mapclient;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.RECEIVE_SMS;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.SdpMasRecord;
import android.content.Intent;
import android.net.Uri;
import android.os.Looper;
import android.os.Message;
import android.os.SystemProperties;
import android.provider.Telephony;
import android.telecom.PhoneAccount;
import android.telephony.SmsManager;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.map.BluetoothMapbMessageMime;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.vcard.VCardConstants;
import com.android.vcard.VCardEntry;
import com.android.vcard.VCardProperty;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/* The MceStateMachine is responsible for setting up and maintaining a connection to a single
* specific Messaging Server Equipment endpoint. Upon connect command an SDP record is retrieved,
* a connection to the Message Access Server is created and a request to enable notification of new
* messages is sent.
*/
class MceStateMachine extends StateMachine {
private static final String TAG = MceStateMachine.class.getSimpleName();
// Messages for events handled by the StateMachine
static final int MSG_MAS_CONNECTED = 1001;
static final int MSG_MAS_DISCONNECTED = 1002;
static final int MSG_MAS_REQUEST_COMPLETED = 1003;
static final int MSG_MAS_REQUEST_FAILED = 1004;
static final int MSG_MAS_SDP_DONE = 1005;
static final int MSG_MAS_SDP_UNSUCCESSFUL = 1006;
static final int MSG_OUTBOUND_MESSAGE = 2001;
static final int MSG_INBOUND_MESSAGE = 2002;
static final int MSG_NOTIFICATION = 2003;
static final int MSG_GET_LISTING = 2004;
static final int MSG_GET_MESSAGE_LISTING = 2005;
// Set message status to read or deleted
static final int MSG_SET_MESSAGE_STATUS = 2006;
static final int MSG_SEARCH_OWN_NUMBER_TIMEOUT = 2007;
// SAVE_OUTBOUND_MESSAGES defaults to true to place the responsibility of managing content on
// Bluetooth, to work with the default Car Messenger. This may need to be set to false if the
// messaging app takes that responsibility.
private static final Boolean SAVE_OUTBOUND_MESSAGES = true;
private static final int DISCONNECT_TIMEOUT = 3000;
private static final int CONNECT_TIMEOUT = 10000;
private static final int MAX_MESSAGES = 20;
private static final int MSG_CONNECT = 1;
private static final int MSG_DISCONNECT = 2;
static final int MSG_CONNECTING_TIMEOUT = 3;
private static final int MSG_DISCONNECTING_TIMEOUT = 4;
// Constants for SDP. Note that these values come from the native stack, but no centralized
// constants exist for them as part of the various SDP APIs.
public static final int SDP_SUCCESS = 0;
public static final int SDP_FAILED = 1;
public static final int SDP_BUSY = 2;
private static final boolean MESSAGE_SEEN = true;
private static final boolean MESSAGE_NOT_SEEN = false;
// Folder names as defined in Bluetooth.org MAP spec V10
private static final String FOLDER_TELECOM = "telecom";
private static final String FOLDER_MSG = "msg";
private static final String FOLDER_OUTBOX = "outbox";
static final String FOLDER_INBOX = "inbox";
static final String FOLDER_SENT = "sent";
private static final String INBOX_PATH = "telecom/msg/inbox";
// URI Scheme for messages with email contact
private static final String SCHEME_MAILTO = "mailto";
private static final String FETCH_MESSAGE_TYPE =
"persist.bluetooth.pts.mapclient.fetchmessagetype";
private static final String SEND_MESSAGE_TYPE =
"persist.bluetooth.pts.mapclient.sendmessagetype";
// Connectivity States
private int mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
private State mDisconnected;
private State mConnecting;
private State mConnected;
private State mDisconnecting;
private final BluetoothDevice mDevice;
private MapClientService mService;
private MasClient mMasClient;
private MapClientContent mDatabase;
private HashMap mSentMessageLog = new HashMap<>(MAX_MESSAGES);
private HashMap mSentReceiptRequested = new HashMap<>(MAX_MESSAGES);
private HashMap mDeliveryReceiptRequested =
new HashMap<>(MAX_MESSAGES);
private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA;
// The amount of time for MCE to search for remote device's own phone number before:
// (1) MCE registering itself for being notified of the arrival of new messages; and
// (2) MCE start downloading existing messages off of MSE.
// NOTE: the value is not "final" so that it can be modified in the unit tests
@VisibleForTesting static int sOwnNumberSearchTimeoutMs = 3_000;
/**
* An object to hold the necessary meta-data for each message so we can broadcast it alongside
* the message content.
*
* This is necessary because the metadata is inferred or received separately from the actual
* message content.
*
*
Note: In the future it may be best to use the entries from the MessageListing in full
* instead of this small subset.
*/
@VisibleForTesting
static class MessageMetadata {
private final String mHandle;
private final Long mTimestamp;
private boolean mRead;
private boolean mSeen;
MessageMetadata(String handle, Long timestamp, boolean read, boolean seen) {
mHandle = handle;
mTimestamp = timestamp;
mRead = read;
mSeen = seen;
}
public String getHandle() {
return mHandle;
}
public Long getTimestamp() {
return mTimestamp;
}
public synchronized boolean getRead() {
return mRead;
}
public synchronized void setRead(boolean read) {
mRead = read;
}
public synchronized boolean getSeen() {
return mSeen;
}
}
// Map each message to its metadata via the handle
@VisibleForTesting
ConcurrentHashMap mMessages =
new ConcurrentHashMap();
MceStateMachine(MapClientService service, BluetoothDevice device) {
this(service, device, null, null);
}
MceStateMachine(MapClientService service, BluetoothDevice device, Looper looper) {
this(service, device, null, null, looper);
}
@VisibleForTesting
MceStateMachine(
MapClientService service,
BluetoothDevice device,
MasClient masClient,
MapClientContent database) {
super(TAG);
mService = service;
mMasClient = masClient;
mDevice = device;
mDatabase = database;
initStateMachine();
}
@VisibleForTesting
MceStateMachine(
MapClientService service,
BluetoothDevice device,
MasClient masClient,
MapClientContent database,
Looper looper) {
super(TAG, looper);
mService = service;
mMasClient = masClient;
mDevice = device;
mDatabase = database;
initStateMachine();
}
private void initStateMachine() {
mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
mDisconnected = new Disconnected();
mConnecting = new Connecting();
mDisconnecting = new Disconnecting();
mConnected = new Connected();
addState(mDisconnected);
addState(mConnecting);
addState(mDisconnecting);
addState(mConnected);
setInitialState(mConnecting);
start();
}
public void doQuit() {
quitNow();
}
@Override
protected void onQuitting() {
if (mService != null) {
mService.cleanupDevice(mDevice, this);
}
}
synchronized BluetoothDevice getDevice() {
return mDevice;
}
private void onConnectionStateChanged(int prevState, int state) {
if (mMostRecentState == state) {
return;
}
// mDevice == null only at setInitialState
if (mDevice == null) {
return;
}
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ ": Connection state changed, prev="
+ prevState
+ ", new="
+ state);
if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.MAP_CLIENT);
}
setState(state);
AdapterService adapterService = AdapterService.getAdapterService();
if (adapterService != null) {
adapterService.updateProfileConnectionAdapterProperties(
mDevice, BluetoothProfile.MAP_CLIENT, state, prevState);
}
Intent intent = new Intent(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mService.sendBroadcastMultiplePermissions(
intent,
new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
Utils.getTempBroadcastOptions());
}
private synchronized void setState(int state) {
mMostRecentState = state;
}
public synchronized int getState() {
return mMostRecentState;
}
/** Notify of SDP completion. */
public void sendSdpResult(int status, SdpMasRecord record) {
Log.d(TAG, "Received SDP Result, status=" + status + ", record=" + record);
if (status != SDP_SUCCESS || record == null) {
Log.w(TAG, "SDP unsuccessful, status: " + status + ", record: " + record);
sendMessage(MceStateMachine.MSG_MAS_SDP_UNSUCCESSFUL, status);
return;
}
sendMessage(MceStateMachine.MSG_MAS_SDP_DONE, record);
}
public boolean disconnect() {
Log.d(TAG, "Disconnect Request " + mDevice);
sendMessage(MSG_DISCONNECT, mDevice);
return true;
}
public synchronized boolean sendMapMessage(
Uri[] contacts,
String message,
PendingIntent sentIntent,
PendingIntent deliveredIntent) {
Log.d(TAG, Utils.getLoggableAddress(mDevice) + ": Send, message=" + message);
if (contacts == null || contacts.length <= 0) {
return false;
}
if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
Bmessage bmsg = new Bmessage();
// Set type and status.
bmsg.setType(getDefaultMessageType());
bmsg.setStatus(Bmessage.Status.READ);
for (Uri contact : contacts) {
// Who to send the message to.
Log.v(TAG, "Scheme " + contact.getScheme());
if (PhoneAccount.SCHEME_TEL.equals(contact.getScheme())) {
String path = contact.getPath();
if (path != null && path.contains(Telephony.Threads.CONTENT_URI.toString())) {
mDatabase.addThreadContactsToEntries(bmsg, contact.getLastPathSegment());
} else {
VCardEntry destEntry = new VCardEntry();
VCardProperty destEntryPhone = new VCardProperty();
destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
destEntryPhone.addValues(contact.getSchemeSpecificPart());
destEntry.addProperty(destEntryPhone);
bmsg.addRecipient(destEntry);
Log.v(TAG, "Sending to phone numbers " + destEntryPhone.getValueList());
}
} else if (SCHEME_MAILTO.equals(contact.getScheme())) {
VCardEntry destEntry = new VCardEntry();
VCardProperty destEntryContact = new VCardProperty();
destEntryContact.setName(VCardConstants.PROPERTY_EMAIL);
destEntryContact.addValues(contact.getSchemeSpecificPart());
destEntry.addProperty(destEntryContact);
bmsg.addRecipient(destEntry);
Log.d(TAG, "SPECIFIC: " + contact.getSchemeSpecificPart());
Log.d(TAG, "Sending to emails " + destEntryContact.getValueList());
} else {
Log.w(TAG, "Scheme " + contact.getScheme() + " not supported.");
return false;
}
}
// Message of the body.
bmsg.setBodyContent(message);
if (sentIntent != null) {
mSentReceiptRequested.put(bmsg, sentIntent);
}
if (deliveredIntent != null) {
mDeliveryReceiptRequested.put(bmsg, deliveredIntent);
}
sendMessage(MSG_OUTBOUND_MESSAGE, bmsg);
return true;
}
return false;
}
synchronized boolean getMessage(String handle) {
Log.d(TAG, "getMessage" + handle);
if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
sendMessage(MSG_INBOUND_MESSAGE, handle);
return true;
}
return false;
}
synchronized boolean getUnreadMessages() {
Log.d(TAG, "getMessage");
if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
return true;
}
return false;
}
synchronized int getSupportedFeatures() {
if (mMostRecentState == BluetoothProfile.STATE_CONNECTED && mMasClient != null) {
Log.d(TAG, "returning getSupportedFeatures from SDP record");
return mMasClient.getSdpMasRecord().getSupportedFeatures();
}
Log.d(TAG, "getSupportedFeatures: no connection, returning 0");
return 0;
}
synchronized boolean setMessageStatus(String handle, int status) {
Log.d(TAG, "setMessageStatus(" + handle + ", " + status + ")");
if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
RequestSetMessageStatus.StatusIndicator statusIndicator;
byte value;
switch (status) {
case BluetoothMapClient.UNREAD:
statusIndicator = RequestSetMessageStatus.StatusIndicator.READ;
value = RequestSetMessageStatus.STATUS_NO;
break;
case BluetoothMapClient.READ:
statusIndicator = RequestSetMessageStatus.StatusIndicator.READ;
value = RequestSetMessageStatus.STATUS_YES;
break;
case BluetoothMapClient.UNDELETED:
statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED;
value = RequestSetMessageStatus.STATUS_NO;
break;
case BluetoothMapClient.DELETED:
statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED;
value = RequestSetMessageStatus.STATUS_YES;
break;
default:
Log.e(TAG, "Invalid parameter for status" + status);
return false;
}
sendMessage(
MSG_SET_MESSAGE_STATUS,
0,
0,
new RequestSetMessageStatus(handle, statusIndicator, value));
return true;
}
return false;
}
private String getContactURIFromPhone(String number) {
return PhoneAccount.SCHEME_TEL + ":" + number;
}
private String getContactURIFromEmail(String email) {
return SCHEME_MAILTO + "://" + email;
}
Bmessage.Type getDefaultMessageType() {
synchronized (mDefaultMessageType) {
if (Utils.isPtsTestMode()) {
int messageType = SystemProperties.getInt(SEND_MESSAGE_TYPE, -1);
if (messageType > 0 && messageType < Bmessage.Type.values().length) {
return Bmessage.Type.values()[messageType];
}
}
return mDefaultMessageType;
}
}
void setDefaultMessageType(SdpMasRecord sdpMasRecord) {
int supportedMessageTypes = sdpMasRecord.getSupportedMessageTypes();
synchronized (mDefaultMessageType) {
if ((supportedMessageTypes & SdpMasRecord.MessageType.MMS) > 0) {
mDefaultMessageType = Bmessage.Type.MMS;
} else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_CDMA) > 0) {
mDefaultMessageType = Bmessage.Type.SMS_CDMA;
} else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_GSM) > 0) {
mDefaultMessageType = Bmessage.Type.SMS_GSM;
}
}
}
public void dump(StringBuilder sb) {
ProfileService.println(
sb,
"mCurrentDevice: "
+ mDevice
+ "("
+ Utils.getName(mDevice)
+ ") "
+ this.toString());
if (mDatabase != null) {
mDatabase.dump(sb);
} else {
ProfileService.println(sb, " Device Message DB: null");
}
sb.append("\n");
}
class Disconnected extends State {
@Override
public void enter() {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Disconnected]: Entered, message="
+ getMessageName(getCurrentMessage().what));
onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTED);
mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
quit();
}
@Override
public void exit() {
mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
}
}
class Connecting extends State {
@Override
public void enter() {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: Entered, message="
+ getMessageName(getCurrentMessage().what));
onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING);
// When commanded to connect begin SDP to find the MAS server.
mDevice.sdpSearch(BluetoothUuid.MAS);
sendMessageDelayed(MSG_CONNECTING_TIMEOUT, CONNECT_TIMEOUT);
Log.i(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: Await SDP results");
}
@Override
public boolean processMessage(Message message) {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: Received "
+ getMessageName(message.what));
switch (message.what) {
case MSG_MAS_SDP_DONE:
Log.i(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: SDP Complete");
if (mMasClient == null) {
SdpMasRecord record = (SdpMasRecord) message.obj;
if (record == null) {
Log.e(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: SDP record is null");
return NOT_HANDLED;
}
mMasClient = new MasClient(mDevice, MceStateMachine.this, record);
setDefaultMessageType(record);
}
break;
case MSG_MAS_SDP_UNSUCCESSFUL:
int sdpStatus = message.arg1;
Log.i(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: SDP unsuccessful, status="
+ sdpStatus);
if (sdpStatus == SDP_BUSY) {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: SDP was busy, try again");
mDevice.sdpSearch(BluetoothUuid.MAS);
} else {
// This means the status is 0 (success, but no record) or 1 (organic
// failure). We historically have never retried SDP in failure cases, so we
// don't need to wait for the timeout anymore.
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: SDP failed completely, disconnecting");
transitionTo(mDisconnecting);
}
break;
case MSG_MAS_CONNECTED:
transitionTo(mConnected);
break;
case MSG_MAS_DISCONNECTED:
if (mMasClient != null) {
mMasClient.shutdown();
}
transitionTo(mDisconnected);
break;
case MSG_CONNECTING_TIMEOUT:
transitionTo(mDisconnecting);
break;
case MSG_CONNECT:
case MSG_DISCONNECT:
deferMessage(message);
break;
default:
Log.w(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connecting]: Unexpected message: "
+ getMessageName(message.what));
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void exit() {
mPreviousState = BluetoothProfile.STATE_CONNECTING;
removeMessages(MSG_CONNECTING_TIMEOUT);
}
}
class Connected extends State {
@Override
public void enter() {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Entered, message="
+ getMessageName(getCurrentMessage().what));
MapClientContent.Callbacks callbacks =
new MapClientContent.Callbacks() {
@Override
public void onMessageStatusChanged(String handle, int status) {
setMessageStatus(handle, status);
}
};
// Keeps mock database from being overwritten in tests
if (mDatabase == null) {
mDatabase = new MapClientContent(mService, callbacks, mDevice);
}
onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTED);
if (Utils.isPtsTestMode()) return;
mMasClient.makeRequest(new RequestSetPath(FOLDER_TELECOM));
mMasClient.makeRequest(new RequestSetPath(FOLDER_MSG));
mMasClient.makeRequest(new RequestSetPath(FOLDER_INBOX));
mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
mMasClient.makeRequest(new RequestSetPath(false));
// Start searching for remote device's own phone number. Only until either:
// (a) the search completes (with or without finding the number), or
// (b) the timeout expires,
// does the MCE:
// (a) register itself for being notified of the arrival of new messages, and
// (b) start downloading existing messages off of MSE.
// In other words, the MCE shouldn't handle any messages (new or existing) until after
// it has tried obtaining the remote's own phone number.
RequestGetMessagesListingForOwnNumber requestForOwnNumber =
new RequestGetMessagesListingForOwnNumber();
mMasClient.makeRequest(requestForOwnNumber);
sendMessageDelayed(
MSG_SEARCH_OWN_NUMBER_TIMEOUT, requestForOwnNumber, sOwnNumberSearchTimeoutMs);
Log.i(TAG, Utils.getLoggableAddress(mDevice) + "[Connected]: Find phone number");
}
@Override
public boolean processMessage(Message message) {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Received "
+ getMessageName(message.what));
switch (message.what) {
case MSG_DISCONNECT:
if (mDevice.equals(message.obj)) {
transitionTo(mDisconnecting);
}
break;
case MSG_MAS_DISCONNECTED:
deferMessage(message);
transitionTo(mDisconnecting);
break;
case MSG_OUTBOUND_MESSAGE:
mMasClient.makeRequest(
new RequestPushMessage(
FOLDER_OUTBOX, (Bmessage) message.obj, null, false, false));
break;
case MSG_INBOUND_MESSAGE:
mMasClient.makeRequest(
new RequestGetMessage(
(String) message.obj, MasClient.CharsetType.UTF_8, false));
break;
case MSG_NOTIFICATION:
EventReport notification = (EventReport) message.obj;
processNotification(notification);
break;
case MSG_GET_LISTING:
mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
break;
case MSG_GET_MESSAGE_LISTING:
// Get the 50 most recent messages from the last week
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -7);
byte messageType;
if (Utils.isPtsTestMode()) {
messageType =
(byte)
SystemProperties.getInt(
FETCH_MESSAGE_TYPE,
MessagesFilter.MESSAGE_TYPE_ALL);
} else {
messageType = MessagesFilter.MESSAGE_TYPE_ALL;
}
mMasClient.makeRequest(
new RequestGetMessagesListing(
(String) message.obj,
0,
new MessagesFilter.Builder()
.setPeriod(calendar.getTime(), null)
.setMessageType(messageType)
.build(),
0,
50,
0));
break;
case MSG_SET_MESSAGE_STATUS:
if (message.obj instanceof RequestSetMessageStatus) {
mMasClient.makeRequest((RequestSetMessageStatus) message.obj);
}
break;
case MSG_MAS_REQUEST_COMPLETED:
if (message.obj instanceof RequestGetMessage) {
processInboundMessage((RequestGetMessage) message.obj);
} else if (message.obj instanceof RequestPushMessage) {
RequestPushMessage requestPushMessage = (RequestPushMessage) message.obj;
String messageHandle = requestPushMessage.getMsgHandle();
Log.i(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Message Sent, handle="
+ messageHandle);
// ignore the top-order byte (converted to string) in the handle for now
// some test devices don't populate messageHandle field.
// in such cases, no need to wait up for response for such messages.
if (messageHandle != null && messageHandle.length() > 2) {
if (SAVE_OUTBOUND_MESSAGES) {
mDatabase.storeMessage(
requestPushMessage.getBMsg(),
messageHandle,
System.currentTimeMillis(),
MESSAGE_SEEN);
}
mSentMessageLog.put(
messageHandle.substring(2), requestPushMessage.getBMsg());
}
} else if (message.obj instanceof RequestGetMessagesListing) {
processMessageListing((RequestGetMessagesListing) message.obj);
} else if (message.obj instanceof RequestSetMessageStatus) {
processSetMessageStatus((RequestSetMessageStatus) message.obj);
} else if (message.obj instanceof RequestGetMessagesListingForOwnNumber) {
processMessageListingForOwnNumber(
(RequestGetMessagesListingForOwnNumber) message.obj);
}
break;
case MSG_CONNECT:
if (!mDevice.equals(message.obj)) {
deferMessage(message);
transitionTo(mDisconnecting);
}
break;
case MSG_SEARCH_OWN_NUMBER_TIMEOUT:
Log.w(TAG, "Timeout while searching for own phone number.");
// Abort any outstanding Request so it doesn't execute on MasClient
RequestGetMessagesListingForOwnNumber request =
(RequestGetMessagesListingForOwnNumber) message.obj;
mMasClient.abortRequest(request);
// Remove any executed/completed Request that MasClient has passed back to
// state machine. Note: {@link StateMachine} doesn't provide a {@code
// removeMessages(int what, Object obj)}, nor direct access to {@link
// mSmHandler}, so this will remove *all* {@code MSG_MAS_REQUEST_COMPLETED}
// messages. However, {@link RequestGetMessagesListingForOwnNumber} should be
// the only MAS Request enqueued at this point, since none of the other MAS
// Requests should trigger/start until after getOwnNumber has completed.
removeMessages(MSG_MAS_REQUEST_COMPLETED);
// If failed to complete search for remote device's own phone number,
// proceed without it (i.e., register MCE for MNS and start download
// of existing messages from MSE).
notificationRegistrationAndStartDownloadMessages();
break;
default:
Log.w(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Unexpected message: "
+ getMessageName(message.what));
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void exit() {
mDatabase.cleanUp();
mDatabase = null;
mPreviousState = BluetoothProfile.STATE_CONNECTED;
}
/**
* Given a message notification event, will ensure message caching and updating and update
* interested applications.
*
* Message notifications arrive for both remote message reception and Message-Listing
* object updates that are triggered by the server side.
*
* @param event - object describing the remote event
*/
private void processNotification(EventReport event) {
Log.i(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Received Notification, event="
+ event);
if (event == null) {
Log.w(
TAG,
Utils.getLoggableAddress(mDevice)
+ "[Connected]: Notification event is null");
return;
}
switch (event.getType()) {
case NEW_MESSAGE:
if (!mMessages.containsKey(event.getHandle())) {
Long timestamp = event.getTimestamp();
if (timestamp == null) {
// Infer the timestamp for this message as 'now' and read status
// false instead of getting the message listing data for it
timestamp = Instant.now().toEpochMilli();
}
MessageMetadata metadata =
new MessageMetadata(
event.getHandle(), timestamp, false, MESSAGE_NOT_SEEN);
mMessages.put(event.getHandle(), metadata);
}
mMasClient.makeRequest(
new RequestGetMessage(
event.getHandle(), MasClient.CharsetType.UTF_8, false));
break;
case DELIVERY_FAILURE:
// fall through
case SENDING_FAILURE:
if (!Flags.handleDeliverySendingFailureEvents()) {
break;
}
// fall through
case DELIVERY_SUCCESS:
// fall through
case SENDING_SUCCESS:
notifySentMessageStatus(event.getHandle(), event.getType());
break;
case READ_STATUS_CHANGED:
mDatabase.markRead(event.getHandle());
break;
case MESSAGE_DELETED:
mDatabase.deleteMessage(event.getHandle());
break;
default:
Log.d(TAG, "processNotification: ignoring event type=" + event.getType());
}
}
/**
* Given the result of a Message Listing request, will cache the contents of each Message in
* the Message Listing Object and kick off requests to retrieve message contents from the
* remote device.
*
* @param request - A request object that has been resolved and returned with a message list
*/
private void processMessageListing(RequestGetMessagesListing request) {
Log.i(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: Received Message Listing, listing="
+ (request != null
? (request.getList() != null
? String.valueOf(request.getList().size())
: "null list")
: "null request"));
ArrayList messageListing = request.getList();
if (messageListing != null) {
// Message listings by spec arrive ordered newest first but we wish to broadcast as
// oldest first. Iterate in reverse order so we initiate requests oldest first.
for (int i = messageListing.size() - 1; i >= 0; i--) {
com.android.bluetooth.mapclient.Message msg = messageListing.get(i);
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Connected]: fetch message content, handle="
+ msg.getHandle());
// A message listing coming from the server should always have up to date data
if (msg.getDateTime() == null) {
Log.w(
TAG,
"message with handle "
+ msg.getHandle()
+ " has a null datetime, ignoring");
continue;
}
mMessages.put(
msg.getHandle(),
new MessageMetadata(
msg.getHandle(),
msg.getDateTime().getTime(),
msg.isRead(),
MESSAGE_SEEN));
getMessage(msg.getHandle());
}
}
}
/**
* Process the result of a MessageListing request that was made specifically to obtain the
* remote device's own phone number.
*
* @param request - A request object that has been resolved and returned with: - a phone
* number (possibly null if a number wasn't found) - a flag indicating whether there are
* still messages that can be searched/requested. - the request will automatically
* update itself if a number wasn't found and there are still messages that can be
* searched.
*/
private void processMessageListingForOwnNumber(
RequestGetMessagesListingForOwnNumber request) {
if (request.isSearchCompleted()) {
Log.d(TAG, "processMessageListingForOwnNumber: search completed");
if (request.getOwnNumber() != null) {
// A phone number was found (should be the remote device's).
Log.d(
TAG,
"processMessageListingForOwnNumber: number found = "
+ request.getOwnNumber());
mDatabase.setRemoteDeviceOwnNumber(request.getOwnNumber());
}
// Remove any outstanding timeouts from state machine queue
removeDeferredMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
removeMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
// Move on to next stage of connection process
notificationRegistrationAndStartDownloadMessages();
} else {
// A phone number wasn't found, but there are still additional messages that can
// be requested and searched.
Log.d(TAG, "processMessageListingForOwnNumber: continuing search");
mMasClient.makeRequest(request);
}
}
/**
* (1) MCE registering itself for being notified of the arrival of new messages; and (2) MCE
* downloading existing messages of off MSE.
*/
private void notificationRegistrationAndStartDownloadMessages() {
Log.i(TAG, Utils.getLoggableAddress(mDevice) + "[Connected]: Queue Message downloads");
mMasClient.makeRequest(new RequestSetNotificationRegistration(true));
sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT);
sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
}
private void processSetMessageStatus(RequestSetMessageStatus request) {
Log.d(TAG, "processSetMessageStatus");
int result = BluetoothMapClient.RESULT_SUCCESS;
if (!request.isSuccess()) {
Log.e(TAG, "Set message status failed");
result = BluetoothMapClient.RESULT_FAILURE;
}
RequestSetMessageStatus.StatusIndicator status = request.getStatusIndicator();
switch (status) {
case READ:
{
Intent intent =
new Intent(BluetoothMapClient.ACTION_MESSAGE_READ_STATUS_CHANGED);
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS,
request.getValue() == RequestSetMessageStatus.STATUS_YES
? true
: false);
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result);
mService.sendBroadcast(intent, BLUETOOTH_CONNECT);
break;
}
case DELETED:
{
Intent intent =
new Intent(
BluetoothMapClient.ACTION_MESSAGE_DELETED_STATUS_CHANGED);
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_DELETED_STATUS,
request.getValue() == RequestSetMessageStatus.STATUS_YES
? true
: false);
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result);
mService.sendBroadcast(intent, BLUETOOTH_CONNECT);
break;
}
default:
Log.e(TAG, "Unknown status indicator " + status);
return;
}
}
/**
* Given the response of a GetMessage request, will broadcast the bMessage contents on to
* all registered applications.
*
* Inbound messages arrive as bMessage objects following a GetMessage request. GetMessage
* uses a message handle that can arrive from both a GetMessageListing request or a Message
* Notification event.
*
* @param request - A request object that has been resolved and returned with message data
*/
private void processInboundMessage(RequestGetMessage request) {
Bmessage message = request.getMessage();
Log.d(TAG, "Notify inbound Message" + message);
if (message == null) {
return;
}
mDatabase.storeMessage(
message,
request.getHandle(),
mMessages.get(request.getHandle()).getTimestamp(),
mMessages.get(request.getHandle()).getSeen());
if (!INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
Log.d(TAG, "Ignoring message received in " + message.getFolder() + ".");
return;
}
switch (message.getType()) {
case SMS_CDMA:
case SMS_GSM:
case MMS:
Log.d(TAG, "Body: " + message.getBodyContent());
Log.d(TAG, message.toString());
Log.d(TAG, "Recipients" + message.getRecipients().toString());
// Grab the message metadata and update the cached read status from the bMessage
MessageMetadata metadata = mMessages.get(request.getHandle());
metadata.setRead(request.getMessage().getStatus() == Bmessage.Status.READ);
Intent intent = new Intent();
intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP, metadata.getTimestamp());
intent.putExtra(
BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS, metadata.getRead());
intent.putExtra(android.content.Intent.EXTRA_TEXT, message.getBodyContent());
VCardEntry originator = message.getOriginator();
if (originator != null) {
Log.d(TAG, originator.toString());
List phoneData = originator.getPhoneList();
List emailData = originator.getEmailList();
if (phoneData != null && phoneData.size() > 0) {
String phoneNumber = phoneData.get(0).getNumber();
Log.d(TAG, "Originator number: " + phoneNumber);
intent.putExtra(
BluetoothMapClient.EXTRA_SENDER_CONTACT_URI,
getContactURIFromPhone(phoneNumber));
} else if (emailData != null && emailData.size() > 0) {
String email = emailData.get(0).getAddress();
Log.d(TAG, "Originator email: " + email);
intent.putExtra(
BluetoothMapClient.EXTRA_SENDER_CONTACT_URI,
getContactURIFromEmail(email));
}
intent.putExtra(
BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME,
originator.getDisplayName());
}
if (message.getType() == Bmessage.Type.MMS) {
BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
mmsBmessage.parseMsgPart(message.getBodyContent());
intent.putExtra(
android.content.Intent.EXTRA_TEXT, mmsBmessage.getMessageAsText());
ArrayList recipients = message.getRecipients();
if (recipients != null && !recipients.isEmpty()) {
intent.putExtra(
android.content.Intent.EXTRA_CC, getRecipientsUri(recipients));
}
}
String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService);
if (defaultMessagingPackage == null) {
// Broadcast to all RECEIVE_SMS recipients, including the SMS receiver
// package defined in system properties if one exists
mService.sendBroadcast(intent, RECEIVE_SMS);
} else {
String smsReceiverPackageName =
SystemProperties.get(
"bluetooth.profile.map_client.sms_receiver_package", null);
if (smsReceiverPackageName != null && !smsReceiverPackageName.isEmpty()) {
// Clone intent and broadcast to SMS receiver package if one exists
Intent messageNotificationIntent = (Intent) intent.clone();
messageNotificationIntent.setPackage(smsReceiverPackageName);
mService.sendBroadcast(messageNotificationIntent, RECEIVE_SMS);
}
// Broadcast to default messaging package
intent.setPackage(defaultMessagingPackage);
mService.sendBroadcast(intent, RECEIVE_SMS);
}
break;
case EMAIL:
default:
Log.e(TAG, "Received unhandled type" + message.getType().toString());
break;
}
}
/**
* Retrieves the URIs of all the participants of a group conversation, besides the sender of
* the message.
*/
private String[] getRecipientsUri(ArrayList recipients) {
Set uris = new HashSet<>();
for (VCardEntry recipient : recipients) {
List phoneData = recipient.getPhoneList();
if (phoneData != null && phoneData.size() > 0) {
String phoneNumber = phoneData.get(0).getNumber();
Log.d(TAG, "CC Recipient number: " + phoneNumber);
uris.add(getContactURIFromPhone(phoneNumber));
}
}
String[] stringUris = new String[uris.size()];
return uris.toArray(stringUris);
}
private void notifySentMessageStatus(String handle, EventReport.Type status) {
Log.d(TAG, "got a status for " + handle + " Status = " + status);
// some test devices don't populate messageHandle field.
// in such cases, ignore such messages.
if (handle == null || handle.length() <= 2) return;
PendingIntent intentToSend = null;
// ignore the top-order byte (converted to string) in the handle for now
String shortHandle = handle.substring(2);
if (status == EventReport.Type.SENDING_FAILURE
|| status == EventReport.Type.SENDING_SUCCESS) {
intentToSend = mSentReceiptRequested.remove(mSentMessageLog.get(shortHandle));
} else if (status == EventReport.Type.DELIVERY_SUCCESS
|| status == EventReport.Type.DELIVERY_FAILURE) {
intentToSend = mDeliveryReceiptRequested.remove(mSentMessageLog.get(shortHandle));
}
if (intentToSend != null) {
try {
Log.d(TAG, "*******Sending " + intentToSend);
int result = Activity.RESULT_OK;
if (status == EventReport.Type.SENDING_FAILURE
|| status == EventReport.Type.DELIVERY_FAILURE) {
result = SmsManager.RESULT_ERROR_GENERIC_FAILURE;
}
intentToSend.send(result);
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Notification Request Canceled" + e);
}
} else {
Log.e(
TAG,
"Received a notification on message with handle = "
+ handle
+ ", but it is NOT found in mSentMessageLog! where did it go?");
}
}
}
class Disconnecting extends State {
@Override
public void enter() {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Disconnecting]: Entered, message="
+ getMessageName(getCurrentMessage().what));
onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTING);
if (mMasClient != null) {
mMasClient.makeRequest(new RequestSetNotificationRegistration(false));
mMasClient.shutdown();
sendMessageDelayed(MSG_DISCONNECTING_TIMEOUT, DISCONNECT_TIMEOUT);
} else {
// MAP was never connected
transitionTo(mDisconnected);
}
}
@Override
public boolean processMessage(Message message) {
Log.d(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Disconnecting]: Received "
+ getMessageName(message.what));
switch (message.what) {
case MSG_DISCONNECTING_TIMEOUT:
case MSG_MAS_DISCONNECTED:
mMasClient = null;
transitionTo(mDisconnected);
break;
case MSG_CONNECT:
case MSG_DISCONNECT:
deferMessage(message);
break;
default:
Log.w(
TAG,
Utils.getLoggableAddress(mDevice)
+ " [Disconnecting]: Unexpected message: "
+ getMessageName(message.what));
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void exit() {
mPreviousState = BluetoothProfile.STATE_DISCONNECTING;
removeMessages(MSG_DISCONNECTING_TIMEOUT);
}
}
void receiveEvent(EventReport ev) {
Log.d(TAG, "Message Type = " + ev.getType() + ", Message handle = " + ev.getHandle());
sendMessage(MSG_NOTIFICATION, ev);
}
private String getMessageName(int what) {
switch (what) {
case MSG_MAS_CONNECTED:
return "MSG_MAS_CONNECTED";
case MSG_MAS_DISCONNECTED:
return "MSG_MAS_DISCONNECTED";
case MSG_MAS_REQUEST_COMPLETED:
return "MSG_MAS_REQUEST_COMPLETED";
case MSG_MAS_REQUEST_FAILED:
return "MSG_MAS_REQUEST_FAILED";
case MSG_MAS_SDP_DONE:
return "MSG_MAS_SDP_DONE";
case MSG_MAS_SDP_UNSUCCESSFUL:
return "MSG_MAS_SDP_UNSUCCESSFUL";
case MSG_OUTBOUND_MESSAGE:
return "MSG_OUTBOUND_MESSAGE";
case MSG_INBOUND_MESSAGE:
return "MSG_INBOUND_MESSAGE";
case MSG_NOTIFICATION:
return "MSG_NOTIFICATION";
case MSG_GET_LISTING:
return "MSG_GET_LISTING";
case MSG_GET_MESSAGE_LISTING:
return "MSG_GET_MESSAGE_LISTING";
case MSG_SET_MESSAGE_STATUS:
return "MSG_SET_MESSAGE_STATUS";
case DISCONNECT_TIMEOUT:
return "DISCONNECT_TIMEOUT";
case CONNECT_TIMEOUT:
return "CONNECT_TIMEOUT";
case MSG_CONNECT:
return "MSG_CONNECT";
case MSG_DISCONNECT:
return "MSG_DISCONNECT";
case MSG_CONNECTING_TIMEOUT:
return "MSG_CONNECTING_TIMEOUT";
case MSG_DISCONNECTING_TIMEOUT:
return "MSG_DISCONNECTING_TIMEOUT";
}
return "UNKNOWN";
}
}