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