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