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 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                 Utils.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