1 /* 2 * Copyright (C) 2020 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 com.android.tv.settings.accessories; 18 19 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.GENERAL_SLICE_URI; 20 21 import android.app.Service; 22 import android.bluetooth.BluetoothA2dp; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothHidHost; 26 import android.bluetooth.BluetoothProfile; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.os.Binder; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.util.Log; 36 import android.widget.Toast; 37 38 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 39 import com.android.tv.settings.R; 40 41 import java.io.FileDescriptor; 42 import java.io.PrintWriter; 43 import java.util.ArrayList; 44 import java.util.List; 45 46 /** The Service for handling Bluetooth-related logic. */ 47 public class BluetoothDevicesService extends Service { 48 49 private static final boolean DEBUG = false; 50 private static final String TAG = "BtDevicesServices"; 51 52 private final List<BluetoothDeviceProvider.Listener> mListeners = new ArrayList<>(); 53 private final Binder mBinder = new LocalBinder(); 54 protected final Handler mHandler = new Handler(Looper.getMainLooper()); 55 56 /** Binder in BluetoothDeviceService. */ 57 public class LocalBinder extends Binder implements BluetoothDeviceProvider { 58 getDevices()59 public List<BluetoothDevice> getDevices() { 60 return BluetoothDevicesService.this.getDevices(); 61 } 62 63 @Override addListener(BluetoothDeviceProvider.Listener listener)64 public void addListener(BluetoothDeviceProvider.Listener listener) { 65 mHandler.post(() -> { 66 mListeners.add(listener); 67 68 // Trigger first update after listener callback is registered. 69 for (BluetoothDevice device : getDevices()) { 70 if (device.isConnected()) { 71 listener.onDeviceUpdated(device); 72 } 73 } 74 }); 75 } 76 77 @Override removeListener(BluetoothDeviceProvider.Listener listener)78 public void removeListener(BluetoothDeviceProvider.Listener listener) { 79 mHandler.post(() -> mListeners.remove(listener)); 80 } 81 82 @Override connectDevice(BluetoothDevice device)83 public void connectDevice(BluetoothDevice device) { 84 BluetoothDevicesService.this.connectDevice(device); 85 } 86 87 @Override disconnectDevice(BluetoothDevice device)88 public void disconnectDevice(BluetoothDevice device) { 89 BluetoothDevicesService.this.disconnectDevice(device); 90 } 91 92 @Override forgetDevice(BluetoothDevice device)93 public void forgetDevice(BluetoothDevice device) { 94 BluetoothDevicesService.this.forgetDevice(device); 95 } 96 97 @Override renameDevice(BluetoothDevice device, String newName)98 public void renameDevice(BluetoothDevice device, String newName) { 99 BluetoothDevicesService.this.renameDevice(device, newName); 100 } 101 } 102 103 BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() { 104 @Override 105 public void onReceive(Context context, Intent intent) { 106 final String action = intent.getAction(); 107 final BluetoothDevice device = 108 intent == null ? null : intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 109 // The sequence of a typical connection is: acl connected, bonding, bonded, profile 110 // connecting, profile connected. 111 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 112 final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); 113 switch (state) { 114 case BluetoothDevice.BOND_BONDED: 115 break; 116 case BluetoothDevice.BOND_NONE: 117 mHandler.post(() -> onDeviceUpdated(device)); 118 break; 119 case BluetoothDevice.BOND_BONDING: 120 break; 121 default: 122 if (DEBUG) { 123 Log.e(TAG, "unknown state " + state + " " + device); 124 } 125 } 126 } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { 127 final int state = 128 intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 129 // Actively refresh the connected devices slice. This is most useful when the 130 // bluetooth toggle is toggled back on. 131 if (state == BluetoothAdapter.STATE_ON) { 132 getContentResolver().notifyChange(GENERAL_SLICE_URI, null); 133 } 134 } else { 135 switch(action) { 136 case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED: 137 if (device.getBondState() == BluetoothDevice.BOND_BONDED) { 138 mHandler.post(() -> onDeviceUpdated(device)); 139 } 140 break; 141 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 142 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 143 mHandler.post(() -> onA2dpConnectionStateChanged(device.getName(), state)); 144 break; 145 case BluetoothDevice.ACTION_ACL_CONNECTED: 146 Log.i(TAG, "acl connected " + device); 147 if (device.getBondState() == BluetoothDevice.BOND_BONDED) { 148 mHandler.post(() -> onDeviceUpdated(device)); 149 } 150 break; 151 case BluetoothDevice.ACTION_ACL_DISCONNECTED: 152 Log.i(TAG, "acl disconnected " + device); 153 mHandler.post(() -> onDeviceUpdated(device)); 154 break; 155 case BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED: 156 Log.i(TAG, "acl disconnect requested: " + device); 157 break; 158 } 159 } 160 } 161 }; 162 163 @Override onBind(Intent intent)164 public IBinder onBind(Intent intent) { 165 return mBinder; 166 } 167 168 @Override onCreate()169 public void onCreate() { 170 super.onCreate(); 171 if (DEBUG) { 172 Log.e(TAG, "onCreate"); 173 } 174 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); 175 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); 176 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 177 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 178 filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); 179 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); // Headset connection 180 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); // Bluetooth toggle 181 registerReceiver(mBluetoothReceiver, filter); 182 } 183 184 @Override onDestroy()185 public void onDestroy() { 186 if (DEBUG) { 187 Log.e(TAG, "onDestroy"); 188 } 189 unregisterReceiver(mBluetoothReceiver); 190 mHandler.removeCallbacksAndMessages(null); 191 super.onDestroy(); 192 } 193 194 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)195 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 196 for (BluetoothDevice device: getDevices()) { 197 if (!device.isConnected()) { 198 continue; 199 } 200 writer.printf("%s (%s):%n", device.getName(), device.getAddress()); 201 } 202 } 203 connectDevice(BluetoothDevice device)204 private void connectDevice(BluetoothDevice device) { 205 if (device != null) { 206 CachedBluetoothDevice cachedDevice = 207 AccessoryUtils.getCachedBluetoothDevice(this, device); 208 if (cachedDevice != null) { 209 cachedDevice.connect(); 210 } 211 } 212 } 213 disconnectDevice(BluetoothDevice device)214 private void disconnectDevice(BluetoothDevice device) { 215 if (device != null) { 216 CachedBluetoothDevice cachedDevice = 217 AccessoryUtils.getCachedBluetoothDevice(this, device); 218 if (cachedDevice != null) { 219 cachedDevice.disconnect(); 220 } 221 } 222 } 223 forgetDevice(BluetoothDevice device)224 private static void forgetDevice(BluetoothDevice device) { 225 if (device == null || !device.removeBond()) { 226 Log.w(TAG, "failed to remove bond: " + device); 227 } 228 } 229 renameDevice(BluetoothDevice device, String newName)230 private void renameDevice(BluetoothDevice device, String newName) { 231 if (device != null) { 232 device.setAlias(newName); 233 mHandler.post(() -> onDeviceUpdated(device)); 234 } 235 } 236 onA2dpConnectionStateChanged(String deviceName, int connectionStatus)237 private void onA2dpConnectionStateChanged(String deviceName, int connectionStatus) { 238 String resStr; 239 String text; 240 switch (connectionStatus) { 241 case BluetoothProfile.STATE_CONNECTED: 242 resStr = getResources().getString(R.string.bluetooth_device_connected_toast); 243 text = String.format(resStr, deviceName); 244 Toast.makeText(BluetoothDevicesService.this.getApplicationContext(), 245 text, Toast.LENGTH_SHORT).show(); 246 break; 247 case BluetoothProfile.STATE_DISCONNECTED: 248 resStr = getResources().getString(R.string.bluetooth_device_disconnected_toast); 249 text = String.format(resStr, deviceName); 250 Toast.makeText(BluetoothDevicesService.this.getApplicationContext(), 251 text, Toast.LENGTH_SHORT).show(); 252 break; 253 case BluetoothProfile.STATE_CONNECTING: 254 case BluetoothProfile.STATE_DISCONNECTING: 255 default: 256 break; 257 } 258 } 259 onDeviceUpdated(BluetoothDevice device)260 private void onDeviceUpdated(BluetoothDevice device) { 261 mListeners.forEach(listener -> listener.onDeviceUpdated(device)); 262 } 263 264 /** Returns the BluetoothDevice object with the input address. */ findDevice(String address)265 public static BluetoothDevice findDevice(String address) { 266 List<BluetoothDevice> devices = getDevices(); 267 BluetoothDevice curDevice = null; 268 for (BluetoothDevice device: devices) { 269 if (address.equals(device.getAddress())) { 270 curDevice = device; 271 break; 272 } 273 } 274 return curDevice; 275 } 276 getDevices()277 private static List<BluetoothDevice> getDevices() { 278 final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); 279 if (btAdapter != null) { 280 return new ArrayList<>(btAdapter.getBondedDevices()); 281 } 282 return new ArrayList<>(); // Empty list 283 } 284 } 285