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