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