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.util.Log; 18 19 import com.android.bluetooth.SignedLongLong; 20 import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 21 import com.android.internal.util.XmlUtils; 22 23 import org.xmlpull.v1.XmlPullParser; 24 import org.xmlpull.v1.XmlPullParserException; 25 import org.xmlpull.v1.XmlSerializer; 26 27 import java.io.IOException; 28 import java.io.UnsupportedEncodingException; 29 import java.text.ParseException; 30 import java.text.SimpleDateFormat; 31 import java.util.ArrayList; 32 import java.util.Date; 33 import java.util.List; 34 35 public 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 getCursorIndex()63 public int getCursorIndex() { 64 return mCursorIndex; 65 } 66 setCursorIndex(int cursorIndex)67 public void setCursorIndex(int cursorIndex) { 68 this.mCursorIndex = cursorIndex; 69 if (D) { 70 Log.d(TAG, "setCursorIndex: " + cursorIndex); 71 } 72 } 73 getVersionCounter()74 public long getVersionCounter() { 75 return mVersionCounter; 76 } 77 setVersionCounter(long vcount)78 public void setVersionCounter(long vcount) { 79 if (D) { 80 Log.d(TAG, "setVersionCounter: " + vcount); 81 } 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 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 getName()101 public String getName() { 102 return mName; 103 } 104 setName(String name)105 public void setName(String name) { 106 if (D) { 107 Log.d(TAG, "setName: " + name); 108 } 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 143 getLastActivity()144 public long getLastActivity() { 145 return mLastActivity; 146 } 147 getLastActivityString()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 setLastActivity(long last)154 public void setLastActivity(long last) { 155 if (D) { 156 Log.d(TAG, "setLastActivity: " + last); 157 } 158 this.mLastActivity = last; 159 } 160 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"); 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 if (D) { 182 Log.d(TAG, "setRead: " + read); 183 } 184 this.mReportRead = reportRead; 185 } 186 setRead(String value)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 */ setConvoId(long type, long threadId)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 getConvoId()209 public String getConvoId() { 210 return mId.toHexString(); 211 } 212 getCpConvoId()213 public long getCpConvoId() { 214 return mId.getLeastSignificantBits(); 215 } 216 setSummary(String summary)217 public void setSummary(String summary) { 218 mSummary = summary; 219 } 220 getFullSummary()221 public String getFullSummary() { 222 return mSummary; 223 } 224 225 /* Get a valid UTF-8 string of maximum 256 bytes */ getSummary()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 getSmsMmsContacts()239 public String getSmsMmsContacts() { 240 return mSmsMmsContacts; 241 } 242 setSmsMmsContacts(String smsMmsContacts)243 public void setSmsMmsContacts(String smsMmsContacts) { 244 mSmsMmsContacts = smsMmsContacts; 245 } 246 247 @Override compareTo(BluetoothMapConvoListingElement e)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. */ encode(XmlSerializer xmlConvoElement)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 */ createFromXml(XmlPullParser parser)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 equals(Object obj)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