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 */ 16 17 package com.android.car.settings.bluetooth; 18 19 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE; 20 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.bluetooth.BluetoothDevice; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.res.Resources; 32 import android.os.IBinder; 33 import android.text.TextUtils; 34 35 import com.android.car.settings.R; 36 import com.android.car.settings.common.Logger; 37 38 /** 39 * BluetoothPairingService shows a notification if there is a pending bond request 40 * which can launch the appropriate pairing dialog when tapped. 41 */ 42 public final class BluetoothPairingService extends Service { 43 44 private static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; 45 46 private static final String ACTION_DISMISS_PAIRING = 47 "com.android.settings.bluetooth.ACTION_DISMISS_PAIRING"; 48 49 private static final String BLUETOOTH_NOTIFICATION_CHANNEL = 50 "bluetooth_notification_channel"; 51 52 private static final Logger LOG = new Logger(BluetoothPairingService.class); 53 54 private BluetoothDevice mDevice; 55 getPairingDialogIntent(Context context, Intent intent)56 public static Intent getPairingDialogIntent(Context context, Intent intent) { 57 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 58 int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, 59 BluetoothDevice.ERROR); 60 Intent pairingIntent = new Intent(); 61 pairingIntent.setClass(context, BluetoothPairingDialog.class); 62 pairingIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 63 pairingIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, type); 64 if (type == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION || 65 type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY || 66 type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { 67 int pairingKey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, 68 BluetoothDevice.ERROR); 69 pairingIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pairingKey); 70 } 71 pairingIntent.setAction(BluetoothDevice.ACTION_PAIRING_REQUEST); 72 return pairingIntent; 73 } 74 75 private boolean mRegistered = false; 76 private final BroadcastReceiver mCancelReceiver = new BroadcastReceiver() { 77 @Override 78 public void onReceive(Context context, Intent intent) { 79 String action = intent.getAction(); 80 if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { 81 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 82 BluetoothDevice.ERROR); 83 if ((bondState != BluetoothDevice.BOND_NONE) && (bondState 84 != BluetoothDevice.BOND_BONDED)) { 85 return; 86 } 87 } else if (action.equals(ACTION_DISMISS_PAIRING)) { 88 LOG.d("Notification cancel " + mDevice.getAddress() + " (" 89 + mDevice.getName() + ")"); 90 mDevice.cancelBondProcess(); 91 } else { 92 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 93 BluetoothDevice.ERROR); 94 LOG.d("Dismiss pairing for " + mDevice.getAddress() + " (" 95 + mDevice.getName() + "), BondState: " + bondState); 96 } 97 stopForeground(true); 98 stopSelf(); 99 } 100 }; 101 102 @Override onCreate()103 public void onCreate() { 104 NotificationManager mgr = (NotificationManager) this 105 .getSystemService(Context.NOTIFICATION_SERVICE); 106 NotificationChannel notificationChannel = new NotificationChannel( 107 BLUETOOTH_NOTIFICATION_CHANNEL, 108 this.getString(R.string.bluetooth_settings_title), 109 NotificationManager.IMPORTANCE_HIGH); 110 mgr.createNotificationChannel(notificationChannel); 111 } 112 113 @Override onStartCommand(Intent intent, int flags, int startId)114 public int onStartCommand(Intent intent, int flags, int startId) { 115 if (intent == null) { 116 LOG.e("Can't start: null intent!"); 117 stopSelf(); 118 return START_NOT_STICKY; 119 } 120 121 Resources res = getResources(); 122 Notification.Builder builder = new Notification.Builder(this, 123 BLUETOOTH_NOTIFICATION_CHANNEL) 124 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) 125 .setTicker(res.getString(R.string.bluetooth_notif_ticker)) 126 .setLocalOnly(true); 127 128 PendingIntent pairIntent = PendingIntent.getActivity(this, 0, 129 getPairingDialogIntent(this, intent), 130 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); 131 132 PendingIntent dismissIntent = PendingIntent.getBroadcast(this, 0, 133 new Intent(ACTION_DISMISS_PAIRING), 134 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); 135 136 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 137 138 if (mDevice != null && mDevice.getBondState() != BluetoothDevice.BOND_BONDING) { 139 LOG.w("Device " + mDevice + " not bonding: " + mDevice.getBondState()); 140 stopSelf(); 141 return START_NOT_STICKY; 142 } 143 144 String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME); 145 if (TextUtils.isEmpty(name)) { 146 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 147 name = device != null ? device.getAlias() : res.getString( 148 android.R.string.unknownName); 149 } 150 151 LOG.d("Show pairing notification for " + mDevice.getAddress() + " (" + name + ")"); 152 153 Notification.Action pairAction = new Notification.Action.Builder(0, 154 res.getString(R.string.bluetooth_device_context_pair_connect), pairIntent).build(); 155 Notification.Action dismissAction = new Notification.Action.Builder(0, 156 res.getString(android.R.string.cancel), dismissIntent).build(); 157 158 builder.setContentTitle(res.getString(R.string.bluetooth_notif_title)) 159 .setContentText(res.getString(R.string.bluetooth_notif_message, name)) 160 .setContentIntent(pairIntent) 161 .setDefaults(Notification.DEFAULT_SOUND) 162 .setColor(getColor(com.android.internal.R.color.system_notification_accent_color)) 163 .addAction(pairAction) 164 .addAction(dismissAction); 165 166 IntentFilter filter = new IntentFilter(); 167 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 168 filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL); 169 filter.addAction(ACTION_DISMISS_PAIRING); 170 registerReceiver(mCancelReceiver, filter); 171 mRegistered = true; 172 173 startForeground(NOTIFICATION_ID, builder.getNotification(), 174 FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE); 175 return START_REDELIVER_INTENT; 176 } 177 178 @Override onDestroy()179 public void onDestroy() { 180 if (mRegistered) { 181 unregisterReceiver(mCancelReceiver); 182 mRegistered = false; 183 } 184 stopForeground(true); 185 } 186 187 @Override onBind(Intent intent)188 public IBinder onBind(Intent intent) { 189 // No binding. 190 return null; 191 } 192 } 193