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 int SEND_SMS_PERMISSIONS_REQUEST = 1;
102     BluetoothMapClient mMapProfile;
103     BluetoothAdapter mBluetoothAdapter;
104     Button mDevicePicker;
105     Button mDeviceDisconnect;
106     TextView mMessage;
107     EditText mOriginator;
108     EditText mSmsTelNum;
109     TextView mOriginatorDisplayName;
110     CheckBox mSent;
111     CheckBox mDelivered;
112     TextView mBluetoothDevice;
113     PendingIntent mSentIntent;
114     PendingIntent mDeliveredIntent;
115     NotificationReceiver mTransmissionStatusReceiver;
116     Object mLock = new Object();
117     private KitchenSinkActivity mActivity;
118     private Intent mSendIntent;
119     private Intent mDeliveryIntent;
120     EditText mUploadingSupportedFeatureText;
121 
122     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)123     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
124             @Nullable Bundle savedInstanceState) {
125         View v = inflater.inflate(R.layout.sms_received, container, false);
126         mActivity = (KitchenSinkActivity) getHost();
127         Button reply = (Button) v.findViewById(R.id.reply);
128         Button checkMessages = (Button) v.findViewById(R.id.check_messages);
129         mBluetoothDevice = (TextView) v.findViewById(R.id.bluetoothDevice);
130         Button sendNewMsgShort = (Button) v.findViewById(R.id.sms_new_message);
131         Button sendNewMsgLong = (Button) v.findViewById(R.id.mms_new_message);
132         Button resetSendNewMsgCounter = (Button) v.findViewById(R.id.reset_message_counter);
133         mSmsTelNum = (EditText) v.findViewById(R.id.sms_tel_num);
134         mOriginator = (EditText) v.findViewById(R.id.messageOriginator);
135         mOriginatorDisplayName = (TextView) v.findViewById(R.id.messageOriginatorDisplayName);
136         mSent = (CheckBox) v.findViewById(R.id.sent_checkbox);
137         mDelivered = (CheckBox) v.findViewById(R.id.delivered_checkbox);
138         mSendIntent = new Intent(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
139         mDeliveryIntent = new Intent(BluetoothMapClient.ACTION_MESSAGE_DELIVERED_SUCCESSFULLY);
140         mMessage = (TextView) v.findViewById(R.id.messageContent);
141         mDevicePicker = (Button) v.findViewById(R.id.bluetooth_pick_device);
142         mDeviceDisconnect = (Button) v.findViewById(R.id.bluetooth_disconnect_device);
143         Button uploadingFeatureValue = (Button) v.findViewById(R.id.uploading_supported_feature);
144         mUploadingSupportedFeatureText =
145             (EditText) v.findViewById(R.id.uploading_supported_feature_value);
146 
147         uploadingFeatureValue.setOnClickListener(new View.OnClickListener() {
148             @Override
149             public void onClick(View view) {
150                 int value = getUploadingFeatureValue();
151                 mUploadingSupportedFeatureText.setText(value + "");
152             }
153         });
154 
155         //TODO add manual entry option for phone number
156         reply.setOnClickListener(new View.OnClickListener() {
157             @Override
158             public void onClick(View view) {
159                 sendMessage(new Uri[]{Uri.parse(mOriginator.getText().toString())},
160                         REPLY_MESSAGE_TO_SEND);
161             }
162         });
163 
164         sendNewMsgShort.setOnClickListener(new View.OnClickListener() {
165             @Override
166             public void onClick(View view) {
167                 sendNewMsgOnClick(SEND_NEW_SMS_SHORT);
168             }
169         });
170 
171         sendNewMsgLong.setOnClickListener(new View.OnClickListener() {
172             @Override
173             public void onClick(View view) {
174                 sendNewMsgOnClick(SEND_NEW_MMS_LONG);
175             }
176         });
177 
178         resetSendNewMsgCounter.setOnClickListener(new View.OnClickListener() {
179             @Override
180             public void onClick(View view) {
181                 mSendNewMsgCounter = 0;
182                 Toast.makeText(getContext(), "Counter reset to zero.", Toast.LENGTH_SHORT).show();
183             }
184         });
185 
186         checkMessages.setOnClickListener(new View.OnClickListener() {
187             @Override
188             public void onClick(View view) {
189                 getMessages();
190             }
191         });
192 
193         // Pick a bluetooth device
194         mDevicePicker.setOnClickListener(new View.OnClickListener() {
195             @Override
196             public void onClick(View view) {
197                 launchDevicePicker();
198             }
199         });
200         mDeviceDisconnect.setOnClickListener(new View.OnClickListener() {
201             @Override
202             public void onClick(View view) {
203                 disconnectDevice(mBluetoothDevice.getText().toString());
204             }
205         });
206 
207         mTransmissionStatusReceiver = new NotificationReceiver();
208         return v;
209     }
210 
launchDevicePicker()211     void launchDevicePicker() {
212         IntentFilter filter = new IntentFilter();
213         filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
214         getContext().registerReceiver(mPickerReceiver, filter);
215 
216         Intent intent = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
217         intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
218         getContext().startActivity(intent);
219     }
220 
disconnectDevice(String device)221     void disconnectDevice(String device) {
222         try {
223             mMapProfile.disconnect(mBluetoothAdapter.getRemoteDevice(device));
224         } catch (IllegalArgumentException e) {
225             Log.e(TAG, "Failed to disconnect from " + device, e);
226         }
227     }
228 
229     @Override
onResume()230     public void onResume() {
231         super.onResume();
232 
233         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
234         mBluetoothAdapter.getProfileProxy(getContext(), new MapServiceListener(),
235                 BluetoothProfile.MAP_CLIENT);
236 
237         IntentFilter intentFilter = new IntentFilter();
238         intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
239         intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_DELIVERED_SUCCESSFULLY);
240         intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
241         intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
242         getContext().registerReceiver(mTransmissionStatusReceiver, intentFilter);
243     }
244 
245     @Override
onPause()246     public void onPause() {
247         super.onPause();
248         getContext().unregisterReceiver(mTransmissionStatusReceiver);
249     }
250 
getMessages()251     private void getMessages() {
252         synchronized (mLock) {
253             BluetoothDevice remoteDevice;
254             try {
255                 remoteDevice = mBluetoothAdapter.getRemoteDevice(
256                         mBluetoothDevice.getText().toString());
257             } catch (java.lang.IllegalArgumentException e) {
258                 Log.e(TAG, e.toString());
259                 return;
260             }
261 
262             if (mMapProfile != null) {
263                 Log.d(TAG, "Getting Messages");
264                 mMapProfile.getUnreadMessages(remoteDevice);
265             }
266         }
267     }
268 
sendNewMsgOnClick(int msgType)269     private void sendNewMsgOnClick(int msgType) {
270         String messageToSend = "";
271         switch (msgType) {
272             case SEND_NEW_SMS_SHORT:
273                 messageToSend = NEW_MESSAGE_TO_SEND_SHORT;
274                 break;
275             case SEND_NEW_MMS_LONG:
276                 messageToSend = NEW_MESSAGE_TO_SEND_LONG;
277                 break;
278         }
279         String s = mSmsTelNum.getText().toString();
280         Toast.makeText(getContext(), "sending msg to " + s, Toast.LENGTH_SHORT).show();
281         HashSet<Uri> uris = new HashSet<Uri>();
282         Uri.Builder builder = new Uri.Builder();
283         for (String telNum : s.split(",")) {
284             uris.add(builder.path(telNum).scheme(PhoneAccount.SCHEME_TEL).build());
285         }
286         sendMessage(uris.toArray(new Uri[uris.size()]), Integer.toString(mSendNewMsgCounter)
287                 + ":  " + messageToSend);
288         mSendNewMsgCounter += 1;
289     }
290 
getUploadingFeatureValue()291     private int getUploadingFeatureValue() {
292         synchronized (mLock) {
293             BluetoothDevice remoteDevice;
294             try {
295                 remoteDevice = mBluetoothAdapter.getRemoteDevice(
296                         mBluetoothDevice.getText().toString());
297             } catch (java.lang.IllegalArgumentException e) {
298                 Log.e(TAG, e.toString());
299                 return -1;
300             }
301 
302             if (mMapProfile != null) {
303                 Log.d(TAG, "getUploadingFeatureValue");
304                 return (mMapProfile.isUploadingSupported(remoteDevice)) ? 1 : 0;
305             }
306             return -1;
307         }
308     }
309 
sendMessage(Uri[] recipients, String message)310     private void sendMessage(Uri[] recipients, String message) {
311         if (mActivity.checkSelfPermission(Manifest.permission.SEND_SMS)
312                 != PackageManager.PERMISSION_GRANTED) {
313             Log.d(TAG,"Don't have SMS permission in kitchesink app. Requesting it");
314             mActivity.requestPermissions(new String[]{Manifest.permission.SEND_SMS},
315                     SEND_SMS_PERMISSIONS_REQUEST);
316             Toast.makeText(getContext(), "Try again after granting SEND_SMS perm!",
317                     Toast.LENGTH_SHORT).show();
318             return;
319         }
320         synchronized (mLock) {
321             BluetoothDevice remoteDevice;
322             try {
323                 remoteDevice = mBluetoothAdapter.getRemoteDevice(
324                         mBluetoothDevice.getText().toString());
325             } catch (java.lang.IllegalArgumentException e) {
326                 Log.e(TAG, e.toString());
327                 return;
328             }
329             mSent.setChecked(false);
330             mDelivered.setChecked(false);
331             if (mMapProfile != null) {
332                 Log.d(TAG, "Sending reply");
333                 if (recipients == null) {
334                     Log.d(TAG, "Recipients is null");
335                     return;
336                 }
337                 if (mBluetoothDevice == null) {
338                     Log.d(TAG, "BluetoothDevice is null");
339                     return;
340                 }
341 
342                 mSentIntent = PendingIntent.getBroadcast(getContext(), 0, mSendIntent,
343                         PendingIntent.FLAG_ONE_SHOT);
344                 mDeliveredIntent = PendingIntent.getBroadcast(getContext(), 0, mDeliveryIntent,
345                         PendingIntent.FLAG_ONE_SHOT);
346                 Log.d(TAG,"Sending message in kitchesink app: " + message);
347                 mMapProfile.sendMessage(
348                         remoteDevice,
349                         recipients, message, mSentIntent, mDeliveredIntent);
350             }
351         }
352     }
353 
354     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)355     public void onRequestPermissionsResult(int requestCode, String[] permissions,
356             int[] grantResults) {
357         Log.d(TAG, "onRequestPermissionsResult reqCode=" + requestCode);
358         if (SEND_SMS_PERMISSIONS_REQUEST == requestCode) {
359             for (int i=0; i<permissions.length; i++) {
360                 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
361                     if (permissions[i] == Manifest.permission.SEND_SMS) {
362                         Log.d(TAG, "Got the SEND_SMS perm");
363                         return;
364                     }
365                 }
366             }
367         }
368     }
369 
370     class MapServiceListener implements BluetoothProfile.ServiceListener {
371         @Override
onServiceConnected(int profile, BluetoothProfile proxy)372         public void onServiceConnected(int profile, BluetoothProfile proxy) {
373             synchronized (mLock) {
374                 mMapProfile = (BluetoothMapClient) proxy;
375                 List<BluetoothDevice> connectedDevices = proxy.getConnectedDevices();
376                 if (connectedDevices.size() > 0) {
377                     mBluetoothDevice.setText(connectedDevices.get(0).getAddress());
378                 }
379             }
380         }
381 
382         @Override
onServiceDisconnected(int profile)383         public void onServiceDisconnected(int profile) {
384             synchronized (mLock) {
385                 mMapProfile = null;
386             }
387         }
388     }
389 
390     private class NotificationReceiver extends BroadcastReceiver {
391         @Override
onReceive(Context context, Intent intent)392         public void onReceive(Context context, Intent intent) {
393             String action = intent.getAction();
394             synchronized (mLock) {
395                 if (action.equals(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED)) {
396                     if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0)
397                             == BluetoothProfile.STATE_CONNECTED) {
398                         mBluetoothDevice.setText(((BluetoothDevice) intent.getParcelableExtra(
399                                 BluetoothDevice.EXTRA_DEVICE)).getAddress());
400                     } else if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0)
401                             == BluetoothProfile.STATE_DISCONNECTED) {
402                         mBluetoothDevice.setText("Disconnected");
403                     }
404                 } else if (action.equals(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY)) {
405                     mSent.setChecked(true);
406                 } else if (action.equals(
407                         BluetoothMapClient.ACTION_MESSAGE_DELIVERED_SUCCESSFULLY)) {
408                     mDelivered.setChecked(true);
409                 } else if (action.equals(BluetoothMapClient.ACTION_MESSAGE_RECEIVED)) {
410                     String senderUri =
411                             intent.getStringExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI);
412                     if (senderUri == null) {
413                         senderUri = "<null>";
414                     }
415 
416                     String senderName = intent.getStringExtra(
417                             BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME);
418                     if (senderName == null) {
419                         senderName = "<null>";
420                     }
421                     Date msgTimestamp = new Date(intent.getLongExtra(
422                             BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP,
423                             System.currentTimeMillis()));
424                     boolean msgReadStatus = intent.getBooleanExtra(
425                             BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS, false);
426                     String msgText = intent.getStringExtra(android.content.Intent.EXTRA_TEXT);
427                     String msg = "[" + msgTimestamp + "] " + "("
428                             + (msgReadStatus ? "READ" : "UNREAD") + ") " + msgText;
429                     mMessage.setText(msg);
430                     mOriginator.setText(senderUri);
431                     mOriginatorDisplayName.setText(senderName);
432                 }
433             }
434         }
435     }
436 
437     private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() {
438         @Override
439         public void onReceive(Context context, Intent intent) {
440             String action = intent.getAction();
441 
442             Log.v(TAG, "mPickerReceiver got " + action);
443 
444             if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
445                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
446                 Log.v(TAG, "mPickerReceiver got " + device);
447                 if (device == null) {
448                     Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
449                     return;
450                 }
451                 mMapProfile.connect(device);
452 
453                 // The receiver can now be disabled.
454                 getContext().unregisterReceiver(mPickerReceiver);
455             }
456         }
457     };
458 }
459