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.cancelPairing(); 102 } else { 103 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 104 BluetoothDevice.ERROR); 105 Log.d(TAG, "Dismiss pairing for " + " (" + 106 mDevice.getName() + "), BondState: " + bondState); 107 } 108 109 mNm.cancel(NOTIFICATION_ID); 110 stopSelf(); 111 } 112 }; 113 114 @Override onCreate()115 public void onCreate() { 116 mNm = getSystemService(NotificationManager.class); 117 NotificationChannel notificationChannel = new NotificationChannel( 118 BLUETOOTH_NOTIFICATION_CHANNEL, 119 this.getString(R.string.bluetooth), 120 NotificationManager.IMPORTANCE_HIGH); 121 mNm.createNotificationChannel(notificationChannel); 122 } 123 124 @Override onStartCommand(Intent intent, int flags, int startId)125 public int onStartCommand(Intent intent, int flags, int startId) { 126 if (intent == null) { 127 Log.e(TAG, "Can't start: null intent!"); 128 stopSelf(); 129 return START_NOT_STICKY; 130 } 131 String action = intent.getAction(); 132 Log.d(TAG, "onStartCommand() action : " + action); 133 134 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 135 136 if (mDevice != null && mDevice.getBondState() != BluetoothDevice.BOND_BONDING) { 137 Log.w(TAG, "Device " + mDevice.getName() + " not bonding: " + mDevice.getBondState()); 138 mNm.cancel(NOTIFICATION_ID); 139 stopSelf(); 140 return START_NOT_STICKY; 141 } 142 143 if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) { 144 createPairingNotification(intent); 145 } else if (TextUtils.equals(action, ACTION_DISMISS_PAIRING)) { 146 Log.d(TAG, "Notification cancel " + " (" + mDevice.getName() + ")"); 147 mDevice.cancelPairing(); 148 mNm.cancel(NOTIFICATION_ID); 149 stopSelf(); 150 } else if (TextUtils.equals(action, ACTION_PAIRING_DIALOG)) { 151 Intent pairingDialogIntent = getPairingDialogIntent(this, intent, 152 BluetoothDevice.EXTRA_PAIRING_INITIATOR_BACKGROUND); 153 154 IntentFilter filter = new IntentFilter(); 155 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 156 filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL); 157 filter.addAction(ACTION_DISMISS_PAIRING); 158 registerReceiver(mCancelReceiver, filter); 159 mRegistered = true; 160 161 startActivity(pairingDialogIntent); 162 } 163 164 return START_STICKY; 165 } 166 createPairingNotification(Intent intent)167 private void createPairingNotification(Intent intent) { 168 Resources res = getResources(); 169 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, 170 BLUETOOTH_NOTIFICATION_CHANNEL) 171 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) 172 .setTicker(res.getString(R.string.bluetooth_notif_ticker)) 173 .setLocalOnly(true); 174 175 int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, 176 BluetoothDevice.ERROR); 177 Intent pairingDialogIntent = new Intent(ACTION_PAIRING_DIALOG); 178 pairingDialogIntent.setClass(this, BluetoothPairingService.class); 179 pairingDialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 180 pairingDialogIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, type); 181 182 if (type == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION 183 || type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY 184 || type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { 185 int pairingKey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, 186 BluetoothDevice.ERROR); 187 pairingDialogIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pairingKey); 188 } 189 190 PendingIntent pairIntent = PendingIntent.getService(this, 0, pairingDialogIntent, 191 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT 192 | PendingIntent.FLAG_IMMUTABLE); 193 194 Intent serviceIntent = new Intent(ACTION_DISMISS_PAIRING); 195 serviceIntent.setClass(this, BluetoothPairingService.class); 196 serviceIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 197 PendingIntent dismissIntent = PendingIntent.getService(this, 0, 198 serviceIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 199 200 String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME); 201 if (TextUtils.isEmpty(name)) { 202 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 203 name = device != null ? device.getAlias() : res.getString(android.R.string.unknownName); 204 } 205 206 Log.d(TAG, "Show pairing notification for " + " (" + name + ")"); 207 208 NotificationCompat.Action pairAction = new NotificationCompat.Action.Builder(0, 209 res.getString(R.string.bluetooth_device_context_pair_connect), pairIntent).build(); 210 NotificationCompat.Action dismissAction = new NotificationCompat.Action.Builder(0, 211 res.getString(android.R.string.cancel), dismissIntent).build(); 212 213 builder.setContentTitle(res.getString(R.string.bluetooth_notif_title)) 214 .setContentText(res.getString(R.string.bluetooth_notif_message, name)) 215 .setContentIntent(pairIntent) 216 .setDefaults(Notification.DEFAULT_SOUND) 217 .setOngoing(true) 218 .setColor(getColor(com.android.internal.R.color.system_notification_accent_color)) 219 .addAction(pairAction) 220 .addAction(dismissAction); 221 222 mNm.notify(NOTIFICATION_ID, builder.build()); 223 } 224 225 @Override onDestroy()226 public void onDestroy() { 227 if (mRegistered) { 228 unregisterReceiver(mCancelReceiver); 229 mRegistered = false; 230 } 231 } 232 233 @Override onBind(Intent intent)234 public IBinder onBind(Intent intent) { 235 // No binding. 236 return null; 237 } 238 } 239