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