1 /*
2  * Copyright (C) 2019 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.common;
18 
19 import static com.android.car.apps.common.util.SafeLog.logw;
20 import static com.android.car.messenger.common.Utils.BMC_EXTRA_MESSAGE_HANDLE;
21 import static com.android.car.messenger.common.Utils.BMC_EXTRA_MESSAGE_READ_STATUS;
22 import static com.android.car.messenger.common.Utils.BMC_EXTRA_MESSAGE_TIMESTAMP;
23 
24 import android.annotation.Nullable;
25 import android.bluetooth.BluetoothDevice;
26 import android.content.Intent;
27 import android.os.Build;
28 import android.util.Log;
29 
30 import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
31 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
32 
33 
34 /**
35  * Represents a SMS, MMS, and {@link NotificationMsg}. This object is based
36  * on {@link NotificationMsg}.
37  */
38 public class Message {
39     private static final String TAG = "CMC.Message";
40 
41     private final String mSenderName;
42     private final String mDeviceId;
43     private final String mMessageText;
44     private final long mReceivedTime;
45     private final boolean mIsReadOnPhone;
46     private boolean mShouldExclude;
47     private final String mHandle;
48     private final MessageType mMessageType;
49     private final SenderKey mSenderKey;
50 
51 
52     /**
53      * Note: MAP messages from iOS version 12 and earlier, as well as {@link MessagingStyleMessage},
54      * don't provide these.
55      */
56     @Nullable
57     final String mSenderContactUri;
58 
59     /**
60      * Describes if the message was received through Bluetooth MAP or is a {@link NotificationMsg}.
61      */
62     public enum MessageType {
63         BLUETOOTH_MAP_MESSAGE, NOTIFICATION_MESSAGE
64     }
65 
66     /**
67      * Creates a Message based on {@link MessagingStyleMessage}. Returns {@code null} if the {@link
68      * MessagingStyleMessage} is missing required fields.
69      *
70      * @param deviceId of the phone that received this message.
71      * @param updatedMessage containing the information to base this message object off of.
72      * @param senderKey of the sender of the message. Not guaranteed to be unique for all senders
73      *                  if this message is part of a group conversation.
74      **/
75     @Nullable
parseFromMessage(String deviceId, MessagingStyleMessage updatedMessage, SenderKey senderKey)76     public static Message parseFromMessage(String deviceId,
77             MessagingStyleMessage updatedMessage, SenderKey senderKey) {
78 
79         if (!Utils.isValidMessagingStyleMessage(updatedMessage)) {
80             if (Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE) {
81                 throw new IllegalArgumentException(
82                         "MessagingStyleMessage is missing required fields");
83             } else {
84                 logw(TAG, "MessagingStyleMessage is missing required fields");
85                 return null;
86             }
87         }
88 
89         return new Message(updatedMessage.getSender().getName(),
90                 deviceId,
91                 updatedMessage.getTextMessage(),
92                 updatedMessage.getTimestamp(),
93                 updatedMessage.getIsRead(),
94                 Utils.createMessageHandle(updatedMessage),
95                 MessageType.NOTIFICATION_MESSAGE,
96                 /* senderContactUri */ null,
97                 senderKey);
98     }
99 
100     /**
101      * Creates a Message based on BluetoothMapClient intent. Returns {@code null} if the
102      * intent is missing required fields.
103      **/
parseFromIntent(Intent intent)104     public static Message parseFromIntent(Intent intent) {
105         if (!Utils.isValidMapClientIntent(intent)) {
106             if (Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE) {
107                 throw new IllegalArgumentException(
108                         "BluetoothMapClient intent is missing required fields");
109             } else {
110                 logw(TAG, "BluetoothMapClient intent is missing required fields");
111                 return null;
112             }
113         }
114         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
115         String senderUri = Utils.getSenderUri(intent);
116 
117         return new Message(
118                 Utils.getSenderName(intent),
119                 device.getAddress(),
120                 intent.getStringExtra(android.content.Intent.EXTRA_TEXT),
121                 intent.getLongExtra(BMC_EXTRA_MESSAGE_TIMESTAMP,
122                         System.currentTimeMillis()),
123                 intent.getBooleanExtra(BMC_EXTRA_MESSAGE_READ_STATUS,
124                         false),
125                 intent.getStringExtra(BMC_EXTRA_MESSAGE_HANDLE),
126                 MessageType.BLUETOOTH_MAP_MESSAGE,
127                 senderUri,
128                 SenderKey.createSenderKey(intent)
129         );
130     }
131 
Message(String senderName, String deviceId, String messageText, long receivedTime, boolean isReadOnPhone, String handle, MessageType messageType, @Nullable String senderContactUri, SenderKey senderKey)132     private Message(String senderName, String deviceId, String messageText, long receivedTime,
133             boolean isReadOnPhone, String handle, MessageType messageType,
134             @Nullable String senderContactUri, SenderKey senderKey) {
135         boolean missingSenderName = (senderName == null);
136         boolean missingDeviceId = (deviceId == null);
137         boolean missingText = (messageText == null);
138         boolean missingHandle = (handle == null);
139         boolean missingType = (messageType == null);
140         if (missingSenderName || missingDeviceId || missingText || missingHandle || missingType) {
141             StringBuilder builder = new StringBuilder("Missing required fields:");
142             if (missingSenderName) {
143                 builder.append(" senderName");
144             }
145             if (missingDeviceId) {
146                 builder.append(" deviceId");
147             }
148             if (missingText) {
149                 builder.append(" messageText");
150             }
151             if (missingHandle) {
152                 builder.append(" handle");
153             }
154             if (missingType) {
155                 builder.append(" type");
156             }
157             throw new IllegalArgumentException(builder.toString());
158         }
159         this.mSenderName = senderName;
160         this.mDeviceId = deviceId;
161         this.mMessageText = messageText;
162         this.mReceivedTime = receivedTime;
163         this.mIsReadOnPhone = isReadOnPhone;
164         this.mShouldExclude = false;
165         this.mHandle = handle;
166         this.mMessageType = messageType;
167         this.mSenderContactUri = senderContactUri;
168         this.mSenderKey = senderKey;
169     }
170 
171     /**
172      * Returns the contact name as obtained from the device.
173      * If contact is in the device's address-book, this is typically the contact name.
174      * Otherwise it will be the phone number.
175      */
getSenderName()176     public String getSenderName() {
177         return mSenderName;
178     }
179 
180     /**
181      * Returns the id of the device from which this message was received.
182      */
getDeviceId()183     public String getDeviceId() {
184         return mDeviceId;
185     }
186 
187     /**
188      * Returns the actual content of the message.
189      */
getMessageText()190     public String getMessageText() {
191         return mMessageText;
192     }
193 
194     /**
195      * Returns the milliseconds since epoch at which this message notification was received on the
196      * head-unit.
197      */
getReceivedTime()198     public long getReceivedTime() {
199         return mReceivedTime;
200     }
201 
202     /**
203      * Whether message should be included in the notification. Messages that have been read aloud on
204      * the car, or that have been dismissed by the user should be excluded from the notification if/
205      * when the notification gets updated. Note: this state will not be propagated to the phone.
206      */
excludeFromNotification()207     public void excludeFromNotification() {
208         mShouldExclude = true;
209     }
210 
211     /**
212      * Returns {@code true} if message was read on the phone before it was received on the car.
213      */
isReadOnPhone()214     public boolean isReadOnPhone() {
215         return mIsReadOnPhone;
216     }
217 
218     /**
219      * Returns {@code true} if message should not be included in the notification. Messages that
220      * have been read aloud on the car, or that have been dismissed by the user should be excluded
221      * from the notification if/when the notification gets updated.
222      */
shouldExcludeFromNotification()223     public boolean shouldExcludeFromNotification() {
224         return mShouldExclude;
225     }
226 
227     /**
228      * Returns a unique handle/key for this message. This is used as this Message's
229      * {@link MessageKey#getSubKey()} Note: this handle might only be unique for the lifetime of a
230      * device connection session.
231      */
getHandle()232     public String getHandle() {
233         return mHandle;
234     }
235 
236     /**
237      * If the message came from BluetoothMapClient, this retrieves a key that is unique
238      * for each contact per device.
239      * If the message came from {@link NotificationMsg}, this retrieves a key that is only
240      * guaranteed to be unique per sender in a 1-1 conversation. If this message is part of a
241      * group conversation, the senderKey will not be unique if more than one participant in the
242      * conversation share the same name.
243      */
getSenderKey()244     public SenderKey getSenderKey() {
245         return mSenderKey;
246     }
247 
248     /** Returns whether the message is a SMS/MMS or a {@link NotificationMsg} **/
getMessageType()249     public MessageType getMessageType() {
250         return mMessageType;
251     }
252 
253     /**
254      * Returns the sender's phone number available as a URI string.
255      * Note: MAP messages from iOS version 12 and earlier, as well as {@link MessagingStyleMessage},
256      * don't provide these.
257      */
258     @Nullable
getSenderContactUri()259     public String getSenderContactUri() {
260         return mSenderContactUri;
261     }
262 
263     @Override
toString()264     public String toString() {
265         return "Message{"
266                 + " mSenderName='" + mSenderName + '\''
267                 + ", mMessageText='" + mMessageText + '\''
268                 + ", mSenderContactUri='" + mSenderContactUri + '\''
269                 + ", mReceiveTime=" + mReceivedTime + '\''
270                 + ", mIsReadOnPhone= " + mIsReadOnPhone + '\''
271                 + ", mShouldExclude= " + mShouldExclude + '\''
272                 + ", mHandle='" + mHandle + '\''
273                 + ", mSenderKey='" + mSenderKey.toString()
274                 + "}";
275     }
276 }
277