| /* |
| * 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<Long, Msg> mMsgListSms = null; |
| private Map<Long, Msg> mMsgListMms = null; |
| private Map<Long, Msg> mMsgListMsg = null; |
| |
| private Map<String, BluetoothMapConvoContactElement> mContactList; |
| |
| private HashMap<Long, BluetoothMapConvoListingElement> mSmsMmsConvoList = |
| new HashMap<Long, BluetoothMapConvoListingElement>(); |
| |
| private HashMap<Long, BluetoothMapConvoListingElement> mImEmailConvoList = |
| new HashMap<Long, BluetoothMapConvoListingElement>(); |
| |
| 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<Long, Msg> getMsgListSms() { |
| return mMsgListSms; |
| } |
| |
| /* package */ void setMsgListSms(Map<Long, Msg> msgListSms) { |
| mMsgListSms = msgListSms; |
| } |
| |
| /* package */ Map<Long, Msg> getMsgListMms() { |
| return mMsgListMms; |
| } |
| |
| /* package */ void setMsgListMms(Map<Long, Msg> msgListMms) { |
| mMsgListMms = msgListMms; |
| } |
| |
| /* package */ Map<Long, Msg> getMsgListMsg() { |
| return mMsgListMsg; |
| } |
| |
| /* package */ void setMsgListMsg(Map<Long, Msg> msgListMsg) { |
| mMsgListMsg = msgListMsg; |
| } |
| |
| /* package */ Map<String, BluetoothMapConvoContactElement> getContactList() { |
| return mContactList; |
| } |
| |
| /* package */ void setContactList(Map<String, BluetoothMapConvoContactElement> contactList) { |
| mContactList = contactList; |
| } |
| |
| HashMap<Long, BluetoothMapConvoListingElement> getSmsMmsConvoList() { |
| return mSmsMmsConvoList; |
| } |
| |
| void setSmsMmsConvoList(HashMap<Long, BluetoothMapConvoListingElement> smsMmsConvoList) { |
| mSmsMmsConvoList = smsMmsConvoList; |
| } |
| |
| HashMap<Long, BluetoothMapConvoListingElement> getImEmailConvoList() { |
| return mImEmailConvoList; |
| } |
| |
| void setImEmailConvoList(HashMap<Long, BluetoothMapConvoListingElement> 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(); |
| } |
| } |
| |
| } |