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