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