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