• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.android.car.messenger;
18 
19 import android.app.NotificationManager;
20 import android.app.PendingIntent;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothMapClient;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.net.Uri;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.support.annotation.Nullable;
32 import android.support.v4.app.NotificationCompat;
33 import android.util.Log;
34 import android.widget.Toast;
35 
36 import java.util.HashMap;
37 import java.util.Iterator;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.function.Predicate;
43 
44 /**
45  * Component that monitors for incoming messages and posts/updates notifications.
46  * <p>
47  * It also handles notifications requests e.g. sending auto-replies and message play-out.
48  * <p>
49  * It will receive broadcasts for new incoming messages as long as the MapClient is connected in
50  * {@link MessengerService}.
51  */
52 class MapMessageMonitor {
53     private static final String TAG = "Messenger.MsgMonitor";
54     private static final boolean DBG = MessengerService.DBG;
55 
56     private final Context mContext;
57     private final BluetoothMapReceiver mBluetoothMapReceiver;
58     private final NotificationManager mNotificationManager;
59     private final Map<MessageKey, MapMessage> mMessages = new HashMap<>();
60     private final Map<SenderKey, NotificationInfo> mNotificationInfos = new HashMap<>();
61     private final TTSHelper mTTSHelper;
62 
MapMessageMonitor(Context context)63     MapMessageMonitor(Context context) {
64         mContext = context;
65         mBluetoothMapReceiver = new BluetoothMapReceiver();
66         mNotificationManager =
67                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
68         mTTSHelper = new TTSHelper(mContext);
69     }
70 
handleNewMessage(Intent intent)71     private void handleNewMessage(Intent intent) {
72         if (DBG) {
73             Log.d(TAG, "Handling new message");
74         }
75         try {
76             MapMessage message = MapMessage.parseFrom(intent);
77             if (MessengerService.VDBG) {
78                 Log.v(TAG, "Parsed message: " + message);
79             }
80             MessageKey messageKey = new MessageKey(message);
81             boolean repeatMessage = mMessages.containsKey(messageKey);
82             mMessages.put(messageKey, message);
83             if (!repeatMessage) {
84                 updateNotificationInfo(message, messageKey);
85             }
86         } catch (IllegalArgumentException e) {
87             Log.e(TAG, "Dropping invalid MAP message", e);
88         }
89     }
90 
updateNotificationInfo(MapMessage message, MessageKey messageKey)91     private void updateNotificationInfo(MapMessage message, MessageKey messageKey) {
92         SenderKey senderKey = new SenderKey(message);
93         NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
94         if (notificationInfo == null) {
95             notificationInfo =
96                     new NotificationInfo(message.getSenderName(), message.getSenderContactUri());
97             mNotificationInfos.put(senderKey, notificationInfo);
98         }
99         notificationInfo.mMessageKeys.add(messageKey);
100         updateNotificationFor(senderKey, notificationInfo, false /* ttsPlaying */);
101     }
102 
updateNotificationFor(SenderKey senderKey, NotificationInfo notificationInfo, boolean ttsPlaying)103     private void updateNotificationFor(SenderKey senderKey,
104             NotificationInfo notificationInfo, boolean ttsPlaying) {
105         NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
106         // TODO(sriniv): Use right icon when switching to correct layout. b/33280056.
107         builder.setSmallIcon(android.R.drawable.btn_plus);
108         builder.setContentTitle(notificationInfo.mSenderName);
109         builder.setContentText(mContext.getResources().getQuantityString(
110                 R.plurals.notification_new_message, notificationInfo.mMessageKeys.size(),
111                 notificationInfo.mMessageKeys.size()));
112 
113         Intent deleteIntent = new Intent(mContext, MessengerService.class)
114                 .setAction(MessengerService.ACTION_CLEAR_NOTIFICATION_STATE)
115                 .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey);
116         builder.setDeleteIntent(
117                 PendingIntent.getService(mContext, notificationInfo.mNotificationId, deleteIntent,
118                         PendingIntent.FLAG_UPDATE_CURRENT));
119 
120         String messageActions[] = {
121                 MessengerService.ACTION_AUTO_REPLY,
122                 MessengerService.ACTION_PLAY_MESSAGES
123         };
124         // TODO(sriniv): Actual spec does not have any of these strings. Remove later. b/33280056.
125         // is implemented for notifications.
126         String actionTexts[] = { "Reply", "Play" };
127         if (ttsPlaying) {
128             messageActions[1] = MessengerService.ACTION_STOP_PLAYOUT;
129             actionTexts[1] = "Stop";
130         }
131         for (int i = 0; i < messageActions.length; i++) {
132             Intent intent = new Intent(mContext, MessengerService.class)
133                     .setAction(messageActions[i])
134                     .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey);
135             PendingIntent pendingIntent = PendingIntent.getService(mContext,
136                     notificationInfo.mNotificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
137             builder.addAction(android.R.drawable.ic_media_play, actionTexts[i], pendingIntent);
138         }
139         mNotificationManager.notify(notificationInfo.mNotificationId, builder.build());
140     }
141 
clearNotificationState(SenderKey senderKey)142     void clearNotificationState(SenderKey senderKey) {
143         if (DBG) {
144             Log.d(TAG, "Clearing notification state for: " + senderKey);
145         }
146         mNotificationInfos.remove(senderKey);
147     }
148 
playMessages(SenderKey senderKey)149     void playMessages(SenderKey senderKey) {
150         NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
151         if (notificationInfo == null) {
152             Log.e(TAG, "Unknown senderKey! " + senderKey);
153             return;
154         }
155 
156         StringBuilder ttsMessage = new StringBuilder();
157         ttsMessage.append(notificationInfo.mSenderName)
158                 .append(" ").append(mContext.getString(R.string.tts_says_verb));
159         for (MessageKey messageKey : notificationInfo.mMessageKeys) {
160             MapMessage message = mMessages.get(messageKey);
161             if (message != null) {
162                 ttsMessage.append(". ").append(message.getText());
163             }
164         }
165 
166         mTTSHelper.requestPlay(ttsMessage.toString(),
167                 new TTSHelper.Listener() {
168             @Override
169             public void onTTSStarted() {
170                 updateNotificationFor(senderKey, notificationInfo, true);
171             }
172 
173             @Override
174             public void onTTSStopped() {
175                 updateNotificationFor(senderKey, notificationInfo, false);
176             }
177 
178             @Override
179             public void onTTSError() {
180                 Toast.makeText(mContext, R.string.tts_failed_toast, Toast.LENGTH_SHORT).show();
181                 onTTSStopped();
182             }
183         });
184     }
185 
stopPlayout()186     void stopPlayout() {
187         mTTSHelper.requestStop();
188     }
189 
sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient)190     boolean sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient) {
191         if (DBG) {
192             Log.d(TAG, "Sending auto-reply to: " + senderKey);
193         }
194         BluetoothDevice device =
195                 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(senderKey.mDeviceAddress);
196         NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
197         if (notificationInfo == null) {
198             Log.w(TAG, "No notificationInfo found for senderKey: " + senderKey);
199             return false;
200         }
201         if (notificationInfo.mSenderContactUri == null) {
202             Log.w(TAG, "Do not have contact URI for sender!");
203             return false;
204         }
205         Uri recipientUris[] = { Uri.parse(notificationInfo.mSenderContactUri) };
206 
207         final int requestCode = senderKey.hashCode();
208         PendingIntent sentIntent =
209                 PendingIntent.getBroadcast(mContext, requestCode, new Intent(
210                         BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY),
211                         PendingIntent.FLAG_ONE_SHOT);
212         String message = mContext.getString(R.string.auto_reply_message);
213         return mapClient.sendMessage(device, recipientUris, message, sentIntent, null);
214     }
215 
handleMapDisconnect()216     void handleMapDisconnect() {
217         cleanupMessagesAndNotifications((key) -> true);
218     }
219 
handleDeviceDisconnect(BluetoothDevice device)220     void handleDeviceDisconnect(BluetoothDevice device) {
221         cleanupMessagesAndNotifications((key) -> key.matches(device.getAddress()));
222     }
223 
cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate)224     private void cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate) {
225         Iterator<Map.Entry<MessageKey, MapMessage>> messageIt = mMessages.entrySet().iterator();
226         while (messageIt.hasNext()) {
227             if (predicate.test(messageIt.next().getKey())) {
228                 messageIt.remove();
229             }
230         }
231         Iterator<Map.Entry<SenderKey, NotificationInfo>> notificationIt =
232                 mNotificationInfos.entrySet().iterator();
233         while (notificationIt.hasNext()) {
234             Map.Entry<SenderKey, NotificationInfo> entry = notificationIt.next();
235             if (predicate.test(entry.getKey())) {
236                 mNotificationManager.cancel(entry.getValue().mNotificationId);
237                 notificationIt.remove();
238             }
239         }
240     }
241 
cleanup()242     void cleanup() {
243         mBluetoothMapReceiver.cleanup();
244         mTTSHelper.cleanup();
245     }
246 
247     // Used to monitor for new incoming messages and sent-message broadcast.
248     private class BluetoothMapReceiver extends BroadcastReceiver {
BluetoothMapReceiver()249         BluetoothMapReceiver() {
250             if (DBG) {
251                 Log.d(TAG, "Registering receiver for new messages");
252             }
253             IntentFilter intentFilter = new IntentFilter();
254             intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
255             intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
256             mContext.registerReceiver(this, intentFilter);
257         }
258 
cleanup()259         void cleanup() {
260             mContext.unregisterReceiver(this);
261         }
262 
263         @Override
onReceive(Context context, Intent intent)264         public void onReceive(Context context, Intent intent) {
265             if (BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY.equals(intent.getAction())) {
266                 if (DBG) {
267                     Log.d(TAG, "SMS was sent successfully!");
268                 }
269             } else if (BluetoothMapClient.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
270                 handleNewMessage(intent);
271             } else {
272                 Log.w(TAG, "Ignoring unknown broadcast " + intent.getAction());
273             }
274         }
275     }
276 
277     /**
278      * Key used in HashMap that is composed from a BT device-address and device-specific "sub key"
279      */
280     private abstract static class CompositeKey {
281         final String mDeviceAddress;
282         final String mSubKey;
283 
CompositeKey(String deviceAddress, String subKey)284         CompositeKey(String deviceAddress, String subKey) {
285             mDeviceAddress = deviceAddress;
286             mSubKey = subKey;
287         }
288 
289         @Override
equals(Object o)290         public boolean equals(Object o) {
291             if (this == o) {
292                 return true;
293             }
294             if (o == null || getClass() != o.getClass()) {
295                 return false;
296             }
297 
298             CompositeKey that = (CompositeKey) o;
299             return Objects.equals(mDeviceAddress, that.mDeviceAddress)
300                     && Objects.equals(mSubKey, that.mSubKey);
301         }
302 
matches(String deviceAddress)303         boolean matches(String deviceAddress) {
304             return mDeviceAddress.equals(deviceAddress);
305         }
306 
307         @Override
hashCode()308         public int hashCode() {
309             return Objects.hash(mDeviceAddress, mSubKey);
310         }
311 
312         @Override
toString()313         public String toString() {
314             return String.format("%s, deviceAddress: %s, subKey: %s",
315                     getClass().getSimpleName(), mDeviceAddress, mSubKey);
316         }
317     }
318 
319     /**
320      * {@link CompositeKey} subclass used to identify specific messages; it uses message-handle as
321      * the secondary key.
322      */
323     private static class MessageKey extends CompositeKey {
MessageKey(MapMessage message)324         MessageKey(MapMessage message) {
325             super(message.getDevice().getAddress(), message.getHandle());
326         }
327     }
328 
329     /**
330      * CompositeKey used to identify Notification info for a sender; it uses a combination of
331      * senderContactUri and senderContactName as the secondary key.
332      */
333     static class SenderKey extends CompositeKey implements Parcelable {
SenderKey(String deviceAddress, String key)334         private SenderKey(String deviceAddress, String key) {
335             super(deviceAddress, key);
336         }
337 
SenderKey(MapMessage message)338         SenderKey(MapMessage message) {
339             // Use a combination of senderName and senderContactUri for key. Ideally we would use
340             // only senderContactUri (which is encoded phone no.). However since some phones don't
341             // provide these, we fall back to senderName. Since senderName may not be unique, we
342             // include senderContactUri also to provide uniqueness in cases it is available.
343             this(message.getDevice().getAddress(),
344                     message.getSenderName() + "/" + message.getSenderContactUri());
345         }
346 
347         @Override
describeContents()348         public int describeContents() {
349             return 0;
350         }
351 
352         @Override
writeToParcel(Parcel dest, int flags)353         public void writeToParcel(Parcel dest, int flags) {
354             dest.writeString(mDeviceAddress);
355             dest.writeString(mSubKey);
356         }
357 
358         public static final Parcelable.Creator<SenderKey> CREATOR =
359                 new Parcelable.Creator<SenderKey>() {
360             @Override
361             public SenderKey createFromParcel(Parcel source) {
362                 return new SenderKey(source.readString(), source.readString());
363             }
364 
365             @Override
366             public SenderKey[] newArray(int size) {
367                 return new SenderKey[size];
368             }
369         };
370     }
371 
372     /**
373      * Information about a single notification that is displayed.
374      */
375     private static class NotificationInfo {
376         private static int NEXT_NOTIFICATION_ID = 0;
377 
378         final int mNotificationId = NEXT_NOTIFICATION_ID++;
379         final String mSenderName;
380         @Nullable
381         final String mSenderContactUri;
382         final List<MessageKey> mMessageKeys = new LinkedList<>();
383 
NotificationInfo(String senderName, @Nullable String senderContactUri)384         NotificationInfo(String senderName, @Nullable String senderContactUri) {
385             mSenderName = senderName;
386             mSenderContactUri = senderContactUri;
387         }
388     }
389 }
390