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