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.Collection; 53 import java.util.Collections; 54 import java.util.Date; 55 import java.util.HashSet; 56 import java.util.List; 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 KitchenSinkActivity mActivity; 139 private Intent mSendIntent; 140 private Intent mDeliveryIntent; 141 142 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)143 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 144 @Nullable Bundle savedInstanceState) { 145 View v = inflater.inflate(R.layout.sms_received, container, false); 146 mActivity = (KitchenSinkActivity) getHost(); 147 148 if (!BluetoothPermissionChecker.isPermissionGranted(mActivity, 149 Manifest.permission.BLUETOOTH_CONNECT)) { 150 BluetoothPermissionChecker.requestPermission(Manifest.permission.BLUETOOTH_CONNECT, 151 this, 152 this::registerMapServiceListenerAndNotificationReceiver, 153 () -> { 154 Toast.makeText(getContext(), 155 "Connected devices can't be detected without BLUETOOTH_CONNECT " 156 + "permission. (You can change permissions in Settings.)", 157 Toast.LENGTH_SHORT).show(); 158 }); 159 } 160 161 Button reply = (Button) v.findViewById(R.id.reply); 162 mBluetoothDevice = (TextView) v.findViewById(R.id.bluetoothDevice); 163 Button sendNewMsgShort = (Button) v.findViewById(R.id.sms_new_message); 164 Button sendNewMsgLong = (Button) v.findViewById(R.id.mms_new_message); 165 Button resetSendNewMsgCounter = (Button) v.findViewById(R.id.reset_message_counter); 166 mSmsTelNum = (EditText) v.findViewById(R.id.sms_tel_num); 167 mOriginator = (EditText) v.findViewById(R.id.messageOriginator); 168 mOriginatorDisplayName = (TextView) v.findViewById(R.id.messageOriginatorDisplayName); 169 mSent = (CheckBox) v.findViewById(R.id.sent_checkbox); 170 mDelivered = (CheckBox) v.findViewById(R.id.delivered_checkbox); 171 mSendIntent = new Intent(ACTION_MESSAGE_SENT_SUCCESSFULLY); 172 mDeliveryIntent = new Intent(ACTION_MESSAGE_DELIVERED_SUCCESSFULLY); 173 mMessage = (TextView) v.findViewById(R.id.messageContent); 174 mDevicePicker = (Button) v.findViewById(R.id.bluetooth_pick_device); 175 mDeviceDisconnect = (Button) v.findViewById(R.id.bluetooth_disconnect_device); 176 177 //TODO add manual entry option for phone number 178 reply.setOnClickListener(new View.OnClickListener() { 179 @Override 180 public void onClick(View view) { 181 sendMessage(Collections.singleton(Uri.parse(mOriginator.getText().toString())), 182 REPLY_MESSAGE_TO_SEND); 183 } 184 }); 185 186 sendNewMsgShort.setOnClickListener(new View.OnClickListener() { 187 @Override 188 public void onClick(View view) { 189 sendNewMsgOnClick(SEND_NEW_SMS_SHORT); 190 } 191 }); 192 193 sendNewMsgLong.setOnClickListener(new View.OnClickListener() { 194 @Override 195 public void onClick(View view) { 196 sendNewMsgOnClick(SEND_NEW_MMS_LONG); 197 } 198 }); 199 200 resetSendNewMsgCounter.setOnClickListener(new View.OnClickListener() { 201 @Override 202 public void onClick(View view) { 203 mSendNewMsgCounter = 0; 204 Toast.makeText(getContext(), "Counter reset to zero.", Toast.LENGTH_SHORT).show(); 205 } 206 }); 207 208 // Pick a bluetooth device 209 mDevicePicker.setOnClickListener(new View.OnClickListener() { 210 @Override 211 public void onClick(View view) { 212 launchDevicePicker(); 213 } 214 }); 215 mDeviceDisconnect.setOnClickListener(new View.OnClickListener() { 216 @Override 217 public void onClick(View view) { 218 disconnectDevice(mBluetoothDevice.getText().toString()); 219 } 220 }); 221 222 return v; 223 } 224 launchDevicePicker()225 void launchDevicePicker() { 226 IntentFilter filter = new IntentFilter(); 227 filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED); 228 getContext().registerReceiver(mPickerReceiver, filter); 229 230 Intent intent = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); 231 intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 232 getContext().startActivity(intent); 233 } 234 disconnectDevice(String device)235 void disconnectDevice(String device) { 236 try { 237 // {@link BluetoothMapClient#disconnect} is a hidden API. 238 // {@link BluetoothMapClient#setConnectionPolicy} is the new method for connecting 239 // and disconnecting a profile. 240 mMapProfile.setConnectionPolicy(mBluetoothAdapter.getRemoteDevice(device), 241 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); 242 } catch (IllegalArgumentException e) { 243 Log.e(TAG, "Failed to disconnect from " + device, e); 244 } 245 } 246 247 @Override onResume()248 public void onResume() { 249 super.onResume(); 250 251 if (BluetoothPermissionChecker.isPermissionGranted(mActivity, 252 Manifest.permission.BLUETOOTH_CONNECT)) { 253 registerMapServiceListenerAndNotificationReceiver(); 254 } 255 } 256 257 @Override onPause()258 public void onPause() { 259 super.onPause(); 260 261 if (mTransmissionStatusReceiver != null) { 262 getContext().unregisterReceiver(mTransmissionStatusReceiver); 263 mTransmissionStatusReceiver = null; 264 } 265 } 266 registerMapServiceListenerAndNotificationReceiver()267 private void registerMapServiceListenerAndNotificationReceiver() { 268 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 269 mBluetoothAdapter.getProfileProxy(getContext(), new MapServiceListener(), 270 BluetoothProfile.MAP_CLIENT); 271 272 mTransmissionStatusReceiver = new NotificationReceiver(); 273 IntentFilter intentFilter = new IntentFilter(); 274 intentFilter.addAction(ACTION_MESSAGE_SENT_SUCCESSFULLY); 275 intentFilter.addAction(ACTION_MESSAGE_DELIVERED_SUCCESSFULLY); 276 intentFilter.addAction(MAP_CLIENT_ACTION_MESSAGE_RECEIVED); 277 getContext().registerReceiver(mTransmissionStatusReceiver, intentFilter, 278 Context.RECEIVER_NOT_EXPORTED); 279 280 intentFilter = new IntentFilter(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); 281 getContext().registerReceiver(mTransmissionStatusReceiver, intentFilter); 282 } 283 sendNewMsgOnClick(int msgType)284 private void sendNewMsgOnClick(int msgType) { 285 String messageToSend = ""; 286 switch (msgType) { 287 case SEND_NEW_SMS_SHORT: 288 messageToSend = NEW_MESSAGE_TO_SEND_SHORT; 289 break; 290 case SEND_NEW_MMS_LONG: 291 messageToSend = NEW_MESSAGE_TO_SEND_LONG; 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 (mActivity.checkSelfPermission(Manifest.permission.SEND_SMS) 307 != PackageManager.PERMISSION_GRANTED) { 308 Log.d(TAG,"Don't have SMS permission in kitchesink app. Requesting it"); 309 mActivity.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 (action.equals(ACTION_MESSAGE_SENT_SUCCESSFULLY)) { 400 mSent.setChecked(true); 401 } else if (action.equals(ACTION_MESSAGE_DELIVERED_SUCCESSFULLY)) { 402 mDelivered.setChecked(true); 403 } else if (action.equals(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 (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) { 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