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