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