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.bluetooth.BluetoothDevice; 25 import android.content.Intent; 26 import android.util.Log; 27 28 import androidx.annotation.Nullable; 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)) { 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)) { 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