/* * Copyright (C) 2014 Samsung System LSI * 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. */ package com.android.bluetooth.map; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.RemoteException; import android.os.SystemProperties; import android.util.Log; import com.android.bluetooth.BluetoothObexTransport; import com.android.bluetooth.IObexConnectionHandler; import com.android.bluetooth.ObexServerSockets; import com.android.bluetooth.map.BluetoothMapContentObserver.Msg; import com.android.bluetooth.map.BluetoothMapUtils.TYPE; import com.android.bluetooth.sdp.SdpManager; import java.io.IOException; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import javax.obex.ServerSession; public class BluetoothMapMasInstance implements IObexConnectionHandler { private final String mTag; private static volatile int sInstanceCounter = 0; private static final boolean D = BluetoothMapService.DEBUG; private static final boolean V = BluetoothMapService.VERBOSE; private static final int SDP_MAP_MSG_TYPE_EMAIL = 0x01; private static final int SDP_MAP_MSG_TYPE_SMS_GSM = 0x02; private static final int SDP_MAP_MSG_TYPE_SMS_CDMA = 0x04; private static final int SDP_MAP_MSG_TYPE_MMS = 0x08; private static final int SDP_MAP_MSG_TYPE_IM = 0x10; private static final String BLUETOOTH_MAP_VERSION_PROPERTY = "persist.bluetooth.mapversion"; private static final int SDP_MAP_MAS_VERSION_1_2 = 0x0102; private static final int SDP_MAP_MAS_VERSION_1_3 = 0x0103; private static final int SDP_MAP_MAS_VERSION_1_4 = 0x0104; /* TODO: Should these be adaptive for each MAS? - e.g. read from app? */ static final int SDP_MAP_MAS_FEATURES_1_2 = 0x0000007F; static final int SDP_MAP_MAS_FEATURES_1_3 = 0x000603FF; static final int SDP_MAP_MAS_FEATURES_1_4 = 0x000603FF; private ServerSession mServerSession = null; // The handle to the socket registration with SDP private ObexServerSockets mServerSockets = null; private int mSdpHandle = -1; // The actual incoming connection handle private BluetoothSocket mConnSocket = null; // The remote connected device private BluetoothDevice mRemoteDevice = null; private BluetoothAdapter mAdapter; private volatile boolean mInterrupted; // Used to interrupt socket accept thread private volatile boolean mShutdown = false; // Used to interrupt socket accept thread private volatile boolean mAcceptNewConnections = false; private Handler mServiceHandler = null; // MAP service message handler private BluetoothMapService mMapService = null; // Handle to the outer MAP service private Context mContext = null; // MAP service context private BluetoothMnsObexClient mMnsClient = null; // Shared MAP MNS client private BluetoothMapAccountItem mAccount = null; // private String mBaseUri = null; // Client base URI for this instance private int mMasInstanceId = -1; private boolean mEnableSmsMms = false; BluetoothMapContentObserver mObserver; private BluetoothMapObexServer mMapServer; private AtomicLong mDbIndetifier = new AtomicLong(); private AtomicLong mFolderVersionCounter = new AtomicLong(0); private AtomicLong mSmsMmsConvoListVersionCounter = new AtomicLong(0); private AtomicLong mImEmailConvoListVersionCounter = new AtomicLong(0); private Map mMsgListSms = null; private Map mMsgListMms = null; private Map mMsgListMsg = null; private Map mContactList; private HashMap mSmsMmsConvoList = new HashMap(); private HashMap mImEmailConvoList = new HashMap(); private int mRemoteFeatureMask = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK; private static int sFeatureMask = SDP_MAP_MAS_FEATURES_1_2; public static final String TYPE_SMS_MMS_STR = "SMS/MMS"; public static final String TYPE_EMAIL_STR = "EMAIL"; public static final String TYPE_IM_STR = "IM"; /** * Create a e-mail MAS instance * @param callback * @param context * @param mns * @param emailBaseUri - use null to create a SMS/MMS MAS instance */ public BluetoothMapMasInstance(BluetoothMapService mapService, Context context, BluetoothMapAccountItem account, int masId, boolean enableSmsMms) { mTag = "BluetoothMapMasInstance" + sInstanceCounter++; mMapService = mapService; mServiceHandler = mapService.getHandler(); mContext = context; mAccount = account; if (account != null) { mBaseUri = account.mBase_uri; } mMasInstanceId = masId; mEnableSmsMms = enableSmsMms; init(); } private void removeSdpRecord() { if (mAdapter != null && mSdpHandle >= 0 && SdpManager.getDefaultManager() != null) { if (V) { Log.d(mTag, "Removing SDP record for MAS instance: " + mMasInstanceId + " Object reference: " + this + "SDP handle: " + mSdpHandle); } boolean status = SdpManager.getDefaultManager().removeSdpRecord(mSdpHandle); Log.d(mTag, "RemoveSDPrecord returns " + status); mSdpHandle = -1; } } /* Needed only for test */ protected BluetoothMapMasInstance() { mTag = "BluetoothMapMasInstance" + sInstanceCounter++; } @Override public String toString() { return "MasId: " + mMasInstanceId + " Uri:" + mBaseUri + " SMS/MMS:" + mEnableSmsMms; } private void init() { mAdapter = BluetoothAdapter.getDefaultAdapter(); } /** * The data base identifier is used by connecting MCE devices to evaluate if cached data * is still valid, hence only update this value when something actually invalidates the data. * Situations where this must be called: * - MAS ID's vs. server channels are scrambled (As neither MAS ID, name or server channels) * can be used by a client to uniquely identify a specific message database - except MAS id 0 * we should change this value if the server channel is changed. * - If a MAS instance folderVersionCounter roles over - will not happen before a long * is too small to hold a unix time-stamp, hence is not handled. */ private void updateDbIdentifier() { mDbIndetifier.set(Calendar.getInstance().getTime().getTime()); } /** * update the time stamp used for FOLDER version counter. * Call once when a content provider notification caused applicable changes to the * list of messages. */ /* package */ void updateFolderVersionCounter() { mFolderVersionCounter.incrementAndGet(); } /** * update the CONVO LIST version counter. * Call once when a content provider notification caused applicable changes to the * list of contacts, or when an update is manually triggered. */ /* package */ void updateSmsMmsConvoListVersionCounter() { mSmsMmsConvoListVersionCounter.incrementAndGet(); } /* package */ void updateImEmailConvoListVersionCounter() { mImEmailConvoListVersionCounter.incrementAndGet(); } /* package */ Map getMsgListSms() { return mMsgListSms; } /* package */ void setMsgListSms(Map msgListSms) { mMsgListSms = msgListSms; } /* package */ Map getMsgListMms() { return mMsgListMms; } /* package */ void setMsgListMms(Map msgListMms) { mMsgListMms = msgListMms; } /* package */ Map getMsgListMsg() { return mMsgListMsg; } /* package */ void setMsgListMsg(Map msgListMsg) { mMsgListMsg = msgListMsg; } /* package */ Map getContactList() { return mContactList; } /* package */ void setContactList(Map contactList) { mContactList = contactList; } HashMap getSmsMmsConvoList() { return mSmsMmsConvoList; } void setSmsMmsConvoList(HashMap smsMmsConvoList) { mSmsMmsConvoList = smsMmsConvoList; } HashMap getImEmailConvoList() { return mImEmailConvoList; } void setImEmailConvoList(HashMap imEmailConvoList) { mImEmailConvoList = imEmailConvoList; } /* package*/ int getMasId() { return mMasInstanceId; } /* package*/ long getDbIdentifier() { return mDbIndetifier.get(); } /* package*/ long getFolderVersionCounter() { return mFolderVersionCounter.get(); } /* package */ long getCombinedConvoListVersionCounter() { long combinedVersionCounter = mSmsMmsConvoListVersionCounter.get(); combinedVersionCounter += mImEmailConvoListVersionCounter.get(); return combinedVersionCounter; } /** * Start Obex Server Sockets and create the SDP record. */ public synchronized void startSocketListeners() { if (D) { Log.d(mTag, "Map Service startSocketListeners"); } if (mServerSession != null) { if (D) { Log.d(mTag, "mServerSession exists - shutting it down..."); } mServerSession.close(); mServerSession = null; } if (mObserver != null) { if (D) { Log.d(mTag, "mObserver exists - shutting it down..."); } mObserver.deinit(); mObserver = null; } closeConnectionSocket(); if (mServerSockets != null) { mAcceptNewConnections = true; } else { mServerSockets = ObexServerSockets.create(this); mAcceptNewConnections = true; if (mServerSockets == null) { // TODO: Handle - was not handled before Log.e(mTag, "Failed to start the listeners"); return; } removeSdpRecord(); mSdpHandle = createMasSdpRecord(mServerSockets.getRfcommChannel(), mServerSockets.getL2capPsm()); // Here we might have changed crucial data, hence reset DB identifier if (V) { Log.d(mTag, "Creating new SDP record for MAS instance: " + mMasInstanceId + " Object reference: " + this + "SDP handle: " + mSdpHandle); } updateDbIdentifier(); } } /** * Create the MAS SDP record with the information stored in the instance. * @param rfcommChannel the rfcomm channel ID * @param l2capPsm the l2capPsm - set to -1 to exclude */ private int createMasSdpRecord(int rfcommChannel, int l2capPsm) { String masName = ""; int messageTypeFlags = 0; if (mEnableSmsMms) { masName = TYPE_SMS_MMS_STR; messageTypeFlags |= SDP_MAP_MSG_TYPE_SMS_GSM | SDP_MAP_MSG_TYPE_SMS_CDMA | SDP_MAP_MSG_TYPE_MMS; } if (mBaseUri != null) { if (mEnableSmsMms) { if (mAccount.getType() == TYPE.EMAIL) { masName += "/" + TYPE_EMAIL_STR; } else if (mAccount.getType() == TYPE.IM) { masName += "/" + TYPE_IM_STR; } } else { masName = mAccount.getName(); } if (mAccount.getType() == TYPE.EMAIL) { messageTypeFlags |= SDP_MAP_MSG_TYPE_EMAIL; } else if (mAccount.getType() == TYPE.IM) { messageTypeFlags |= SDP_MAP_MSG_TYPE_IM; } } final String currentValue = SystemProperties.get(BLUETOOTH_MAP_VERSION_PROPERTY, "map12"); int masVersion; switch (currentValue) { case "map12": masVersion = SDP_MAP_MAS_VERSION_1_2; sFeatureMask = SDP_MAP_MAS_FEATURES_1_2; break; case "map13": masVersion = SDP_MAP_MAS_VERSION_1_3; sFeatureMask = SDP_MAP_MAS_FEATURES_1_3; break; case "map14": masVersion = SDP_MAP_MAS_VERSION_1_4; sFeatureMask = SDP_MAP_MAS_FEATURES_1_4; break; default: masVersion = SDP_MAP_MAS_VERSION_1_2; sFeatureMask = SDP_MAP_MAS_FEATURES_1_2; } return SdpManager.getDefaultManager() .createMapMasRecord(masName, mMasInstanceId, rfcommChannel, l2capPsm, masVersion, messageTypeFlags, sFeatureMask); } /* Called for all MAS instances for each instance when auth. is completed, hence * must check if it has a valid connection before creating a session. * Returns true at success. */ public boolean startObexServerSession(BluetoothMnsObexClient mnsClient) throws IOException, RemoteException { if (D) { Log.d(mTag, "Map Service startObexServerSession masid = " + mMasInstanceId); } if (mConnSocket != null) { if (mServerSession != null) { // Already connected, just return true return true; } mMnsClient = mnsClient; mObserver = new BluetoothMapContentObserver(mContext, mMnsClient, this, mAccount, mEnableSmsMms); mObserver.init(); mMapServer = new BluetoothMapObexServer(mServiceHandler, mContext, mObserver, this, mAccount, mEnableSmsMms); mMapServer.setRemoteFeatureMask(mRemoteFeatureMask); // setup transport BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket); mServerSession = new ServerSession(transport, mMapServer, null); if (D) { Log.d(mTag, " ServerSession started."); } return true; } if (D) { Log.d(mTag, " No connection for this instance"); } return false; } public boolean handleSmsSendIntent(Context context, Intent intent) { if (mObserver != null) { return mObserver.handleSmsSendIntent(context, intent); } return false; } /** * Check if this instance is started. * @return true if started */ public boolean isStarted() { return (mConnSocket != null); } public void shutdown() { if (D) { Log.d(mTag, "MAP Service shutdown"); } if (mServerSession != null) { mServerSession.close(); mServerSession = null; } if (mObserver != null) { mObserver.deinit(); mObserver = null; } removeSdpRecord(); closeConnectionSocket(); // Do not block for Accept thread cleanup. // Fix Handler Thread block during BT Turn OFF. closeServerSockets(false); } /** * Signal to the ServerSockets handler that a new connection may be accepted. */ public void restartObexServerSession() { if (D) { Log.d(mTag, "MAP Service restartObexServerSession()"); } startSocketListeners(); } private synchronized void closeServerSockets(boolean block) { // exit SocketAcceptThread early ObexServerSockets sockets = mServerSockets; if (sockets != null) { sockets.shutdown(block); mServerSockets = null; } } private synchronized void closeConnectionSocket() { if (mConnSocket != null) { try { mConnSocket.close(); } catch (IOException e) { Log.e(mTag, "Close Connection Socket error: ", e); } finally { mConnSocket = null; } } } public void setRemoteFeatureMask(int supportedFeatures) { if (V) { Log.v(mTag, "setRemoteFeatureMask : Curr: " + mRemoteFeatureMask); } mRemoteFeatureMask = supportedFeatures & sFeatureMask; BluetoothMapUtils.savePeerSupportUtcTimeStamp(mRemoteFeatureMask); if (mObserver != null) { mObserver.setObserverRemoteFeatureMask(mRemoteFeatureMask); mMapServer.setRemoteFeatureMask(mRemoteFeatureMask); if (V) { Log.v(mTag, "setRemoteFeatureMask : set: " + mRemoteFeatureMask); } } } public int getRemoteFeatureMask() { return this.mRemoteFeatureMask; } public static int getFeatureMask() { return sFeatureMask; } @Override public synchronized boolean onConnect(BluetoothDevice device, BluetoothSocket socket) { if (!mAcceptNewConnections) { return false; } /* Signal to the service that we have received an incoming connection. */ boolean isValid = mMapService.onConnect(device, BluetoothMapMasInstance.this); if (isValid) { mRemoteDevice = device; mConnSocket = socket; mAcceptNewConnections = false; } return isValid; } /** * Called when an unrecoverable error occurred in an accept thread. * Close down the server socket, and restart. * TODO: Change to message, to call start in correct context. */ @Override public synchronized void onAcceptFailed() { mServerSockets = null; // Will cause a new to be created when calling start. if (mShutdown) { Log.e(mTag, "Failed to accept incomming connection - " + "shutdown"); } else { Log.e(mTag, "Failed to accept incomming connection - " + "restarting"); startSocketListeners(); } } }