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 java.io.IOException; 18 import java.io.UnsupportedEncodingException; 19 import java.text.ParseException; 20 import java.text.SimpleDateFormat; 21 import java.util.ArrayList; 22 import java.util.Date; 23 import java.util.List; 24 25 import org.xmlpull.v1.XmlPullParser; 26 import org.xmlpull.v1.XmlPullParserException; 27 import org.xmlpull.v1.XmlSerializer; 28 29 import android.util.Log; 30 31 import com.android.bluetooth.SignedLongLong; 32 import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 33 import com.android.internal.util.XmlUtils; 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) Log.d(TAG, "setCursorIndex: " + cursorIndex); 70 } 71 getVersionCounter()72 public long getVersionCounter(){ 73 return mVersionCounter; 74 } 75 setVersionCounter(long vcount)76 public void setVersionCounter(long vcount){ 77 if(D) Log.d(TAG, "setVersionCounter: " + vcount); 78 this.mVersionCounter = vcount; 79 } 80 incrementVersionCounter()81 public void incrementVersionCounter() { 82 mVersionCounter++; 83 } 84 setVersionCounter(String vcount)85 private void setVersionCounter(String vcount){ 86 if(D) Log.d(TAG, "setVersionCounter: " + vcount); 87 try { 88 this.mVersionCounter = Long.parseLong(vcount); 89 } catch (NumberFormatException e) { 90 Log.w(TAG, "unable to parse XML versionCounter:" + vcount); 91 mVersionCounter = -1; 92 } 93 } 94 getName()95 public String getName() { 96 return mName; 97 } 98 setName(String name)99 public void setName(String name) { 100 if(D) Log.d(TAG, "setName: " + name); 101 this.mName = name; 102 } 103 getType()104 public TYPE getType() { 105 return mType; 106 } 107 setType(TYPE type)108 public void setType(TYPE type) { 109 this.mType = type; 110 } 111 getContacts()112 public List<BluetoothMapConvoContactElement> getContacts() { 113 return mContacts; 114 } 115 setContacts(List<BluetoothMapConvoContactElement> contacts)116 public void setContacts(List<BluetoothMapConvoContactElement> contacts) { 117 this.mContacts = contacts; 118 } 119 addContact(BluetoothMapConvoContactElement contact)120 public void addContact(BluetoothMapConvoContactElement contact){ 121 if(mContacts == null) 122 mContacts = new ArrayList<BluetoothMapConvoContactElement>(); 123 mContacts.add(contact); 124 } 125 removeContact(BluetoothMapConvoContactElement contact)126 public void removeContact(BluetoothMapConvoContactElement contact){ 127 mContacts.remove(contact); 128 } 129 removeContact(int index)130 public void removeContact(int index){ 131 mContacts.remove(index); 132 } 133 134 getLastActivity()135 public long getLastActivity() { 136 return mLastActivity; 137 } 138 getLastActivityString()139 public String getLastActivityString() { 140 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 141 Date date = new Date(mLastActivity); 142 return format.format(date); // Format to YYYYMMDDTHHMMSS local time 143 } 144 setLastActivity(long last)145 public void setLastActivity(long last) { 146 if(D) Log.d(TAG, "setLastActivity: " + last); 147 this.mLastActivity = last; 148 } 149 setLastActivity(String lastActivity)150 public void setLastActivity(String lastActivity)throws ParseException { 151 // TODO: Encode with time-zone if MCE requests it 152 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 153 Date date = format.parse(lastActivity); 154 this.mLastActivity = date.getTime(); 155 } 156 getRead()157 public String getRead() { 158 if(mReportRead == false) { 159 return "UNKNOWN"; 160 } 161 return (mRead?"READ":"UNREAD"); 162 } 163 getReadBool()164 public boolean getReadBool() { 165 return mRead; 166 } 167 setRead(boolean read, boolean reportRead)168 public void setRead(boolean read, boolean reportRead) { 169 this.mRead = read; 170 if(D) Log.d(TAG, "setRead: " + read); 171 this.mReportRead = reportRead; 172 } 173 setRead(String value)174 private void setRead(String value) { 175 if(value.trim().equalsIgnoreCase("yes")) { 176 mRead = true; 177 } else { 178 mRead = false; 179 } 180 mReportRead = true; 181 } 182 183 /** 184 * Set the conversation ID 185 * @param type 0 if the thread ID is valid across all message types in the instance - else 186 * use one of the CONVO_ID_xxx types. 187 * @param threadId the conversation ID 188 */ setConvoId(long type, long threadId)189 public void setConvoId(long type, long threadId) { 190 this.mId = new SignedLongLong(threadId,type); 191 if(D) Log.d(TAG, "setConvoId: " + threadId + " type:" + type); 192 } 193 getConvoId()194 public String getConvoId(){ 195 return mId.toHexString(); 196 } 197 getCpConvoId()198 public long getCpConvoId() { 199 return mId.getLeastSignificantBits(); 200 } 201 setSummary(String summary)202 public void setSummary(String summary) { 203 mSummary = summary; 204 } 205 getFullSummary()206 public String getFullSummary() { 207 return mSummary; 208 } 209 210 /* Get a valid UTF-8 string of maximum 256 bytes */ getSummary()211 private String getSummary() { 212 if(mSummary != null) { 213 try { 214 return new String(BluetoothMapUtils.truncateUtf8StringToBytearray(mSummary, 256), 215 "UTF-8"); 216 } catch (UnsupportedEncodingException e) { 217 // This cannot happen on an Android platform - UTF-8 is mandatory 218 Log.e(TAG,"Missing UTF-8 support on platform", e); 219 } 220 } 221 return null; 222 } 223 getSmsMmsContacts()224 public String getSmsMmsContacts() { 225 return mSmsMmsContacts; 226 } 227 setSmsMmsContacts(String smsMmsContacts)228 public void setSmsMmsContacts(String smsMmsContacts) { 229 mSmsMmsContacts = smsMmsContacts; 230 } 231 compareTo(BluetoothMapConvoListingElement e)232 public int compareTo(BluetoothMapConvoListingElement e) { 233 if (this.mLastActivity < e.mLastActivity) { 234 return 1; 235 } else if (this.mLastActivity > e.mLastActivity) { 236 return -1; 237 } else { 238 return 0; 239 } 240 } 241 242 /* Encode the MapMessageListingElement into the StringBuilder reference. 243 * Here we have taken the choice not to report empty attributes, to reduce the 244 * amount of data to be transfered over BT. */ encode(XmlSerializer xmlConvoElement)245 public void encode(XmlSerializer xmlConvoElement) 246 throws IllegalArgumentException, IllegalStateException, IOException 247 { 248 249 // contruct the XML tag for a single conversation in the convolisting 250 xmlConvoElement.startTag(null, XML_TAG_CONVERSATION); 251 xmlConvoElement.attribute(null, XML_ATT_ID, mId.toHexString()); 252 if(mName != null) { 253 xmlConvoElement.attribute(null, XML_ATT_NAME, 254 BluetoothMapUtils.stripInvalidChars(mName)); 255 } 256 if(mLastActivity != -1) { 257 xmlConvoElement.attribute(null, XML_ATT_LAST_ACTIVITY, 258 getLastActivityString()); 259 } 260 // Even though this is implied, the value "UNKNOWN" kind of indicated it is required. 261 if(mReportRead == true) { 262 xmlConvoElement.attribute(null, XML_ATT_READ, getRead()); 263 } 264 if(mVersionCounter != -1) { 265 xmlConvoElement.attribute(null, XML_ATT_VERSION_COUNTER, 266 Long.toString(getVersionCounter())); 267 } 268 if(mSummary != null) { 269 xmlConvoElement.attribute(null, XML_ATT_SUMMARY, getSummary()); 270 } 271 if(mContacts != null){ 272 for(BluetoothMapConvoContactElement contact:mContacts){ 273 contact.encode(xmlConvoElement); 274 } 275 } 276 xmlConvoElement.endTag(null, XML_TAG_CONVERSATION); 277 278 } 279 280 /** 281 * Consumes a conversation tag. It is expected that the parser is beyond the start-tag event, 282 * with the name "conversation". 283 * @param parser 284 * @return 285 * @throws XmlPullParserException 286 * @throws IOException 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 if(D) Log.i(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 if(D) Log.i(TAG,"Unknown XML tag: " + name); 326 XmlUtils.skipCurrentTag(parser); 327 continue; 328 } 329 } 330 // As we have extracted all attributes, we should expect an end-tag 331 // parser.nextTag(); // consume the end-tag 332 // TODO: Is this needed? - we should already be at end-tag, as this is the top condition 333 334 return newElement; 335 } 336 337 @Override equals(Object obj)338 public boolean equals(Object obj) { 339 if (this == obj) { 340 return true; 341 } 342 if (obj == null) { 343 return false; 344 } 345 if (getClass() != obj.getClass()) { 346 return false; 347 } 348 BluetoothMapConvoListingElement other = (BluetoothMapConvoListingElement) obj; 349 if (mContacts == null) { 350 if (other.mContacts != null) { 351 return false; 352 } 353 } else if (!mContacts.equals(other.mContacts)) { 354 return false; 355 } 356 /* As we use equals only for test, we don't compare auto assigned values 357 * if (mId == null) { 358 if (other.mId != null) { 359 return false; 360 } 361 } else if (!mId.equals(other.mId)) { 362 return false; 363 } */ 364 365 if (mLastActivity != other.mLastActivity) { 366 return false; 367 } 368 if (mName == null) { 369 if (other.mName != null) { 370 return false; 371 } 372 } else if (!mName.equals(other.mName)) { 373 return false; 374 } 375 if (mRead != other.mRead) { 376 return false; 377 } 378 return true; 379 } 380 381 /* @Override 382 public boolean equals(Object o) { 383 384 return true; 385 }; 386 */ 387 388 } 389 390 391