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.Utils; 21 import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 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 BluetoothMapUtils.truncateUtf8StringToString(mSummary, 256); 230 } catch (UnsupportedEncodingException e) { 231 // This cannot happen on an Android platform - UTF-8 is mandatory 232 Log.e(TAG, "Missing UTF-8 support on platform", e); 233 } 234 } 235 return null; 236 } 237 getSmsMmsContacts()238 public String getSmsMmsContacts() { 239 return mSmsMmsContacts; 240 } 241 setSmsMmsContacts(String smsMmsContacts)242 public void setSmsMmsContacts(String smsMmsContacts) { 243 mSmsMmsContacts = smsMmsContacts; 244 } 245 246 @Override compareTo(BluetoothMapConvoListingElement e)247 public int compareTo(BluetoothMapConvoListingElement e) { 248 if (this.mLastActivity < e.mLastActivity) { 249 return 1; 250 } else if (this.mLastActivity > e.mLastActivity) { 251 return -1; 252 } else { 253 return 0; 254 } 255 } 256 257 /* Encode the MapMessageListingElement into the StringBuilder reference. 258 * Here we have taken the choice not to report empty attributes, to reduce the 259 * amount of data to be transfered over BT. */ encode(XmlSerializer xmlConvoElement)260 public void encode(XmlSerializer xmlConvoElement) 261 throws IllegalArgumentException, IllegalStateException, IOException { 262 263 // contruct the XML tag for a single conversation in the convolisting 264 xmlConvoElement.startTag(null, XML_TAG_CONVERSATION); 265 xmlConvoElement.attribute(null, XML_ATT_ID, mId.toHexString()); 266 if (mName != null) { 267 xmlConvoElement.attribute(null, XML_ATT_NAME, 268 BluetoothMapUtils.stripInvalidChars(mName)); 269 } 270 if (mLastActivity != -1) { 271 xmlConvoElement.attribute(null, XML_ATT_LAST_ACTIVITY, getLastActivityString()); 272 } 273 // Even though this is implied, the value "UNKNOWN" kind of indicated it is required. 274 if (mReportRead) { 275 xmlConvoElement.attribute(null, XML_ATT_READ, getRead()); 276 } 277 if (mVersionCounter != -1) { 278 xmlConvoElement.attribute(null, XML_ATT_VERSION_COUNTER, 279 Long.toString(getVersionCounter())); 280 } 281 if (mSummary != null) { 282 xmlConvoElement.attribute(null, XML_ATT_SUMMARY, getSummary()); 283 } 284 if (mContacts != null) { 285 for (BluetoothMapConvoContactElement contact : mContacts) { 286 contact.encode(xmlConvoElement); 287 } 288 } 289 xmlConvoElement.endTag(null, XML_TAG_CONVERSATION); 290 291 } 292 293 /** 294 * Consumes a conversation tag. It is expected that the parser is beyond the start-tag event, 295 * with the name "conversation". 296 * @param parser 297 * @return 298 * @throws XmlPullParserException 299 * @throws IOException 300 */ createFromXml(XmlPullParser parser)301 public static BluetoothMapConvoListingElement createFromXml(XmlPullParser parser) 302 throws XmlPullParserException, IOException, ParseException { 303 BluetoothMapConvoListingElement newElement = new BluetoothMapConvoListingElement(); 304 int count = parser.getAttributeCount(); 305 int type; 306 for (int i = 0; i < count; i++) { 307 String attributeName = parser.getAttributeName(i).trim(); 308 String attributeValue = parser.getAttributeValue(i); 309 if (attributeName.equalsIgnoreCase(XML_ATT_ID)) { 310 newElement.mId = SignedLongLong.fromString(attributeValue); 311 } else if (attributeName.equalsIgnoreCase(XML_ATT_NAME)) { 312 newElement.mName = attributeValue; 313 } else if (attributeName.equalsIgnoreCase(XML_ATT_LAST_ACTIVITY)) { 314 newElement.setLastActivity(attributeValue); 315 } else if (attributeName.equalsIgnoreCase(XML_ATT_READ)) { 316 newElement.setRead(attributeValue); 317 } else if (attributeName.equalsIgnoreCase(XML_ATT_VERSION_COUNTER)) { 318 newElement.setVersionCounter(attributeValue); 319 } else if (attributeName.equalsIgnoreCase(XML_ATT_SUMMARY)) { 320 newElement.setSummary(attributeValue); 321 } else { 322 if (D) { 323 Log.i(TAG, "Unknown XML attribute: " + parser.getAttributeName(i)); 324 } 325 } 326 } 327 328 // Now determine if we get an end-tag, or a new start tag for contacts 329 while ((type = parser.next()) != XmlPullParser.END_TAG 330 && type != XmlPullParser.END_DOCUMENT) { 331 // Skip until we get a start tag 332 if (parser.getEventType() != XmlPullParser.START_TAG) { 333 continue; 334 } 335 // Skip until we get a convocontact tag 336 String name = parser.getName().trim(); 337 if (name.equalsIgnoreCase(BluetoothMapConvoContactElement.XML_TAG_CONVOCONTACT)) { 338 newElement.addContact(BluetoothMapConvoContactElement.createFromXml(parser)); 339 } else { 340 if (D) { 341 Log.i(TAG, "Unknown XML tag: " + name); 342 } 343 Utils.skipCurrentTag(parser); 344 continue; 345 } 346 } 347 // As we have extracted all attributes, we should expect an end-tag 348 // parser.nextTag(); // consume the end-tag 349 // TODO: Is this needed? - we should already be at end-tag, as this is the top condition 350 351 return newElement; 352 } 353 354 @Override equals(Object obj)355 public boolean equals(Object obj) { 356 if (this == obj) { 357 return true; 358 } 359 if (obj == null) { 360 return false; 361 } 362 if (getClass() != obj.getClass()) { 363 return false; 364 } 365 BluetoothMapConvoListingElement other = (BluetoothMapConvoListingElement) obj; 366 if (mContacts == null) { 367 if (other.mContacts != null) { 368 return false; 369 } 370 } else if (!mContacts.equals(other.mContacts)) { 371 return false; 372 } 373 /* As we use equals only for test, we don't compare auto assigned values 374 * if (mId == null) { 375 if (other.mId != null) { 376 return false; 377 } 378 } else if (!mId.equals(other.mId)) { 379 return false; 380 } */ 381 382 if (mLastActivity != other.mLastActivity) { 383 return false; 384 } 385 if (mName == null) { 386 if (other.mName != null) { 387 return false; 388 } 389 } else if (!mName.equals(other.mName)) { 390 return false; 391 } 392 if (mRead != other.mRead) { 393 return false; 394 } 395 return true; 396 } 397 398 /* @Override 399 public boolean equals(Object o) { 400 401 return true; 402 }; 403 */ 404 405 } 406 407 408