1/*
2* Copyright (C) 2014 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15package com.android.bluetooth.map;
16
17import java.io.IOException;
18
19import javax.obex.ServerSession;
20
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.BluetoothServerSocket;
24import android.bluetooth.BluetoothSocket;
25import android.bluetooth.BluetoothUuid;
26import android.content.Context;
27import android.content.Intent;
28import android.os.Handler;
29import android.os.RemoteException;
30import android.util.Log;
31
32public class BluetoothMapMasInstance {
33    private static final String TAG = "BluetoothMapMasInstance";
34
35    private static final boolean D = BluetoothMapService.DEBUG;
36    private static final boolean V = BluetoothMapService.VERBOSE;
37
38    private static final int SDP_MAP_MSG_TYPE_EMAIL    = 0x01;
39    private static final int SDP_MAP_MSG_TYPE_SMS_GSM  = 0x02;
40    private static final int SDP_MAP_MSG_TYPE_SMS_CDMA = 0x04;
41    private static final int SDP_MAP_MSG_TYPE_MMS      = 0x08;
42
43    private SocketAcceptThread mAcceptThread = null;
44
45    private ServerSession mServerSession = null;
46
47    // The handle to the socket registration with SDP
48    private BluetoothServerSocket mServerSocket = null;
49
50    // The actual incoming connection handle
51    private BluetoothSocket mConnSocket = null;
52
53    private BluetoothDevice mRemoteDevice = null; // The remote connected device
54
55    private BluetoothAdapter mAdapter;
56
57    private volatile boolean mInterrupted;              // Used to interrupt socket accept thread
58
59    private Handler mServiceHandler = null;             // MAP service message handler
60    private BluetoothMapService mMapService = null;     // Handle to the outer MAP service
61    private Context mContext = null;                    // MAP service context
62    private BluetoothMnsObexClient mMnsClient = null;   // Shared MAP MNS client
63    private BluetoothMapEmailSettingsItem mAccount = null; //
64    private String mBaseEmailUri = null;                // Email client base URI for this instance
65    private int mMasInstanceId = -1;
66    private boolean mEnableSmsMms = false;
67    BluetoothMapContentObserver mObserver;
68
69    /**
70     * Create a e-mail MAS instance
71     * @param callback
72     * @param context
73     * @param mns
74     * @param emailBaseUri - use null to create a SMS/MMS MAS instance
75     */
76    public BluetoothMapMasInstance (BluetoothMapService mapService,
77            Context context,
78            BluetoothMapEmailSettingsItem account,
79            int masId,
80            boolean enableSmsMms) {
81        mMapService = mapService;
82        mServiceHandler = mapService.getHandler();
83        mContext = context;
84        mAccount = account;
85        if(account != null) {
86            mBaseEmailUri = account.mBase_uri;
87        }
88        mMasInstanceId = masId;
89        mEnableSmsMms = enableSmsMms;
90        init();
91    }
92
93    @Override
94    public String toString() {
95        return "MasId: " + mMasInstanceId + " Uri:" + mBaseEmailUri + " SMS/MMS:" + mEnableSmsMms;
96    }
97
98    private void init() {
99        mAdapter = BluetoothAdapter.getDefaultAdapter();
100    }
101
102    public int getMasId() {
103        return mMasInstanceId;
104    }
105
106    /**
107     * A thread that runs in the background waiting for remote rfcomm
108     * connect. Once a remote socket connected, this thread shall be
109     * shutdown. When the remote disconnect, this thread shall run again
110     * waiting for next request.
111     */
112    private class SocketAcceptThread extends Thread {
113
114        private boolean stopped = false;
115
116        @Override
117        public void run() {
118            BluetoothServerSocket serverSocket;
119            if (mServerSocket == null) {
120                if (!initSocket()) {
121                    return;
122                }
123            }
124
125            while (!stopped) {
126                try {
127                    if (D) Log.d(TAG, "Accepting socket connection...");
128                    serverSocket = mServerSocket;
129                    if(serverSocket == null) {
130                        Log.w(TAG, "mServerSocket is null");
131                        break;
132                    }
133                    mConnSocket = serverSocket.accept();
134                    if (D) Log.d(TAG, "Accepted socket connection...");
135
136                    synchronized (BluetoothMapMasInstance.this) {
137                        if (mConnSocket == null) {
138                            Log.w(TAG, "mConnSocket is null");
139                            break;
140                        }
141                        mRemoteDevice = mConnSocket.getRemoteDevice();
142                    }
143
144                    if (mRemoteDevice == null) {
145                        Log.i(TAG, "getRemoteDevice() = null");
146                        break;
147                    }
148
149                    /* Signal to the service that we have received an incoming connection.
150                     */
151                    boolean isValid = mMapService.onConnect(mRemoteDevice, BluetoothMapMasInstance.this);
152
153                    if(isValid == false) {
154                        // Close connection if we already have a connection with another device
155                        Log.i(TAG, "RemoteDevice is invalid - closing.");
156                        mConnSocket.close();
157                        mConnSocket = null;
158                        // now wait for a new connect
159                    } else {
160                        stopped = true; // job done ,close this thread;
161                    }
162                } catch (IOException ex) {
163                    stopped=true;
164                    if (D) Log.v(TAG, "Accept exception: (expected at shutdown)", ex);
165                }
166            }
167        }
168
169        void shutdown() {
170            stopped = true;
171            if(mServerSocket != null) {
172                try {
173                    mServerSocket.close();
174                } catch (IOException e) {
175                    if(D) Log.d(TAG, "Exception while thread shurdown:", e);
176                } finally {
177                    mServerSocket = null;
178                }
179            }
180            interrupt();
181        }
182    }
183
184    public void startRfcommSocketListener() {
185        if (D) Log.d(TAG, "Map Service startRfcommSocketListener");
186        mInterrupted = false; /* For this to work all calls to this function
187                                 and shutdown() must be from same thread. */
188        if (mAcceptThread == null) {
189            mAcceptThread = new SocketAcceptThread();
190            mAcceptThread.setName("BluetoothMapAcceptThread masId=" + mMasInstanceId);
191            mAcceptThread.start();
192        }
193    }
194
195    private final boolean initSocket() {
196        if (D) Log.d(TAG, "MAS initSocket()");
197
198        boolean initSocketOK = false;
199        final int CREATE_RETRY_TIME = 10;
200
201        // It's possible that create will fail in some cases. retry for 10 times
202        for (int i = 0; (i < CREATE_RETRY_TIME) && !mInterrupted; i++) {
203            initSocketOK = true;
204            try {
205                // It is mandatory for MSE to support initiation of bonding and
206                // encryption.
207                String masId = String.format("%02x", mMasInstanceId & 0xff);
208                String masName = "";
209                int messageTypeFlags = 0;
210                if(mEnableSmsMms) {
211                    masName = "SMS/MMS";
212                    messageTypeFlags |= SDP_MAP_MSG_TYPE_SMS_GSM |
213                                   SDP_MAP_MSG_TYPE_SMS_CDMA|
214                                   SDP_MAP_MSG_TYPE_MMS;
215                }
216                if(mBaseEmailUri != null) {
217                    if(mEnableSmsMms) {
218                        masName += "/EMAIL";
219                    } else {
220                        masName = mAccount.getName();
221                    }
222                    messageTypeFlags |= SDP_MAP_MSG_TYPE_EMAIL;
223                }
224                String msgTypes = String.format("%02x", messageTypeFlags & 0xff);
225                String sdpString = masId + msgTypes + masName;
226                if(V) Log.d(TAG, "  masId = " + masId +
227                                 "\n  msgTypes = " + msgTypes +
228                                 "\n  masName = " + masName +
229                                 "\n  SDP string = " + sdpString);
230                mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord
231                    (sdpString, BluetoothUuid.MAS.getUuid());
232
233            } catch (IOException e) {
234                Log.e(TAG, "Error create RfcommServerSocket " + e.toString());
235                initSocketOK = false;
236            }
237            if (!initSocketOK) {
238                // Need to break out of this loop if BT is being turned off.
239                if (mAdapter == null) break;
240                int state = mAdapter.getState();
241                if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
242                    (state != BluetoothAdapter.STATE_ON)) {
243                    Log.w(TAG, "initServerSocket failed as BT is (being) turned off");
244                    break;
245                }
246                try {
247                    if (V) Log.v(TAG, "waiting 300 ms...");
248                    Thread.sleep(300);
249                } catch (InterruptedException e) {
250                    Log.e(TAG, "socketAcceptThread thread was interrupted (3)");
251                }
252            } else {
253                break;
254            }
255        }
256        if (mInterrupted) {
257            initSocketOK = false;
258            // close server socket to avoid resource leakage
259            closeServerSocket();
260        }
261
262        if (initSocketOK) {
263            if (V) Log.v(TAG, "Succeed to create listening socket ");
264
265        } else {
266            Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
267        }
268        return initSocketOK;
269    }
270
271    /* Called for all MAS instances for each instance when auth. is completed, hence
272     * must check if it has a valid connection before creating a session.
273     * Returns true at success. */
274    public boolean startObexServerSession(BluetoothMnsObexClient mnsClient) throws IOException, RemoteException {
275        if (D) Log.d(TAG, "Map Service startObexServerSession masid = " + mMasInstanceId);
276
277        if (mConnSocket != null) {
278            if(mServerSession != null) {
279                // Already connected, just return true
280                return true;
281            }
282            mMnsClient = mnsClient;
283            BluetoothMapObexServer mapServer;
284            mObserver = new  BluetoothMapContentObserver(mContext,
285                                                         mMnsClient,
286                                                         this,
287                                                         mAccount,
288                                                         mEnableSmsMms);
289            mObserver.init();
290            mapServer = new BluetoothMapObexServer(mServiceHandler,
291                                                    mContext,
292                                                    mObserver,
293                                                    mMasInstanceId,
294                                                    mAccount,
295                                                    mEnableSmsMms);
296            // setup RFCOMM transport
297            BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket);
298            mServerSession = new ServerSession(transport, mapServer, null);
299            if (D) Log.d(TAG, "    ServerSession started.");
300
301            return true;
302        }
303        if (D) Log.d(TAG, "    No connection for this instance");
304        return false;
305    }
306
307    public boolean handleSmsSendIntent(Context context, Intent intent){
308        if(mObserver != null) {
309            return mObserver.handleSmsSendIntent(context, intent);
310        }
311        return false;
312    }
313
314    /**
315     * Check if this instance is started.
316     * @return true if started
317     */
318    public boolean isStarted() {
319        return (mConnSocket != null);
320    }
321
322    public void shutdown() {
323        if (D) Log.d(TAG, "MAP Service shutdown");
324
325        if (mServerSession != null) {
326            mServerSession.close();
327            mServerSession = null;
328        }
329        if (mObserver != null) {
330            mObserver.deinit();
331            mObserver = null;
332        }
333        mInterrupted = true;
334        if(mAcceptThread != null) {
335            mAcceptThread.shutdown();
336            try {
337                mAcceptThread.join();
338            } catch (InterruptedException e) {/* Not much we can do about this*/}
339            mAcceptThread = null;
340        }
341
342        closeConnectionSocket();
343    }
344
345    /**
346     * Stop a running server session or cleanup, and start a new
347     * RFComm socket listener thread.
348     */
349    public void restartObexServerSession() {
350        if (D) Log.d(TAG, "MAP Service stopObexServerSession");
351
352        shutdown();
353
354        // Last obex transaction is finished, we start to listen for incoming
355        // connection again -
356        startRfcommSocketListener();
357    }
358
359
360    private final synchronized void closeServerSocket() {
361        // exit SocketAcceptThread early
362        if (mServerSocket != null) {
363            try {
364                // this will cause mServerSocket.accept() return early with IOException
365                mServerSocket.close();
366            } catch (IOException ex) {
367                Log.e(TAG, "Close Server Socket error: " + ex);
368            } finally {
369                mServerSocket = null;
370            }
371        }
372    }
373
374    private final synchronized void closeConnectionSocket() {
375        if (mConnSocket != null) {
376            try {
377                mConnSocket.close();
378            } catch (IOException e) {
379                Log.e(TAG, "Close Connection Socket error: " + e.toString());
380            } finally {
381                mConnSocket = null;
382            }
383        }
384    }
385
386}
387