1/*
2 * Copyright (C) 2011 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 com.android.settings.bluetooth;
18
19import android.bluetooth.BluetoothA2dp;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothHeadset;
22import android.bluetooth.BluetoothInputDevice;
23import android.bluetooth.BluetoothPan;
24import android.bluetooth.BluetoothProfile;
25import android.bluetooth.BluetoothUuid;
26import android.content.Context;
27import android.content.Intent;
28import android.os.ParcelUuid;
29import android.util.Log;
30
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.HashMap;
34import java.util.Map;
35
36/**
37 * LocalBluetoothProfileManager provides access to the LocalBluetoothProfile
38 * objects for the available Bluetooth profiles.
39 */
40final class LocalBluetoothProfileManager {
41    private static final String TAG = "LocalBluetoothProfileManager";
42
43    /** Singleton instance. */
44    private static LocalBluetoothProfileManager sInstance;
45
46    /**
47     * An interface for notifying BluetoothHeadset IPC clients when they have
48     * been connected to the BluetoothHeadset service.
49     * Only used by {@link DockService}.
50     */
51    public interface ServiceListener {
52        /**
53         * Called to notify the client when this proxy object has been
54         * connected to the BluetoothHeadset service. Clients must wait for
55         * this callback before making IPC calls on the BluetoothHeadset
56         * service.
57         */
58        void onServiceConnected();
59
60        /**
61         * Called to notify the client that this proxy object has been
62         * disconnected from the BluetoothHeadset service. Clients must not
63         * make IPC calls on the BluetoothHeadset service after this callback.
64         * This callback will currently only occur if the application hosting
65         * the BluetoothHeadset service, but may be called more often in future.
66         */
67        void onServiceDisconnected();
68    }
69
70    private final Context mContext;
71    private final LocalBluetoothAdapter mLocalAdapter;
72    private final CachedBluetoothDeviceManager mDeviceManager;
73    private final BluetoothEventManager mEventManager;
74
75    private A2dpProfile mA2dpProfile;
76    private HeadsetProfile mHeadsetProfile;
77    private final HidProfile mHidProfile;
78    private OppProfile mOppProfile;
79    private final PanProfile mPanProfile;
80
81    /**
82     * Mapping from profile name, e.g. "HEADSET" to profile object.
83     */
84    private final Map<String, LocalBluetoothProfile>
85            mProfileNameMap = new HashMap<String, LocalBluetoothProfile>();
86
87    LocalBluetoothProfileManager(Context context,
88            LocalBluetoothAdapter adapter,
89            CachedBluetoothDeviceManager deviceManager,
90            BluetoothEventManager eventManager) {
91        mContext = context;
92
93        mLocalAdapter = adapter;
94        mDeviceManager = deviceManager;
95        mEventManager = eventManager;
96        // pass this reference to adapter and event manager (circular dependency)
97        mLocalAdapter.setProfileManager(this);
98        mEventManager.setProfileManager(this);
99
100        ParcelUuid[] uuids = adapter.getUuids();
101
102        // uuids may be null if Bluetooth is turned off
103        if (uuids != null) {
104            updateLocalProfiles(uuids);
105        }
106
107        // Always add HID and PAN profiles
108        mHidProfile = new HidProfile(context, mLocalAdapter);
109        addProfile(mHidProfile, HidProfile.NAME,
110                BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
111
112        mPanProfile = new PanProfile(context);
113        addPanProfile(mPanProfile, PanProfile.NAME,
114                BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
115
116        Log.d(TAG, "LocalBluetoothProfileManager construction complete");
117    }
118
119    /**
120     * Initialize or update the local profile objects. If a UUID was previously
121     * present but has been removed, we print a warning but don't remove the
122     * profile object as it might be referenced elsewhere, or the UUID might
123     * come back and we don't want multiple copies of the profile objects.
124     * @param uuids
125     */
126    void updateLocalProfiles(ParcelUuid[] uuids) {
127        // A2DP
128        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSource)) {
129            if (mA2dpProfile == null) {
130                Log.d(TAG, "Adding local A2DP profile");
131                mA2dpProfile = new A2dpProfile(mContext);
132                addProfile(mA2dpProfile, A2dpProfile.NAME,
133                        BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
134            }
135        } else if (mA2dpProfile != null) {
136            Log.w(TAG, "Warning: A2DP profile was previously added but the UUID is now missing.");
137        }
138
139        // Headset / Handsfree
140        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree_AG) ||
141            BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP_AG)) {
142            if (mHeadsetProfile == null) {
143                Log.d(TAG, "Adding local HEADSET profile");
144                mHeadsetProfile = new HeadsetProfile(mContext, mLocalAdapter,
145                        mDeviceManager, this);
146                addProfile(mHeadsetProfile, HeadsetProfile.NAME,
147                        BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
148            }
149        } else if (mHeadsetProfile != null) {
150            Log.w(TAG, "Warning: HEADSET profile was previously added but the UUID is now missing.");
151        }
152
153        // OPP
154        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.ObexObjectPush)) {
155            if (mOppProfile == null) {
156                Log.d(TAG, "Adding local OPP profile");
157                mOppProfile = new OppProfile();
158                // Note: no event handler for OPP, only name map.
159                mProfileNameMap.put(OppProfile.NAME, mOppProfile);
160            }
161        } else if (mOppProfile != null) {
162            Log.w(TAG, "Warning: OPP profile was previously added but the UUID is now missing.");
163        }
164        mEventManager.registerProfileIntentReceiver();
165
166        // There is no local SDP record for HID and Settings app doesn't control PBAP
167    }
168
169    private final Collection<ServiceListener> mServiceListeners =
170            new ArrayList<ServiceListener>();
171
172    private void addProfile(LocalBluetoothProfile profile,
173            String profileName, String stateChangedAction) {
174        mEventManager.addProfileHandler(stateChangedAction, new StateChangedHandler(profile));
175        mProfileNameMap.put(profileName, profile);
176    }
177
178    private void addPanProfile(LocalBluetoothProfile profile,
179            String profileName, String stateChangedAction) {
180        mEventManager.addProfileHandler(stateChangedAction,
181                new PanStateChangedHandler(profile));
182        mProfileNameMap.put(profileName, profile);
183    }
184
185    LocalBluetoothProfile getProfileByName(String name) {
186        return mProfileNameMap.get(name);
187    }
188
189    // Called from LocalBluetoothAdapter when state changes to ON
190    void setBluetoothStateOn() {
191        ParcelUuid[] uuids = mLocalAdapter.getUuids();
192        if (uuids != null) {
193            updateLocalProfiles(uuids);
194        }
195        mEventManager.readPairedDevices();
196    }
197
198    /**
199     * Generic handler for connection state change events for the specified profile.
200     */
201    private class StateChangedHandler implements BluetoothEventManager.Handler {
202        final LocalBluetoothProfile mProfile;
203
204        StateChangedHandler(LocalBluetoothProfile profile) {
205            mProfile = profile;
206        }
207
208        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
209            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
210            if (cachedDevice == null) {
211                Log.w(TAG, "StateChangedHandler found new device: " + device);
212                cachedDevice = mDeviceManager.addDevice(mLocalAdapter,
213                        LocalBluetoothProfileManager.this, device);
214            }
215            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
216            int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
217            if (newState == BluetoothProfile.STATE_DISCONNECTED &&
218                    oldState == BluetoothProfile.STATE_CONNECTING) {
219                Log.i(TAG, "Failed to connect " + mProfile + " device");
220            }
221
222            cachedDevice.onProfileStateChanged(mProfile, newState);
223            cachedDevice.refresh();
224        }
225    }
226
227    /** State change handler for NAP and PANU profiles. */
228    private class PanStateChangedHandler extends StateChangedHandler {
229
230        PanStateChangedHandler(LocalBluetoothProfile profile) {
231            super(profile);
232        }
233
234        @Override
235        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
236            PanProfile panProfile = (PanProfile) mProfile;
237            int role = intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, 0);
238            panProfile.setLocalRole(device, role);
239            super.onReceive(context, intent, device);
240        }
241    }
242
243    // called from DockService
244    void addServiceListener(ServiceListener l) {
245        mServiceListeners.add(l);
246    }
247
248    // called from DockService
249    void removeServiceListener(ServiceListener l) {
250        mServiceListeners.remove(l);
251    }
252
253    // not synchronized: use only from UI thread! (TODO: verify)
254    void callServiceConnectedListeners() {
255        for (ServiceListener l : mServiceListeners) {
256            l.onServiceConnected();
257        }
258    }
259
260    // not synchronized: use only from UI thread! (TODO: verify)
261    void callServiceDisconnectedListeners() {
262        for (ServiceListener listener : mServiceListeners) {
263            listener.onServiceDisconnected();
264        }
265    }
266
267    // This is called by DockService, so check Headset and A2DP.
268    public synchronized boolean isManagerReady() {
269        // Getting just the headset profile is fine for now. Will need to deal with A2DP
270        // and others if they aren't always in a ready state.
271        LocalBluetoothProfile profile = mHeadsetProfile;
272        if (profile != null) {
273            return profile.isProfileReady();
274        }
275        profile = mA2dpProfile;
276        if (profile != null) {
277            return profile.isProfileReady();
278        }
279        return false;
280    }
281
282    A2dpProfile getA2dpProfile() {
283        return mA2dpProfile;
284    }
285
286    HeadsetProfile getHeadsetProfile() {
287        return mHeadsetProfile;
288    }
289
290    /**
291     * Fill in a list of LocalBluetoothProfile objects that are supported by
292     * the local device and the remote device.
293     *
294     * @param uuids of the remote device
295     * @param localUuids UUIDs of the local device
296     * @param profiles The list of profiles to fill
297     * @param removedProfiles list of profiles that were removed
298     */
299    synchronized void updateProfiles(ParcelUuid[] uuids, ParcelUuid[] localUuids,
300            Collection<LocalBluetoothProfile> profiles,
301            Collection<LocalBluetoothProfile> removedProfiles) {
302        // Copy previous profile list into removedProfiles
303        removedProfiles.clear();
304        removedProfiles.addAll(profiles);
305        profiles.clear();
306
307        if (uuids == null) {
308            return;
309        }
310
311        if (mHeadsetProfile != null) {
312            if ((BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.HSP_AG) &&
313                    BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)) ||
314                    (BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.Handsfree_AG) &&
315                            BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree))) {
316                profiles.add(mHeadsetProfile);
317                removedProfiles.remove(mHeadsetProfile);
318            }
319        }
320
321        if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS) &&
322            mA2dpProfile != null) {
323            profiles.add(mA2dpProfile);
324            removedProfiles.remove(mA2dpProfile);
325        }
326
327        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.ObexObjectPush) &&
328            mOppProfile != null) {
329            profiles.add(mOppProfile);
330            removedProfiles.remove(mOppProfile);
331        }
332
333        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hid) &&
334            mHidProfile != null) {
335            profiles.add(mHidProfile);
336            removedProfiles.remove(mHidProfile);
337        }
338
339        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.NAP) &&
340            mPanProfile != null) {
341            profiles.add(mPanProfile);
342            removedProfiles.remove(mPanProfile);
343        }
344    }
345}
346