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