1/*
2 * Copyright (C) 2016 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
17package android.bluetooth;
18
19import android.app.PendingIntent;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.ServiceConnection;
24import android.net.Uri;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.util.Log;
28
29import java.util.ArrayList;
30import java.util.List;
31
32/**
33 * This class provides the APIs to control the Bluetooth MAP MCE Profile.
34 *
35 * @hide
36 */
37public final class BluetoothMapClient implements BluetoothProfile {
38
39    private static final String TAG = "BluetoothMapClient";
40    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
41    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
42
43    public static final String ACTION_CONNECTION_STATE_CHANGED =
44            "android.bluetooth.mapmce.profile.action.CONNECTION_STATE_CHANGED";
45    public static final String ACTION_MESSAGE_RECEIVED =
46            "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED";
47    /* Actions to be used for pending intents */
48    public static final String ACTION_MESSAGE_SENT_SUCCESSFULLY =
49            "android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY";
50    public static final String ACTION_MESSAGE_DELIVERED_SUCCESSFULLY =
51            "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY";
52
53    /* Extras used in ACTION_MESSAGE_RECEIVED intent.
54     * NOTE: HANDLE is only valid for a single session with the device. */
55    public static final String EXTRA_MESSAGE_HANDLE =
56            "android.bluetooth.mapmce.profile.extra.MESSAGE_HANDLE";
57    public static final String EXTRA_SENDER_CONTACT_URI =
58            "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI";
59    public static final String EXTRA_SENDER_CONTACT_NAME =
60            "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME";
61
62    private volatile IBluetoothMapClient mService;
63    private final Context mContext;
64    private ServiceListener mServiceListener;
65    private BluetoothAdapter mAdapter;
66
67    /** There was an error trying to obtain the state */
68    public static final int STATE_ERROR = -1;
69
70    public static final int RESULT_FAILURE = 0;
71    public static final int RESULT_SUCCESS = 1;
72    /** Connection canceled before completion. */
73    public static final int RESULT_CANCELED = 2;
74
75    final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
76            new IBluetoothStateChangeCallback.Stub() {
77                public void onBluetoothStateChange(boolean up) {
78                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
79                    if (!up) {
80                        if (VDBG) Log.d(TAG, "Unbinding service...");
81                        synchronized (mConnection) {
82                            try {
83                                mService = null;
84                                mContext.unbindService(mConnection);
85                            } catch (Exception re) {
86                                Log.e(TAG, "", re);
87                            }
88                        }
89                    } else {
90                        synchronized (mConnection) {
91                            try {
92                                if (mService == null) {
93                                    if (VDBG) Log.d(TAG, "Binding service...");
94                                    doBind();
95                                }
96                            } catch (Exception re) {
97                                Log.e(TAG, "", re);
98                            }
99                        }
100                    }
101                }
102            };
103
104    /**
105     * Create a BluetoothMapClient proxy object.
106     */
107    /*package*/ BluetoothMapClient(Context context, ServiceListener l) {
108        if (DBG) Log.d(TAG, "Create BluetoothMapClient proxy object");
109        mContext = context;
110        mServiceListener = l;
111        mAdapter = BluetoothAdapter.getDefaultAdapter();
112        IBluetoothManager mgr = mAdapter.getBluetoothManager();
113        if (mgr != null) {
114            try {
115                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
116            } catch (RemoteException e) {
117                Log.e(TAG, "", e);
118            }
119        }
120        doBind();
121    }
122
123    boolean doBind() {
124        Intent intent = new Intent(IBluetoothMapClient.class.getName());
125        ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
126        intent.setComponent(comp);
127        if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
128                android.os.Process.myUserHandle())) {
129            Log.e(TAG, "Could not bind to Bluetooth MAP MCE Service with " + intent);
130            return false;
131        }
132        return true;
133    }
134
135    protected void finalize() throws Throwable {
136        try {
137            close();
138        } finally {
139            super.finalize();
140        }
141    }
142
143    /**
144     * Close the connection to the backing service.
145     * Other public functions of BluetoothMap will return default error
146     * results once close() has been called. Multiple invocations of close()
147     * are ok.
148     */
149    public void close() {
150        IBluetoothManager mgr = mAdapter.getBluetoothManager();
151        if (mgr != null) {
152            try {
153                mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
154            } catch (Exception e) {
155                Log.e(TAG, "", e);
156            }
157        }
158
159        synchronized (mConnection) {
160            if (mService != null) {
161                try {
162                    mService = null;
163                    mContext.unbindService(mConnection);
164                } catch (Exception re) {
165                    Log.e(TAG, "", re);
166                }
167            }
168        }
169        mServiceListener = null;
170    }
171
172    /**
173     * Returns true if the specified Bluetooth device is connected.
174     * Returns false if not connected, or if this proxy object is not
175     * currently connected to the Map service.
176     */
177    public boolean isConnected(BluetoothDevice device) {
178        if (VDBG) Log.d(TAG, "isConnected(" + device + ")");
179        final IBluetoothMapClient service = mService;
180        if (service != null) {
181            try {
182                return service.isConnected(device);
183            } catch (RemoteException e) {
184                Log.e(TAG, e.toString());
185            }
186        } else {
187            Log.w(TAG, "Proxy not attached to service");
188            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
189        }
190        return false;
191    }
192
193    /**
194     * Initiate connection. Initiation of outgoing connections is not
195     * supported for MAP server.
196     */
197    public boolean connect(BluetoothDevice device) {
198        if (DBG) Log.d(TAG, "connect(" + device + ")" + "for MAPS MCE");
199        final IBluetoothMapClient service = mService;
200        if (service != null) {
201            try {
202                return service.connect(device);
203            } catch (RemoteException e) {
204                Log.e(TAG, e.toString());
205            }
206        } else {
207            Log.w(TAG, "Proxy not attached to service");
208            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
209        }
210        return false;
211    }
212
213    /**
214     * Initiate disconnect.
215     *
216     * @param device Remote Bluetooth Device
217     * @return false on error, true otherwise
218     */
219    public boolean disconnect(BluetoothDevice device) {
220        if (DBG) Log.d(TAG, "disconnect(" + device + ")");
221        final IBluetoothMapClient service = mService;
222        if (service != null && isEnabled() && isValidDevice(device)) {
223            try {
224                return service.disconnect(device);
225            } catch (RemoteException e) {
226                Log.e(TAG, Log.getStackTraceString(new Throwable()));
227            }
228        }
229        if (service == null) Log.w(TAG, "Proxy not attached to service");
230        return false;
231    }
232
233    /**
234     * Get the list of connected devices. Currently at most one.
235     *
236     * @return list of connected devices
237     */
238    @Override
239    public List<BluetoothDevice> getConnectedDevices() {
240        if (DBG) Log.d(TAG, "getConnectedDevices()");
241        final IBluetoothMapClient service = mService;
242        if (service != null && isEnabled()) {
243            try {
244                return service.getConnectedDevices();
245            } catch (RemoteException e) {
246                Log.e(TAG, Log.getStackTraceString(new Throwable()));
247                return new ArrayList<>();
248            }
249        }
250        if (service == null) Log.w(TAG, "Proxy not attached to service");
251        return new ArrayList<>();
252    }
253
254    /**
255     * Get the list of devices matching specified states. Currently at most one.
256     *
257     * @return list of matching devices
258     */
259    @Override
260    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
261        if (DBG) Log.d(TAG, "getDevicesMatchingStates()");
262        final IBluetoothMapClient service = mService;
263        if (service != null && isEnabled()) {
264            try {
265                return service.getDevicesMatchingConnectionStates(states);
266            } catch (RemoteException e) {
267                Log.e(TAG, Log.getStackTraceString(new Throwable()));
268                return new ArrayList<>();
269            }
270        }
271        if (service == null) Log.w(TAG, "Proxy not attached to service");
272        return new ArrayList<>();
273    }
274
275    /**
276     * Get connection state of device
277     *
278     * @return device connection state
279     */
280    @Override
281    public int getConnectionState(BluetoothDevice device) {
282        if (DBG) Log.d(TAG, "getConnectionState(" + device + ")");
283        final IBluetoothMapClient service = mService;
284        if (service != null && isEnabled() && isValidDevice(device)) {
285            try {
286                return service.getConnectionState(device);
287            } catch (RemoteException e) {
288                Log.e(TAG, Log.getStackTraceString(new Throwable()));
289                return BluetoothProfile.STATE_DISCONNECTED;
290            }
291        }
292        if (service == null) Log.w(TAG, "Proxy not attached to service");
293        return BluetoothProfile.STATE_DISCONNECTED;
294    }
295
296    /**
297     * Set priority of the profile
298     *
299     * <p> The device should already be paired.  Priority can be one of {@link #PRIORITY_ON} or
300     * {@link #PRIORITY_OFF},
301     *
302     * @param device Paired bluetooth device
303     * @return true if priority is set, false on error
304     */
305    public boolean setPriority(BluetoothDevice device, int priority) {
306        if (DBG) Log.d(TAG, "setPriority(" + device + ", " + priority + ")");
307        final IBluetoothMapClient service = mService;
308        if (service != null && isEnabled() && isValidDevice(device)) {
309            if (priority != BluetoothProfile.PRIORITY_OFF
310                    && priority != BluetoothProfile.PRIORITY_ON) {
311                return false;
312            }
313            try {
314                return service.setPriority(device, priority);
315            } catch (RemoteException e) {
316                Log.e(TAG, Log.getStackTraceString(new Throwable()));
317                return false;
318            }
319        }
320        if (service == null) Log.w(TAG, "Proxy not attached to service");
321        return false;
322    }
323
324    /**
325     * Get the priority of the profile.
326     *
327     * <p> The priority can be any of:
328     * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
329     * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
330     *
331     * @param device Bluetooth device
332     * @return priority of the device
333     */
334    public int getPriority(BluetoothDevice device) {
335        if (VDBG) Log.d(TAG, "getPriority(" + device + ")");
336        final IBluetoothMapClient service = mService;
337        if (service != null && isEnabled() && isValidDevice(device)) {
338            try {
339                return service.getPriority(device);
340            } catch (RemoteException e) {
341                Log.e(TAG, Log.getStackTraceString(new Throwable()));
342                return PRIORITY_OFF;
343            }
344        }
345        if (service == null) Log.w(TAG, "Proxy not attached to service");
346        return PRIORITY_OFF;
347    }
348
349    /**
350     * Send a message.
351     *
352     * Send an SMS message to either the contacts primary number or the telephone number specified.
353     *
354     * @param device          Bluetooth device
355     * @param contacts        Uri[] of the contacts
356     * @param message         Message to be sent
357     * @param sentIntent      intent issued when message is sent
358     * @param deliveredIntent intent issued when message is delivered
359     * @return true if the message is enqueued, false on error
360     */
361    public boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
362            PendingIntent sentIntent, PendingIntent deliveredIntent) {
363        if (DBG) Log.d(TAG, "sendMessage(" + device + ", " + contacts + ", " + message);
364        final IBluetoothMapClient service = mService;
365        if (service != null && isEnabled() && isValidDevice(device)) {
366            try {
367                return service.sendMessage(device, contacts, message, sentIntent, deliveredIntent);
368            } catch (RemoteException e) {
369                Log.e(TAG, Log.getStackTraceString(new Throwable()));
370                return false;
371            }
372        }
373        return false;
374    }
375
376    /**
377     * Get unread messages.  Unread messages will be published via {@link #ACTION_MESSAGE_RECEIVED}.
378     *
379     * @param device Bluetooth device
380     * @return true if the message is enqueued, false on error
381     */
382    public boolean getUnreadMessages(BluetoothDevice device) {
383        if (DBG) Log.d(TAG, "getUnreadMessages(" + device + ")");
384        final IBluetoothMapClient service = mService;
385        if (service != null && isEnabled() && isValidDevice(device)) {
386            try {
387                return service.getUnreadMessages(device);
388            } catch (RemoteException e) {
389                Log.e(TAG, Log.getStackTraceString(new Throwable()));
390                return false;
391            }
392        }
393        return false;
394    }
395
396    private final ServiceConnection mConnection = new ServiceConnection() {
397        public void onServiceConnected(ComponentName className, IBinder service) {
398            if (DBG) Log.d(TAG, "Proxy object connected");
399            mService = IBluetoothMapClient.Stub.asInterface(service);
400            if (mServiceListener != null) {
401                mServiceListener.onServiceConnected(BluetoothProfile.MAP_CLIENT,
402                    BluetoothMapClient.this);
403            }
404        }
405
406        public void onServiceDisconnected(ComponentName className) {
407            if (DBG) Log.d(TAG, "Proxy object disconnected");
408            mService = null;
409            if (mServiceListener != null) {
410                mServiceListener.onServiceDisconnected(BluetoothProfile.MAP_CLIENT);
411            }
412        }
413    };
414
415    private boolean isEnabled() {
416        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
417        if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true;
418        if (DBG) Log.d(TAG, "Bluetooth is Not enabled");
419        return false;
420    }
421
422    private static boolean isValidDevice(BluetoothDevice device) {
423        return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
424    }
425
426}
427