1 /* 2 * Copyright (C) 2016 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.google.android.car.kitchensink.bluetooth; 18 19 import android.Manifest; 20 import android.annotation.TargetApi; 21 import android.app.PendingIntent; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothDevicePicker; 25 import android.bluetooth.BluetoothMapClient; 26 import android.bluetooth.BluetoothProfile; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.PackageManager; 32 import android.net.Uri; 33 import android.os.Build; 34 import android.os.Bundle; 35 import android.telecom.PhoneAccount; 36 import android.util.Log; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.Button; 41 import android.widget.CheckBox; 42 import android.widget.EditText; 43 import android.widget.TextView; 44 import android.widget.Toast; 45 46 import androidx.annotation.Nullable; 47 import androidx.fragment.app.Fragment; 48 49 import com.google.android.car.kitchensink.R; 50 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.Date; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Objects; 57 58 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 59 public class MapMceTestFragment extends Fragment { 60 static final String REPLY_MESSAGE_TO_SEND = "I am currently driving."; 61 static final String NEW_MESSAGE_TO_SEND_SHORT = "This is a new message."; 62 static final String NEW_MESSAGE_TO_SEND_LONG = "Lorem ipsum dolor sit amet, consectetur " 63 + "adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna " 64 + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi " 65 + "ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " 66 + "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " 67 + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim " 68 + "id est laborum.\n\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. " 69 + "Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus " 70 + "magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis " 71 + "ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. " 72 + "Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt " 73 + "sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. " 74 + "Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, " 75 + "consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl " 76 + "adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque " 77 + "nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, " 78 + "laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, " 79 + "feugiat in, orci. In hac habitasse platea dictumst.\n\nLorem ipsum dolor sit " 80 + "amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " 81 + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " 82 + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " 83 + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " 84 + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia " 85 + "deserunt mollit anim id est laborum.\n\nCurabitur pretium tincidunt lacus. Nulla " 86 + "gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum " 87 + "elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh " 88 + "euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus " 89 + "a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod " 90 + "turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec " 91 + "fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, " 92 + "commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, " 93 + "felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis " 94 + "scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus " 95 + "quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, " 96 + "feugiat in, orci. In hac habitasse platea dictumst."; 97 private static final int SEND_NEW_SMS_SHORT = 1; 98 private static final int SEND_NEW_SMS_LONG = 2; 99 private static final int SEND_NEW_MMS_SHORT = 3; 100 private static final int SEND_NEW_MMS_LONG = 4; 101 private int mSendNewMsgCounter = 0; 102 private static final String TAG = "CAR.BLUETOOTH.KS"; 103 private static final String ACTION_MESSAGE_SENT_SUCCESSFULLY = 104 "com.google.android.car.kitchensink.bluetooth.MESSAGE_SENT_SUCCESSFULLY"; 105 private static final String ACTION_MESSAGE_DELIVERED_SUCCESSFULLY = 106 "com.google.android.car.kitchensink.bluetooth.MESSAGE_DELIVERED_SUCCESSFULLY"; 107 // {@link BluetoothMapClient.ACTION_MESSAGE_RECEIVED} is a hidden API. 108 private static final String MAP_CLIENT_ACTION_MESSAGE_RECEIVED = 109 "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED"; 110 // {@link BluetoothMapClient.EXTRA_SENDER_CONTACT_URI} is a hidden API. 111 private static final String MAP_CLIENT_EXTRA_SENDER_CONTACT_URI = 112 "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI"; 113 // {@link BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME} is a hidden API. 114 private static final String MAP_CLIENT_EXTRA_SENDER_CONTACT_NAME = 115 "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME"; 116 // {@link BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP} is a hidden API. 117 private static final String MAP_CLIENT_EXTRA_MESSAGE_TIMESTAMP = 118 "android.bluetooth.mapmce.profile.extra.MESSAGE_TIMESTAMP"; 119 // {@link BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS} is a hidden API. 120 private static final String MAP_CLIENT_EXTRA_MESSAGE_READ_STATUS = 121 "android.bluetooth.mapmce.profile.extra.MESSAGE_READ_STATUS"; 122 private static final int SEND_SMS_PERMISSIONS_REQUEST = 1; 123 BluetoothMapClient mMapProfile; 124 BluetoothAdapter mBluetoothAdapter; 125 Button mDevicePicker; 126 Button mDeviceDisconnect; 127 TextView mMessage; 128 EditText mOriginator; 129 EditText mSmsTelNum; 130 TextView mOriginatorDisplayName; 131 CheckBox mSent; 132 CheckBox mDelivered; 133 TextView mBluetoothDevice; 134 PendingIntent mSentIntent; 135 PendingIntent mDeliveredIntent; 136 NotificationReceiver mTransmissionStatusReceiver; 137 Object mLock = new Object(); 138 private Intent mSendIntent; 139 private Intent mDeliveryIntent; 140 141 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)142 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 143 @Nullable Bundle savedInstanceState) { 144 View v = inflater.inflate(R.layout.sms_received, container, false); 145 146 if (!BluetoothPermissionChecker.isPermissionGranted(getActivity(), 147 Manifest.permission.BLUETOOTH_CONNECT)) { 148 BluetoothPermissionChecker.requestPermission(Manifest.permission.BLUETOOTH_CONNECT, 149 this, 150 this::registerMapServiceListenerAndNotificationReceiver, 151 () -> { 152 Toast.makeText(getContext(), 153 "Connected devices can't be detected without BLUETOOTH_CONNECT " 154 + "permission. (You can change permissions in Settings.)", 155 Toast.LENGTH_SHORT).show(); 156 }); 157 } 158 159 Button reply = (Button) v.findViewById(R.id.reply); 160 mBluetoothDevice = (TextView) v.findViewById(R.id.bluetoothDevice); 161 Button sendNewMsgShort = (Button) v.findViewById(R.id.sms_new_message); 162 Button sendNewMsgLong = (Button) v.findViewById(R.id.mms_new_message); 163 Button resetSendNewMsgCounter = (Button) v.findViewById(R.id.reset_message_counter); 164 mSmsTelNum = (EditText) v.findViewById(R.id.sms_tel_num); 165 mOriginator = (EditText) v.findViewById(R.id.messageOriginator); 166 mOriginatorDisplayName = (TextView) v.findViewById(R.id.messageOriginatorDisplayName); 167 mSent = (CheckBox) v.findViewById(R.id.sent_checkbox); 168 mDelivered = (CheckBox) v.findViewById(R.id.delivered_checkbox); 169 mSendIntent = new Intent(ACTION_MESSAGE_SENT_SUCCESSFULLY); 170 mDeliveryIntent = new Intent(ACTION_MESSAGE_DELIVERED_SUCCESSFULLY); 171 mMessage = (TextView) v.findViewById(R.id.messageContent); 172 mDevicePicker = (Button) v.findViewById(R.id.bluetooth_pick_device); 173 mDeviceDisconnect = (Button) v.findViewById(R.id.bluetooth_disconnect_device); 174 175 //TODO add manual entry option for phone number 176 reply.setOnClickListener(new View.OnClickListener() { 177 @Override 178 public void onClick(View view) { 179 sendMessage(Collections.singleton(Uri.parse(mOriginator.getText().toString())), 180 REPLY_MESSAGE_TO_SEND); 181 } 182 }); 183 184 sendNewMsgShort.setOnClickListener(new View.OnClickListener() { 185 @Override 186 public void onClick(View view) { 187 sendNewMsgOnClick(SEND_NEW_SMS_SHORT); 188 } 189 }); 190 191 sendNewMsgLong.setOnClickListener(new View.OnClickListener() { 192 @Override 193 public void onClick(View view) { 194 sendNewMsgOnClick(SEND_NEW_MMS_LONG); 195 } 196 }); 197 198 resetSendNewMsgCounter.setOnClickListener(new View.OnClickListener() { 199 @Override 200 public void onClick(View view) { 201 mSendNewMsgCounter = 0; 202 Toast.makeText(getContext(), "Counter reset to zero.", Toast.LENGTH_SHORT).show(); 203 } 204 }); 205 206 // Pick a bluetooth device 207 mDevicePicker.setOnClickListener(new View.OnClickListener() { 208 @Override 209 public void onClick(View view) { 210 launchDevicePicker(); 211 } 212 }); 213 mDeviceDisconnect.setOnClickListener(new View.OnClickListener() { 214 @Override 215 public void onClick(View view) { 216 disconnectDevice(mBluetoothDevice.getText().toString()); 217 } 218 }); 219 220 return v; 221 } 222 launchDevicePicker()223 void launchDevicePicker() { 224 IntentFilter filter = new IntentFilter(); 225 filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED); 226 getContext().registerReceiver(mPickerReceiver, filter); 227 228 Intent intent = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); 229 intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 230 getContext().startActivity(intent); 231 } 232 disconnectDevice(String device)233 void disconnectDevice(String device) { 234 try { 235 // {@link BluetoothMapClient#disconnect} is a hidden API. 236 // {@link BluetoothMapClient#setConnectionPolicy} is the new method for connecting 237 // and disconnecting a profile. 238 mMapProfile.setConnectionPolicy(mBluetoothAdapter.getRemoteDevice(device), 239 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); 240 } catch (IllegalArgumentException e) { 241 Log.e(TAG, "Failed to disconnect from " + device, e); 242 } 243 } 244 245 @Override onResume()246 public void onResume() { 247 super.onResume(); 248 249 if (BluetoothPermissionChecker.isPermissionGranted(getActivity(), 250 Manifest.permission.BLUETOOTH_CONNECT)) { 251 registerMapServiceListenerAndNotificationReceiver(); 252 } 253 } 254 255 @Override onPause()256 public void onPause() { 257 super.onPause(); 258 259 if (mTransmissionStatusReceiver != null) { 260 getContext().unregisterReceiver(mTransmissionStatusReceiver); 261 mTransmissionStatusReceiver = null; 262 } 263 } 264 registerMapServiceListenerAndNotificationReceiver()265 private void registerMapServiceListenerAndNotificationReceiver() { 266 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 267 mBluetoothAdapter.getProfileProxy(getContext(), new MapServiceListener(), 268 BluetoothProfile.MAP_CLIENT); 269 270 mTransmissionStatusReceiver = new NotificationReceiver(); 271 IntentFilter intentFilter = new IntentFilter(); 272 intentFilter.addAction(ACTION_MESSAGE_SENT_SUCCESSFULLY); 273 intentFilter.addAction(ACTION_MESSAGE_DELIVERED_SUCCESSFULLY); 274 intentFilter.addAction(MAP_CLIENT_ACTION_MESSAGE_RECEIVED); 275 getContext().registerReceiver(mTransmissionStatusReceiver, intentFilter, 276 Context.RECEIVER_NOT_EXPORTED); 277 278 intentFilter = new IntentFilter(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); 279 getContext().registerReceiver(mTransmissionStatusReceiver, intentFilter); 280 } 281 sendNewMsgOnClick(int msgType)282 private void sendNewMsgOnClick(int msgType) { 283 String messageToSend = ""; 284 switch (msgType) { 285 case SEND_NEW_SMS_SHORT: 286 messageToSend = NEW_MESSAGE_TO_SEND_SHORT; 287 break; 288 case SEND_NEW_MMS_LONG: 289 messageToSend = NEW_MESSAGE_TO_SEND_LONG; 290 break; 291 default: 292 break; 293 } 294 String s = mSmsTelNum.getText().toString(); 295 Toast.makeText(getContext(), "sending msg to " + s, Toast.LENGTH_SHORT).show(); 296 HashSet<Uri> uris = new HashSet<Uri>(); 297 Uri.Builder builder = new Uri.Builder(); 298 for (String telNum : s.split(",")) { 299 uris.add(builder.path(telNum).scheme(PhoneAccount.SCHEME_TEL).build()); 300 } 301 sendMessage(uris, Integer.toString(mSendNewMsgCounter) + ": " + messageToSend); 302 mSendNewMsgCounter += 1; 303 } 304 sendMessage(Collection recipients, String message)305 private void sendMessage(Collection recipients, String message) { 306 if (getActivity().checkSelfPermission(Manifest.permission.SEND_SMS) 307 != PackageManager.PERMISSION_GRANTED) { 308 Log.d(TAG,"Don't have SMS permission in kitchesink app. Requesting it"); 309 getActivity().requestPermissions(new String[]{Manifest.permission.SEND_SMS}, 310 SEND_SMS_PERMISSIONS_REQUEST); 311 Toast.makeText(getContext(), "Try again after granting SEND_SMS perm!", 312 Toast.LENGTH_SHORT).show(); 313 return; 314 } 315 synchronized (mLock) { 316 BluetoothDevice remoteDevice; 317 try { 318 remoteDevice = mBluetoothAdapter.getRemoteDevice( 319 mBluetoothDevice.getText().toString()); 320 } catch (java.lang.IllegalArgumentException e) { 321 Log.e(TAG, e.toString()); 322 return; 323 } 324 mSent.setChecked(false); 325 mDelivered.setChecked(false); 326 if (mMapProfile != null) { 327 Log.d(TAG, "Sending reply"); 328 if (recipients == null) { 329 Log.d(TAG, "Recipients is null"); 330 return; 331 } 332 if (mBluetoothDevice == null) { 333 Log.d(TAG, "BluetoothDevice is null"); 334 return; 335 } 336 337 mSentIntent = PendingIntent.getBroadcast(getContext(), 0, mSendIntent, 338 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 339 mDeliveredIntent = PendingIntent.getBroadcast(getContext(), 0, mDeliveryIntent, 340 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 341 Log.d(TAG,"Sending message in kitchesink app: " + message); 342 mMapProfile.sendMessage( 343 remoteDevice, 344 recipients, message, mSentIntent, mDeliveredIntent); 345 } 346 } 347 } 348 349 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)350 public void onRequestPermissionsResult(int requestCode, String[] permissions, 351 int[] grantResults) { 352 Log.d(TAG, "onRequestPermissionsResult reqCode=" + requestCode); 353 if (SEND_SMS_PERMISSIONS_REQUEST == requestCode) { 354 for (int i=0; i<permissions.length; i++) { 355 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { 356 if (permissions[i] == Manifest.permission.SEND_SMS) { 357 Log.d(TAG, "Got the SEND_SMS perm"); 358 return; 359 } 360 } 361 } 362 } 363 } 364 365 class MapServiceListener implements BluetoothProfile.ServiceListener { 366 @Override onServiceConnected(int profile, BluetoothProfile proxy)367 public void onServiceConnected(int profile, BluetoothProfile proxy) { 368 synchronized (mLock) { 369 mMapProfile = (BluetoothMapClient) proxy; 370 List<BluetoothDevice> connectedDevices = proxy.getConnectedDevices(); 371 if (connectedDevices.size() > 0) { 372 mBluetoothDevice.setText(connectedDevices.get(0).getAddress()); 373 } 374 } 375 } 376 377 @Override onServiceDisconnected(int profile)378 public void onServiceDisconnected(int profile) { 379 synchronized (mLock) { 380 mMapProfile = null; 381 } 382 } 383 } 384 385 private class NotificationReceiver extends BroadcastReceiver { 386 @Override onReceive(Context context, Intent intent)387 public void onReceive(Context context, Intent intent) { 388 String action = intent.getAction(); 389 synchronized (mLock) { 390 if (action.equals(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED)) { 391 if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0) 392 == BluetoothProfile.STATE_CONNECTED) { 393 mBluetoothDevice.setText(((BluetoothDevice) intent.getParcelableExtra( 394 BluetoothDevice.EXTRA_DEVICE)).getAddress()); 395 } else if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0) 396 == BluetoothProfile.STATE_DISCONNECTED) { 397 mBluetoothDevice.setText("Disconnected"); 398 } 399 } else if (Objects.equals(action, ACTION_MESSAGE_SENT_SUCCESSFULLY)) { 400 mSent.setChecked(true); 401 } else if (Objects.equals(action, ACTION_MESSAGE_DELIVERED_SUCCESSFULLY)) { 402 mDelivered.setChecked(true); 403 } else if (Objects.equals(action, MAP_CLIENT_ACTION_MESSAGE_RECEIVED)) { 404 String senderUri = 405 intent.getStringExtra(MAP_CLIENT_EXTRA_SENDER_CONTACT_URI); 406 if (senderUri == null) { 407 senderUri = "<null>"; 408 } 409 410 String senderName = intent.getStringExtra( 411 MAP_CLIENT_EXTRA_SENDER_CONTACT_NAME); 412 if (senderName == null) { 413 senderName = "<null>"; 414 } 415 Date msgTimestamp = new Date(intent.getLongExtra( 416 MAP_CLIENT_EXTRA_MESSAGE_TIMESTAMP, 417 System.currentTimeMillis())); 418 boolean msgReadStatus = intent.getBooleanExtra( 419 MAP_CLIENT_EXTRA_MESSAGE_READ_STATUS, false); 420 String msgText = intent.getStringExtra(android.content.Intent.EXTRA_TEXT); 421 String msg = "[" + msgTimestamp + "] " + "(" 422 + (msgReadStatus ? "READ" : "UNREAD") + ") " + msgText; 423 mMessage.setText(msg); 424 mOriginator.setText(senderUri); 425 mOriginatorDisplayName.setText(senderName); 426 } 427 } 428 } 429 } 430 431 private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() { 432 @Override 433 public void onReceive(Context context, Intent intent) { 434 String action = intent.getAction(); 435 436 Log.v(TAG, "mPickerReceiver got " + action); 437 438 if (Objects.equals(action, BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) { 439 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 440 Log.v(TAG, "mPickerReceiver got " + device); 441 if (device == null) { 442 Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show(); 443 return; 444 } 445 // {@link BluetoothMapClient#connect} is a hidden API. 446 // {@link BluetoothMapClient#setConnectionPolicy} is the new method for connecting 447 // and disconnecting a profile. 448 mMapProfile.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); 449 450 // The receiver can now be disabled. 451 getContext().unregisterReceiver(mPickerReceiver); 452 } 453 } 454 }; 455 } 456