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