1 /* 2 * Copyright (C) 2013 Samsung System LSI 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.android.bluetooth.map; 16 17 import android.bluetooth.BluetoothProfile; 18 import android.bluetooth.BluetoothProtoEnums; 19 import android.util.Log; 20 21 import com.android.bluetooth.BluetoothStatsLog; 22 import com.android.bluetooth.SignedLongLong; 23 import com.android.bluetooth.Utils; 24 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 25 import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 26 27 import org.xmlpull.v1.XmlPullParser; 28 import org.xmlpull.v1.XmlPullParserException; 29 import org.xmlpull.v1.XmlSerializer; 30 31 import java.io.IOException; 32 import java.text.ParseException; 33 import java.text.SimpleDateFormat; 34 import java.util.ArrayList; 35 import java.util.Date; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.Objects; 39 40 // Next tag value for ContentProfileErrorReportUtils.report(): 2 41 public class BluetoothMapConvoListingElement 42 implements Comparable<BluetoothMapConvoListingElement> { 43 private static final String TAG = BluetoothMapConvoListingElement.class.getSimpleName(); 44 45 public static final String XML_TAG_CONVERSATION = "conversation"; 46 private static final String XML_ATT_LAST_ACTIVITY = "last_activity"; 47 private static final String XML_ATT_NAME = "name"; 48 private static final String XML_ATT_ID = "id"; 49 private static final String XML_ATT_READ = "readstatus"; 50 private static final String XML_ATT_VERSION_COUNTER = "version_counter"; 51 private static final String XML_ATT_SUMMARY = "summary"; 52 53 private SignedLongLong mId = null; 54 private String mName = ""; // title of the conversation #REQUIRED, but allowed empty 55 private long mLastActivity = -1; 56 private boolean mRead = false; 57 private boolean mReportRead = false; // TODO: Is this needed? - false means UNKNOWN 58 private List<BluetoothMapConvoContactElement> mContacts; 59 private long mVersionCounter = -1; 60 private int mCursorIndex = 0; 61 private TYPE mType = null; 62 private String mSummary = null; 63 64 // Used only to keep track of changes to convoListVersionCounter; 65 private String mSmsMmsContacts = null; 66 getCursorIndex()67 public int getCursorIndex() { 68 return mCursorIndex; 69 } 70 setCursorIndex(int cursorIndex)71 public void setCursorIndex(int cursorIndex) { 72 this.mCursorIndex = cursorIndex; 73 Log.d(TAG, "setCursorIndex: " + cursorIndex); 74 } 75 getVersionCounter()76 public long getVersionCounter() { 77 return mVersionCounter; 78 } 79 setVersionCounter(long vcount)80 public void setVersionCounter(long vcount) { 81 Log.d(TAG, "setVersionCounter: " + vcount); 82 this.mVersionCounter = vcount; 83 } 84 incrementVersionCounter()85 public void incrementVersionCounter() { 86 mVersionCounter++; 87 } 88 setVersionCounter(String vcount)89 private void setVersionCounter(String vcount) { 90 Log.d(TAG, "setVersionCounter: " + vcount); 91 try { 92 this.mVersionCounter = Long.parseLong(vcount); 93 } catch (NumberFormatException e) { 94 ContentProfileErrorReportUtils.report( 95 BluetoothProfile.MAP, 96 BluetoothProtoEnums.BLUETOOTH_MAP_CONVO_LISTING_ELEMENT, 97 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 98 0); 99 Log.w(TAG, "unable to parse XML versionCounter:" + vcount); 100 mVersionCounter = -1; 101 } 102 } 103 getName()104 public String getName() { 105 return mName; 106 } 107 setName(String name)108 public void setName(String name) { 109 Log.d(TAG, "setName: " + name); 110 this.mName = name; 111 } 112 getType()113 public TYPE getType() { 114 return mType; 115 } 116 setType(TYPE type)117 public void setType(TYPE type) { 118 this.mType = type; 119 } 120 getContacts()121 public List<BluetoothMapConvoContactElement> getContacts() { 122 return mContacts; 123 } 124 setContacts(List<BluetoothMapConvoContactElement> contacts)125 public void setContacts(List<BluetoothMapConvoContactElement> contacts) { 126 this.mContacts = contacts; 127 } 128 addContact(BluetoothMapConvoContactElement contact)129 public void addContact(BluetoothMapConvoContactElement contact) { 130 if (mContacts == null) { 131 mContacts = new ArrayList<BluetoothMapConvoContactElement>(); 132 } 133 mContacts.add(contact); 134 } 135 removeContact(BluetoothMapConvoContactElement contact)136 public void removeContact(BluetoothMapConvoContactElement contact) { 137 mContacts.remove(contact); 138 } 139 removeContact(int index)140 public void removeContact(int index) { 141 mContacts.remove(index); 142 } 143 getLastActivity()144 public long getLastActivity() { 145 return mLastActivity; 146 } 147 148 @SuppressWarnings("JavaUtilDate") // TODO: b/365629730 -- prefer Instant or LocalDate getLastActivityString()149 public String getLastActivityString() { 150 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT); 151 Date date = new Date(mLastActivity); 152 return format.format(date); // Format to YYYYMMDDTHHMMSS local time 153 } 154 setLastActivity(long last)155 public void setLastActivity(long last) { 156 Log.d(TAG, "setLastActivity: " + last); 157 this.mLastActivity = last; 158 } 159 160 @SuppressWarnings("JavaUtilDate") // TODO: b/365629730 -- prefer Instant or LocalDate setLastActivity(String lastActivity)161 public void setLastActivity(String lastActivity) throws ParseException { 162 // TODO: Encode with time-zone if MCE requests it 163 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT); 164 Date date = format.parse(lastActivity); 165 this.mLastActivity = date.getTime(); 166 } 167 getRead()168 public String getRead() { 169 if (!mReportRead) { 170 return "UNKNOWN"; 171 } 172 return (mRead ? "READ" : "UNREAD"); 173 } 174 getReadBool()175 public boolean getReadBool() { 176 return mRead; 177 } 178 setRead(boolean read, boolean reportRead)179 public void setRead(boolean read, boolean reportRead) { 180 this.mRead = read; 181 Log.d(TAG, "setRead: " + read); 182 this.mReportRead = reportRead; 183 } 184 setRead(String value)185 private void setRead(String value) { 186 if (value.trim().equalsIgnoreCase("yes")) { 187 mRead = true; 188 } else { 189 mRead = false; 190 } 191 mReportRead = true; 192 } 193 194 /** 195 * Set the conversation ID 196 * 197 * @param type 0 if the thread ID is valid across all message types in the instance - else use 198 * one of the CONVO_ID_xxx types. 199 * @param threadId the conversation ID 200 */ setConvoId(long type, long threadId)201 public void setConvoId(long type, long threadId) { 202 this.mId = new SignedLongLong(threadId, type); 203 Log.d(TAG, "setConvoId: " + threadId + " type:" + type); 204 } 205 getConvoId()206 public String getConvoId() { 207 return mId.toHexString(); 208 } 209 getCpConvoId()210 public long getCpConvoId() { 211 return mId.leastSignificantBits(); 212 } 213 setSummary(String summary)214 public void setSummary(String summary) { 215 mSummary = summary; 216 } 217 getFullSummary()218 public String getFullSummary() { 219 return mSummary; 220 } 221 222 /* Get a valid UTF-8 string of maximum 256 bytes */ getSummary()223 private String getSummary() { 224 if (mSummary != null) { 225 return BluetoothMapUtils.truncateUtf8StringToString(mSummary, 256); 226 } 227 return null; 228 } 229 getSmsMmsContacts()230 public String getSmsMmsContacts() { 231 return mSmsMmsContacts; 232 } 233 setSmsMmsContacts(String smsMmsContacts)234 public void setSmsMmsContacts(String smsMmsContacts) { 235 mSmsMmsContacts = smsMmsContacts; 236 } 237 238 @Override compareTo(BluetoothMapConvoListingElement e)239 public int compareTo(BluetoothMapConvoListingElement e) { 240 if (this.mLastActivity < e.mLastActivity) { 241 return 1; 242 } else if (this.mLastActivity > e.mLastActivity) { 243 return -1; 244 } else { 245 return 0; 246 } 247 } 248 249 /* Encode the MapMessageListingElement into the StringBuilder reference. 250 * Here we have taken the choice not to report empty attributes, to reduce the 251 * amount of data to be transferred over BT. */ encode(XmlSerializer xmlConvoElement)252 public void encode(XmlSerializer xmlConvoElement) 253 throws IllegalArgumentException, IllegalStateException, IOException { 254 255 // construct the XML tag for a single conversation in the convolisting 256 xmlConvoElement.startTag(null, XML_TAG_CONVERSATION); 257 xmlConvoElement.attribute(null, XML_ATT_ID, mId.toHexString()); 258 if (mName != null) { 259 xmlConvoElement.attribute( 260 null, XML_ATT_NAME, BluetoothMapUtils.stripInvalidChars(mName)); 261 } 262 if (mLastActivity != -1) { 263 xmlConvoElement.attribute(null, XML_ATT_LAST_ACTIVITY, getLastActivityString()); 264 } 265 // Even though this is implied, the value "UNKNOWN" kind of indicated it is required. 266 if (mReportRead) { 267 xmlConvoElement.attribute(null, XML_ATT_READ, getRead()); 268 } 269 if (mVersionCounter != -1) { 270 xmlConvoElement.attribute( 271 null, XML_ATT_VERSION_COUNTER, Long.toString(getVersionCounter())); 272 } 273 if (mSummary != null) { 274 xmlConvoElement.attribute(null, XML_ATT_SUMMARY, getSummary()); 275 } 276 if (mContacts != null) { 277 for (BluetoothMapConvoContactElement contact : mContacts) { 278 contact.encode(xmlConvoElement); 279 } 280 } 281 xmlConvoElement.endTag(null, XML_TAG_CONVERSATION); 282 } 283 284 /** 285 * Consumes a conversation tag. It is expected that the parser is beyond the start-tag event, 286 * with the name "conversation". 287 */ createFromXml(XmlPullParser parser)288 public static BluetoothMapConvoListingElement createFromXml(XmlPullParser parser) 289 throws XmlPullParserException, IOException, ParseException { 290 BluetoothMapConvoListingElement newElement = new BluetoothMapConvoListingElement(); 291 int count = parser.getAttributeCount(); 292 int type; 293 for (int i = 0; i < count; i++) { 294 String attributeName = parser.getAttributeName(i).trim(); 295 String attributeValue = parser.getAttributeValue(i); 296 if (attributeName.equalsIgnoreCase(XML_ATT_ID)) { 297 newElement.mId = SignedLongLong.fromString(attributeValue); 298 } else if (attributeName.equalsIgnoreCase(XML_ATT_NAME)) { 299 newElement.mName = attributeValue; 300 } else if (attributeName.equalsIgnoreCase(XML_ATT_LAST_ACTIVITY)) { 301 newElement.setLastActivity(attributeValue); 302 } else if (attributeName.equalsIgnoreCase(XML_ATT_READ)) { 303 newElement.setRead(attributeValue); 304 } else if (attributeName.equalsIgnoreCase(XML_ATT_VERSION_COUNTER)) { 305 newElement.setVersionCounter(attributeValue); 306 } else if (attributeName.equalsIgnoreCase(XML_ATT_SUMMARY)) { 307 newElement.setSummary(attributeValue); 308 } else { 309 Log.w(TAG, "Unknown XML attribute: " + parser.getAttributeName(i)); 310 } 311 } 312 313 // Now determine if we get an end-tag, or a new start tag for contacts 314 while ((type = parser.next()) != XmlPullParser.END_TAG 315 && type != XmlPullParser.END_DOCUMENT) { 316 // Skip until we get a start tag 317 if (parser.getEventType() != XmlPullParser.START_TAG) { 318 continue; 319 } 320 // Skip until we get a convocontact tag 321 String name = parser.getName().trim(); 322 if (name.equalsIgnoreCase(BluetoothMapConvoContactElement.XML_TAG_CONVOCONTACT)) { 323 newElement.addContact(BluetoothMapConvoContactElement.createFromXml(parser)); 324 } else { 325 Log.w(TAG, "Unknown XML tag: " + name); 326 Utils.skipCurrentTag(parser); 327 } 328 } 329 // As we have extracted all attributes, we should expect an end-tag 330 // parser.nextTag(); // consume the end-tag 331 // TODO: Is this needed? - we should already be at end-tag, as this is the top condition 332 333 return newElement; 334 } 335 336 @Override equals(Object obj)337 public boolean equals(Object obj) { 338 if (this == obj) { 339 return true; 340 } 341 if (!(obj instanceof BluetoothMapConvoListingElement other)) { 342 return false; 343 } 344 345 if (!Objects.equals(mContacts, other.mContacts)) { 346 return false; 347 } 348 349 // Skip comparing auto assigned value `mId`. Equals is only used for test 350 351 if (mLastActivity != other.mLastActivity) { 352 return false; 353 } 354 if (!Objects.equals(mName, other.mName)) { 355 return false; 356 } 357 if (mRead != other.mRead) { 358 return false; 359 } 360 return true; 361 } 362 363 @Override hashCode()364 public int hashCode() { 365 return Objects.hash(mContacts, mLastActivity, mName, mRead); 366 } 367 } 368