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*/
15package com.android.bluetooth.map;
16
17import android.util.Log;
18
19import com.android.bluetooth.SignedLongLong;
20import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
21import com.android.internal.util.XmlUtils;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25import org.xmlpull.v1.XmlSerializer;
26
27import java.io.IOException;
28import java.io.UnsupportedEncodingException;
29import java.text.ParseException;
30import java.text.SimpleDateFormat;
31import java.util.ArrayList;
32import java.util.Date;
33import java.util.List;
34
35public 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
63    public int getCursorIndex() {
64        return mCursorIndex;
65    }
66
67    public void setCursorIndex(int cursorIndex) {
68        this.mCursorIndex = cursorIndex;
69        if (D) {
70            Log.d(TAG, "setCursorIndex: " + cursorIndex);
71        }
72    }
73
74    public long getVersionCounter() {
75        return mVersionCounter;
76    }
77
78    public void setVersionCounter(long vcount) {
79        if (D) {
80            Log.d(TAG, "setVersionCounter: " + vcount);
81        }
82        this.mVersionCounter = vcount;
83    }
84
85    public void incrementVersionCounter() {
86        mVersionCounter++;
87    }
88
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
101    public String getName() {
102        return mName;
103    }
104
105    public void setName(String name) {
106        if (D) {
107            Log.d(TAG, "setName: " + name);
108        }
109        this.mName = name;
110    }
111
112    public TYPE getType() {
113        return mType;
114    }
115
116    public void setType(TYPE type) {
117        this.mType = type;
118    }
119
120    public List<BluetoothMapConvoContactElement> getContacts() {
121        return mContacts;
122    }
123
124    public void setContacts(List<BluetoothMapConvoContactElement> contacts) {
125        this.mContacts = contacts;
126    }
127
128    public void addContact(BluetoothMapConvoContactElement contact) {
129        if (mContacts == null) {
130            mContacts = new ArrayList<BluetoothMapConvoContactElement>();
131        }
132        mContacts.add(contact);
133    }
134
135    public void removeContact(BluetoothMapConvoContactElement contact) {
136        mContacts.remove(contact);
137    }
138
139    public void removeContact(int index) {
140        mContacts.remove(index);
141    }
142
143
144    public long getLastActivity() {
145        return mLastActivity;
146    }
147
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
154    public void setLastActivity(long last) {
155        if (D) {
156            Log.d(TAG, "setLastActivity: " + last);
157        }
158        this.mLastActivity = last;
159    }
160
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
168    public String getRead() {
169        if (!mReportRead) {
170            return "UNKNOWN";
171        }
172        return (mRead ? "READ" : "UNREAD");
173    }
174
175    public boolean getReadBool() {
176        return mRead;
177    }
178
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
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     */
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
209    public String getConvoId() {
210        return mId.toHexString();
211    }
212
213    public long getCpConvoId() {
214        return mId.getLeastSignificantBits();
215    }
216
217    public void setSummary(String summary) {
218        mSummary = summary;
219    }
220
221    public String getFullSummary() {
222        return mSummary;
223    }
224
225    /* Get a valid UTF-8 string of maximum 256 bytes */
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
239    public String getSmsMmsContacts() {
240        return mSmsMmsContacts;
241    }
242
243    public void setSmsMmsContacts(String smsMmsContacts) {
244        mSmsMmsContacts = smsMmsContacts;
245    }
246
247    @Override
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. */
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     */
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                XmlUtils.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
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