1/*
2 * Copyright (C) 2017 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 */
16package com.android.car;
17
18import static android.car.settings.CarSettings.Secure
19        .KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_0;
20import static android.car.settings.CarSettings.Secure
21        .KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_1;
22import static android.car.settings.CarSettings.Secure
23        .KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_0;
24import static android.car.settings.CarSettings.Secure
25        .KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_1;
26import static android.car.settings.CarSettings.Secure
27        .KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_0;
28import static android.car.settings.CarSettings.Secure
29        .KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_1;
30
31import android.app.ActivityManager;
32import android.bluetooth.BluetoothDevice;
33import android.bluetooth.BluetoothProfile;
34import android.car.CarBluetoothManager;
35import android.car.ICarBluetooth;
36import android.content.Context;
37import android.content.pm.PackageManager;
38import android.provider.Settings;
39import android.util.Log;
40
41import java.io.PrintWriter;
42
43/**
44 * CarBluetoothService - deals with the automatically connecting to a known device via bluetooth.
45 * Interacts with a policy -{@link BluetoothDeviceConnectionPolicy} -to initiate connections and
46 * update status.
47 * The {@link BluetoothDeviceConnectionPolicy} is responsible for finding the appropriate device to
48 * connect for a specific profile.
49 */
50
51public class CarBluetoothService extends ICarBluetooth.Stub implements CarServiceBase {
52
53    private static final String TAG = "CarBluetoothService";
54    private final Context mContext;
55    private final BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicy;
56    private static final boolean DBG = false;
57
58    public CarBluetoothService(Context context, CarCabinService carCabinService,
59            CarSensorService carSensorService, PerUserCarServiceHelper userSwitchService) {
60        mContext = context;
61        mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext,
62                carCabinService, carSensorService, userSwitchService, this);
63    }
64
65    @Override
66    public void init() {
67        mBluetoothDeviceConnectionPolicy.init();
68    }
69
70    @Override
71    public synchronized void release() {
72        mBluetoothDeviceConnectionPolicy.release();
73    }
74
75    /**
76     * Set the Auto connect priority for a paired Bluetooth Device.
77     * For example, if a device is tagged as a Primary device for a supported Bluetooth Profile,
78     * every new Auto Connect attempt would start with trying to connect to *that* device.
79     * This priority is set at a Bluetooth profile granularity
80     *
81     * @param deviceToSet   - Device to set priority (Tag)
82     * @param profileToSet  - BluetoothProfile to set priority for.
83     * @param priorityToSet - What priority level to set to
84     * @hide
85     */
86    public void setBluetoothDeviceConnectionPriority(BluetoothDevice deviceToSet, int profileToSet,
87            int priorityToSet) {
88        setBluetoothDeviceConnectionPriority(deviceToSet.getAddress(), profileToSet, priorityToSet);
89    }
90
91    public void setBluetoothDeviceConnectionPriority(String deviceAddress, int profileToSet,
92            int priorityToSet) {
93        // Check if the caller has Bluetooth Admin Permissions
94        enforceBluetoothAdminPermission();
95        if (priorityToSet == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_1) {
96            if (!isPriorityDevicePresent(profileToSet,
97                    CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)) {
98                Log.e(TAG, "Secondary Device not allowed without a primary device");
99                return;
100            }
101        }
102        // Write the priority preference to Secure settings.  The Bluetooth device connection policy
103        // will look up the Settings when it initiates a connection
104        Settings.Secure.putStringForUser(mContext.getContentResolver(),
105                getKeyForProfile(profileToSet, priorityToSet), deviceAddress,
106                ActivityManager.getCurrentUser());
107
108    }
109
110    /**
111     * Unset the Auto connect priority for the given profile
112     *
113     * @param profileToClear  - Profile to unset priority
114     * @param priorityToClear - Which priority to clear (Primary or Secondary)
115     * @hide
116     */
117    public void clearBluetoothDeviceConnectionPriority(int profileToClear, int priorityToClear) {
118        // Check if the caller has Bluetooth Admin F@Permissions
119        enforceBluetoothAdminPermission();
120        if (priorityToClear == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0) {
121            if (isPriorityDevicePresent(profileToClear,
122                    CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_1)) {
123                Log.e(TAG, "Please remove Secondary device before removing Primary Device");
124                return;
125            }
126        }
127        Settings.Secure.putStringForUser(mContext.getContentResolver(),
128                getKeyForProfile(profileToClear, priorityToClear),
129                CarBluetoothManager.BLUETOOTH_NO_PRIORITY_DEVICE,
130                ActivityManager.getCurrentUser());
131    }
132
133    /**
134     * Returns if there is a device that has been tagged with the given priority for the given
135     * profile.
136     *
137     * @param profile         - BluetoothProfile
138     * @param priorityToCheck - Priority to check
139     * @return true if there is a device present with the given priority, false if not
140     */
141    public boolean isPriorityDevicePresent(int profile, int priorityToCheck) {
142        String deviceName = getDeviceNameWithPriority(profile, priorityToCheck);
143        if (deviceName != null && !deviceName.equalsIgnoreCase(
144                CarBluetoothManager.BLUETOOTH_NO_PRIORITY_DEVICE)) {
145            return true;
146        } else {
147            if (DBG) {
148                Log.d(TAG,
149                        "No device present for priority: " + priorityToCheck + " profile: "
150                                + profile);
151            }
152            return false;
153        }
154    }
155
156    /**
157     * Returns the Bluetooth device address as a String that has been tagged with the given priority
158     * for the given profile.
159     *
160     * @param profile         - BluetoothProfile
161     * @param priorityToCheck - Priority to check
162     * @return BluetoothDevice address if present, null if absent
163     */
164    public String getDeviceNameWithPriority(int profile, int priorityToCheck) {
165        String keyToQuery = null;
166        String deviceName = null;
167        enforceBluetoothAdminPermission();
168        switch (profile) {
169            case BluetoothProfile.A2DP_SINK:
170                keyToQuery = (priorityToCheck
171                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
172                        ? KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_0
173                        : KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_1;
174                break;
175            case BluetoothProfile.HEADSET_CLIENT:
176            case BluetoothProfile.PBAP_CLIENT:
177                keyToQuery = (priorityToCheck
178                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
179                        ? KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_0
180                        : KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_1;
181                break;
182            case BluetoothProfile.MAP_CLIENT:
183                keyToQuery = (priorityToCheck
184                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
185                        ? KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_0
186                        : KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_1;
187                break;
188            default:
189                if (DBG) {
190                    Log.d(TAG, "Unknown Bluetooth profile");
191                }
192        }
193        if (keyToQuery != null) {
194            deviceName = Settings.Secure.getStringForUser(mContext.getContentResolver(),
195                    keyToQuery, (int) ActivityManager.getCurrentUser());
196        }
197        return deviceName;
198    }
199
200    private void enforceBluetoothAdminPermission() {
201        if (mContext != null
202                && PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
203                android.Manifest.permission.BLUETOOTH_ADMIN)) {
204            return;
205        }
206        if (mContext == null) {
207            Log.e(TAG, "CarBluetoothPrioritySettings does not have a Context");
208        }
209        throw new SecurityException("requires permission " + android.Manifest.permission.BLUETOOTH_ADMIN);
210    }
211
212    private String getKeyForProfile(int profile, int priority) {
213        String keyToLookup = null;
214        switch (profile) {
215            case BluetoothProfile.A2DP_SINK:
216                keyToLookup = (priority
217                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
218                        ? KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_0
219                        : KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_1;
220                break;
221            case BluetoothProfile.MAP_CLIENT:
222                keyToLookup = (priority
223                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
224                        ? KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_0
225                        : KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_1;
226                break;
227            case BluetoothProfile.PBAP_CLIENT:
228                // fall through
229            case BluetoothProfile.HEADSET_CLIENT:
230                keyToLookup = (priority
231                        == CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0)
232                        ? KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_0
233                        : KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_1;
234                break;
235            default:
236                Log.e(TAG, "Unsupported Bluetooth profile to set priority to");
237                break;
238        }
239        return keyToLookup;
240    }
241
242    @Override
243    public synchronized void dump(PrintWriter writer) {
244        mBluetoothDeviceConnectionPolicy.dump(writer);
245    }
246
247}
248