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